[インデックス 11230] ファイルの概要
このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh
) において、SSHプロトコルのバージョン文字列の読み取り処理を改善するものです。具体的には、RFC 4253で規定されている CR LF
(キャリッジリターンとラインフィード) ではなく、LF
(ラインフィード) のみでバージョン文字列を終端するSSHサーバーからの入力を適切に処理できるように変更しています。これにより、より多くのSSH実装との互換性が向上します。
コミット
commit dbebb08601ae43566ed19748a838b2a36481f61a
Author: Adam Langley <agl@golang.org>
Date: Wed Jan 18 15:04:17 2012 -0500
exp/ssh: handle versions with just '\n'
djm recommend that we do this because OpenSSL was only fixed in 2008:
http://anoncvs.mindrot.org/index.cgi/openssh/sshd.c?revision=1.380&view=markup
R=dave, jonathan.mark.pittman
CC=golang-dev
https://golang.org/cl/5555044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dbebb08601ae43566ed19748a838b2a36481f61a
元コミット内容
exp/ssh: handle versions with just '\n'
djm recommend that we do this because OpenSSL was only fixed in 2008:
http://anoncvs.mindrot.org/index.cgi/openssh/sshd.c?revision=1.380&view=markup
R=dave, jonathan.mark.pittman
CC=golang-dev
https://golang.org/cl/5555044
変更の背景
SSHプロトコル (RFC 4253) では、クライアントとサーバーが接続確立時に交換するバージョン文字列は CR LF
(キャリッジリターンとラインフィード) で終端されると規定されています。しかし、一部のSSH実装、特に古いバージョンのOpenSSLを使用したSSHサーバーでは、この規定に厳密に従わず、LF
(ラインフィード) のみでバージョン文字列を終端するケースが存在しました。
コミットメッセージにある djm
(おそらくOpenSSHの開発者であるDamien Miller氏を指す) からの推奨は、この非標準的な挙動に対応する必要性を示唆しています。OpenSSLの関連する修正が2008年に行われたという言及は、それ以前のOpenSSLベースのSSHサーバーがこの問題を引き起こしていた可能性が高いことを示しています。
Goの exp/ssh
パッケージがこれらの非標準的な実装と互換性を持つためには、LF
のみで終端されるバージョン文字列も適切に解釈できるように readVersion
関数を修正する必要がありました。これにより、より広範なSSHサーバーとの接続性が確保されます。
前提知識の解説
SSHプロトコルとバージョン交換 (RFC 4253, Section 4.2)
Secure Shell (SSH) は、ネットワークを介して安全なデータ通信を行うためのプロトコルです。RFC 4253は、SSHのトランスポート層プロトコルを定義しており、そのセクション4.2「Protocol Version Exchange」では、SSH接続が確立された直後に行われるクライアントとサーバー間のバージョン文字列の交換について詳細に規定しています。
この規定によると、バージョン文字列のフォーマットは以下の通りです。
SSH-protoversion-softwareversion SP comments CR LF
protoversion
: SSHプロトコルのバージョン。RFC 4253で定義されているSSHプロトコルでは、これは「2.0」でなければなりません。softwareversion
: 使用されている特定のソフトウェア(例: OpenSSH_8.9)を識別する文字列です。comments
: オプションのコメント部分です。存在する場合は、softwareversion
と単一のスペース (SP, ASCII 32) で区切られます。- 終端: 最も重要な点として、バージョン文字列全体は、単一のキャリッジリターン (CR, ASCII 13) の後に単一のラインフィード (LF, ASCII 10) が続くシーケンス (
CR LF
) で終端されなければなりません。
このバージョン文字列は、Diffie-Hellman鍵交換プロセスで使用されるため、その正確な解析はSSH接続の確立において非常に重要です。
CR LF
と LF
の違い
CR LF
(Carriage Return + Line Feed): Windowsや一部のインターネットプロトコル (HTTP, SMTP, SSHなど) で行の終端を示すために使用される2文字のシーケンスです。タイプライターのキャリッジリターン(行頭に戻る)とラインフィード(次の行に進む)に由来します。LF
(Line Feed): Unix/Linux系システムやmacOS (OS X以降) で行の終端を示すために使用される1文字のシーケンスです。単に次の行に進むことを意味します。
RFC 4253では CR LF
が規定されていますが、歴史的な経緯や実装のバグにより、一部のSSHサーバーが LF
のみでバージョン文字列を終端してしまうケースがありました。これはプロトコル仕様からの逸脱ですが、現実世界の互換性を確保するためには、クライアント側がこのような非標準的な挙動にも対応できる柔軟性を持つことが望ましいとされます。
技術的詳細
Goの exp/ssh
パッケージ内の readVersion
関数は、SSHプロトコルのバージョン文字列を読み取る役割を担っています。この関数は、RFC 4253の規定に従い、バージョン文字列が CR LF
で終端されることを期待していました。
変更前の readVersion
関数は、seenCR
というブール変数を使用して、CR
が検出されたかどうかを追跡していました。
- 文字を1バイトずつ読み込みます。
seenCR
がfalse
の場合、読み込んだバイトがCR
であればseenCR
をtrue
に設定します。seenCR
がtrue
の場合、読み込んだバイトがLF
であれば、バージョン文字列の読み取りを終了し、ok
をtrue
に設定してループを抜けます。seenCR
がtrue
で、読み込んだバイトがLF
でない場合、seenCR
をfalse
にリセットし、現在のバイトをバージョン文字列に追加します。これは、CR
の後にLF
以外の文字が来た場合に、そのCR
を通常の文字として扱い、次のLF
を探すためです。- 最後に、読み取ったバージョン文字列の末尾から
CR
を削除していました。
このロジックでは、LF
のみが終端として送られてきた場合、seenCR
が true
になることがないため、LF
を終端として認識できず、maxVersionStringBytes
に達するか、EOFに到達するまで読み取りを続け、最終的に「failed to read version string」エラーを返していました。
今回の変更では、このロジックを簡素化し、LF
のみを終端として受け入れるように修正しました。
seenCR
変数を削除しました。- 読み込んだバイトが直接
LF
(\n
) であれば、それが終端であると判断し、ループを抜けるようにしました。 - バージョン文字列の末尾に
CR
(\r
) が残っている場合は、それを削除するように変更しました。これにより、CR LF
で終端された場合でも、LF
のみで終端された場合でも、正しくバージョン文字列を抽出できるようになります。
この修正により、readVersion
関数はRFC 4253に厳密に従わないSSHサーバー(特に古いOpenSSLベースの実装)とも互換性を持つようになり、堅牢性が向上しました。
コアとなるコードの変更箇所
--- a/src/pkg/exp/ssh/transport.go
+++ b/src/pkg/exp/ssh/transport.go
@@ -339,7 +339,7 @@ const maxVersionStringBytes = 1024
// Read version string as specified by RFC 4253, section 4.2.
func readVersion(r io.Reader) ([]byte, error) {
versionString := make([]byte, 0, 64)
- var ok, seenCR bool
+ var ok bool
var buf [1]byte
forEachByte:
for len(versionString) < maxVersionStringBytes {
@@ -347,27 +347,22 @@ forEachByte:
if err != nil {
return nil, err
}
- b := buf[0]
-
- if !seenCR {
- if b == '\r' {
- seenCR = true
- }
- } else {
- if b == '\n' {
- ok = true
- break forEachByte
- } else {
- seenCR = false
- }
+ // The RFC says that the version should be terminated with \r\n
+
+ // but several SSH servers actually only send a \n.
+ if buf[0] == '\n' {
+ ok = true
+ break forEachByte
}
- versionString = append(versionString, b)
+ versionString = append(versionString, buf[0])
}
if !ok {
- return nil, errors.New("failed to read version string")
+ return nil, errors.New("ssh: failed to read version string")
}
- // We need to remove the CR from versionString
- return versionString[:len(versionString)-1], nil
+ // There might be a '\r' on the end which we should remove.
+ if len(versionString) > 0 && versionString[len(versionString)-1] == '\r' {
+ versionString = versionString[:len(versionString)-1]
+ }
+ return versionString, nil
}
diff --git a/src/pkg/exp/ssh/transport_test.go b/src/pkg/exp/ssh/transport_test.go
index b2e2a7fc92..ab9177f0d1 100644
--- a/src/pkg/exp/ssh/transport_test.go
+++ b/src/pkg/exp/ssh/transport_test.go
@@ -11,7 +11,7 @@ import (
)
func TestReadVersion(t *testing.T) {
- buf := []byte(serverVersion)
+ buf := serverVersion
result, err := readVersion(bufio.NewReader(bytes.NewBuffer(buf)))\n if err != nil {
t.Errorf("readVersion didn't read version correctly: %s", err)
}
@@ -21,6 +21,20 @@ func TestReadVersion(t *testing.T) {
}
}
+func TestReadVersionWithJustLF(t *testing.T) {
+ var buf []byte
+ buf = append(buf, serverVersion...)
+ buf = buf[:len(buf)-1]
+ buf[len(buf)-1] = '\n'
+ result, err := readVersion(bufio.NewReader(bytes.NewBuffer(buf)))
+ if err != nil {
+ t.Error("readVersion failed to handle just a \\n")
+ }
+ if !bytes.Equal(buf[:len(buf)-1], result) {
+ t.Errorf("version read did not match expected: got %x, want %x", result, buf[:len(buf)-1])
+ }
+}
+
func TestReadVersionTooLong(t *testing.T) {
buf := make([]byte, maxVersionStringBytes+1)
if _, err := readVersion(bufio.NewReader(bytes.NewBuffer(buf))); err == nil {
@@ -29,7 +43,7 @@ func TestReadVersionTooLong(t *go.testing.T) {
}
func TestReadVersionWithoutCRLF(t *go.testing.T) {
- buf := []byte(serverVersion)
+ buf := serverVersion
buf = buf[:len(buf)-1]
if _, err := readVersion(bufio.NewReader(bytes.NewBuffer(buf))); err == nil {
t.Error("readVersion did not notice \\\\n was missing")
コアとなるコードの解説
src/pkg/exp/ssh/transport.go
の変更点
-
seenCR
変数の削除:var ok, seenCR bool
からvar ok bool
に変更されました。これは、CR
の検出を追跡するロジックが不要になったためです。 -
バージョン文字列終端ロジックの簡素化: 変更前は
seenCR
を使ってCR LF
シーケンスを検出していましたが、変更後はif buf[0] == '\n'
というシンプルな条件でLF
を検出するように変わりました。これにより、LF
が単独で終端として送られてきた場合でも、すぐにバージョン文字列の読み取りを終了できるようになります。 -
末尾の
CR
処理の変更: 変更前はreturn versionString[:len(versionString)-1], nil
と、無条件に末尾の1バイト(期待されるCR
)を削除していました。 変更後は、if len(versionString) > 0 && versionString[len(versionString)-1] == '\r'
という条件を追加し、バージョン文字列の末尾にCR
が存在する場合にのみ削除するように変更されました。これにより、LF
のみで終端された場合にはCR
が存在しないため、誤ってバージョン文字列の最後の文字が削除されることを防ぎます。 -
エラーメッセージの変更:
errors.New("failed to read version string")
からerrors.New("ssh: failed to read version string")
に変更され、エラーメッセージにssh:
プレフィックスが追加されました。これは、Goの標準ライブラリにおけるエラーメッセージの慣習に合わせたものです。
src/pkg/exp/ssh/transport_test.go
の変更点
-
TestReadVersionWithJustLF
テスト関数の追加: この新しいテスト関数は、LF
のみで終端されるバージョン文字列がreadVersion
関数によって正しく処理されることを検証します。serverVersion
(既存のテストで使用される標準的なバージョン文字列) を基に、末尾のCR
をLF
に置き換えたバイトスライスbuf
を作成します。readVersion
を呼び出し、エラーが発生しないこと、および読み取られたバージョン文字列が期待される値(末尾のCR
がない元のバージョン文字列)と一致することを確認します。
-
[]byte(serverVersion)
の削除: 既存のテスト関数TestReadVersion
とTestReadVersionWithoutCRLF
で、buf := []byte(serverVersion)
となっていた箇所がbuf := serverVersion
に変更されました。serverVersion
は既にバイトスライスとして定義されているため、冗長な型変換が削除されました。これは機能的な変更ではなく、コードの簡素化です。
これらの変更により、readVersion
関数はより堅牢になり、SSHプロトコルのバージョン交換における現実世界の多様な実装に対応できるようになりました。
関連リンク
- Go CL 5555044: https://golang.org/cl/5555044
参考にした情報源リンク
- OpenSSH
sshd.c
の関連コミット: http://anoncvs.mindrot.org/index.cgi/openssh/sshd.c?revision=1.380&view=markup - RFC 4253 - The Secure Shell (SSH) Transport Layer Protocol: https://www.ietf.org/rfc/rfc4253.txt
- RFC 4253, Section 4.2 "Protocol Version Exchange" の解説:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGEPoNpccW29B829D5OTaWY9SDGy9KuvmcmA83pdKKj119IHvoTU4D-mFVn5bMg-5i7Us2ugcqXYDbbjk9DdvNUiXUaMYTp_HIOAVf9XJ5jhfpKLNj-cLDtrPSxMQwHvKRTMKzKwXg_lg==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF1G2IP-X0S0zaNOHFY0AE6_fIZ4W5uD5YoqBumJMp9nx7VZuUrXWaXVFd64AKAPLocRp8YsfJjE7RbGmVDY86-hUPVRx0F_oagt6OOK4xRzubFdKGIIkLnOUiNZeCLL7vlJgzYtt9NWykFYmykqE4RQhBs
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHNJUPfN56uXOm_3pg_eMJUkLgeOyMatPIUVaQIK4U0RMABzM5-Bhy0CsADjwZqIU7RHiF9ej2jykC5BkuEyUKoE943s0tcz9JM35Hc9FijstqxmpvGQQY043xA9P8n3U5iPTyREog==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFUvh4n2wFaAIX8GKh7s8uFKshyHKixZQ0UZ5xO9Gq9o6JtUSk_mOblQ5pJ5RaBV5IpIHGVU24-t2Ahlg9NZrCiSZcSdi2-3aXzntirKL1zL9gZHbx0_KE_XR7DSeklqrf1o-ek3NY5