Windows標準のssh.exeで謎のエラーが表示される理由を探る


# 状況
最近のWindowsではコンパネ経由でOpenSSH(クライアント・サーバともに)をインストール[^openssh-install]することが出来ます。

さっそく使ってみたんですが~~いい感じに~~辛いログが出る。

```console
PS > ssh -V
OpenSSH_for_Windows_8.1p1, LibreSSL 3.0.2
PS > ssh hoge
ssh: Could not resolve hostname hoge: \202\273\202\314\202\346\202\244\202\310\203z\203X\203g\202\315\225s\226\276\202\305\202\267\201B
PS >
```

[^openssh-install]: やり方については[OpenSSH をインストールする](https://learn.microsoft.com/ja-jp/windows-server/administration/openssh/openssh_install_firstuse)を参照。

WindowsだしShift_JIS吐いていると踏んでdecodeしてみた。

```console
Python 3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> msg = b"\202\273\202\314\202\346\202\244\202\310\203z\203X\203g\202\315\225s\226\276\202\305\202\267\201B"
>>> print(msg.decode('shift_jis'))
そのようなホストは不明です。
```
はい。

# 原因
どこでエラー吐いてるのか、GitHubでエラーメッセージをキーにして検索するとシュッと見つかる。

```C
		do_log2(loglevel, "%s: Could not resolve hostname %.100s: %s",
		    __progname, name, ssh_gai_strerror(gaierr));
```

https://github.com/PowerShell/openssh-portable/blob/d9a9b3ce88c43f964a1ddcf1b77f9fd23d881a60/ssh.c#L274-L275

で、この`ssh_gai_strerror`が何をしているかですが[実装](https://github.com/PowerShell/openssh-portable/blob/d9a9b3ce88c43f964a1ddcf1b77f9fd23d881a60/misc.c#L152-L158)を見ると基本的に`gai_strerror`を呼んでるだけ。WinSock版の`gai_strerror`[^winsock-gai_strerror]がMBCS返すと踏んだので試してみる。

[^winsock-gai_strerror]:MSDNのドキュメントは[gai_strerrorA関数 (ws2tcpip.h)](https://learn.microsoft.com/ja-jp/windows/win32/api/ws2tcpip/nf-ws2tcpip-gai_strerrora)にある(ANSI版)。

```C
#include <stdio.h>
#include <ws2tcpip.h>

int main()
{
    printf("[%s]\n", gai_strerrorA(EAI_NONAME));
}
```
実行結果は以下。
```CONSOLE
[そのようなホストは不明です。 ]
```
MBCS文字列の出自はわかった。

でもこれだと`\202\273\202...`って出てくる理由にはならないので`do_log`の呼び出しを追いかけていくと、[strnvis()を呼んでる](https://github.com/PowerShell/openssh-portable/blob/d9a9b3ce88c43f964a1ddcf1b77f9fd23d881a60/log.c#L403)ところが見つかる。これの実装は[vis.c](https://github.com/PowerShell/openssh-portable/blob/d9a9b3ce88c43f964a1ddcf1b77f9fd23d881a60/openbsd-compat/vis.c)にあって、こいつがMBCS文字列を`\0xx`へ書き換えている。

# どうなればいいか

WinSockの`gai_strerror`は内部で`FormatMessage`を呼んでいると想定できて、`FormatMessage`が英語メッセージを返してくれれば良い。

[FormatMessage](https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-formatmessagea)は`dwLanguageId=0`で呼ばれるとスレッドロケールを参照するので、[SetThreadUILanguage ](https://learn.microsoft.com/ja-jp/windows/win32/api/winnls/nf-winnls-setthreaduilanguage)を使って試してみる。

```C
#include <stdio.h>
#include <ws2tcpip.h>

int main()
{
    SetThreadUILanguage(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));
    printf("[%s]\n", gai_strerrorA(EAI_NONAME));
}
```
```console
[No such host is known. ]
```
なのでスレッドロケールを`en-US`とかにすれば良くて、現状のソースを見ると、`setlocale`[^setlocale-not-effect]してるところはあるけど呼ばれていない[^msetlocale]。

[^msetlocale]: `main()`で呼ばれる`msetlocale()`の[utf8.c](https://github.com/PowerShell/openssh-portable/blob/latestw_all/utf8.c)での実装は`setlocale()`を呼んでいますが、こいつはVSプロジェクトで弾かれていて、実際に呼ばれるのは[contrib/win32/win32compat/win32-utf8.c](https://github.com/PowerShell/openssh-portable/blob/latestw_all/contrib/win32/win32compat/win32-utf8.c)にある実装。

[^setlocale-not-effect]:MSVCでは`setlocale`しても`gai_strerror`の返事に変化がなかったので、CRTランタイムでスレッドロケールをいじってないように見える。

なので実装をいじるしかないわけですが、この謎表示は非ASCII圏では誰もが遭遇するので既に2020年(2年前…)にはissueが立っていました。

https://github.com/PowerShell/Win32-OpenSSH/issues/1654#issuecomment-1315418249

ということで、`SetThreadUILanguage`してくれ～というコメントを書いておいたがどうなることやら。