Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 LFLF の違い

  • 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. 文字を1バイトずつ読み込みます。
  2. seenCRfalse の場合、読み込んだバイトが CR であれば seenCRtrue に設定します。
  3. seenCRtrue の場合、読み込んだバイトが LF であれば、バージョン文字列の読み取りを終了し、oktrue に設定してループを抜けます。
  4. seenCRtrue で、読み込んだバイトが LF でない場合、seenCRfalse にリセットし、現在のバイトをバージョン文字列に追加します。これは、CR の後に LF 以外の文字が来た場合に、その CR を通常の文字として扱い、次の LF を探すためです。
  5. 最後に、読み取ったバージョン文字列の末尾から CR を削除していました。

このロジックでは、LF のみが終端として送られてきた場合、seenCRtrue になることがないため、LF を終端として認識できず、maxVersionStringBytes に達するか、EOFに到達するまで読み取りを続け、最終的に「failed to read version string」エラーを返していました。

今回の変更では、このロジックを簡素化し、LF のみを終端として受け入れるように修正しました。

  1. seenCR 変数を削除しました。
  2. 読み込んだバイトが直接 LF (\n) であれば、それが終端であると判断し、ループを抜けるようにしました。
  3. バージョン文字列の末尾に 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 の変更点

  1. seenCR 変数の削除: var ok, seenCR bool から var ok bool に変更されました。これは、CR の検出を追跡するロジックが不要になったためです。

  2. バージョン文字列終端ロジックの簡素化: 変更前は seenCR を使って CR LF シーケンスを検出していましたが、変更後は if buf[0] == '\n' というシンプルな条件で LF を検出するように変わりました。これにより、LF が単独で終端として送られてきた場合でも、すぐにバージョン文字列の読み取りを終了できるようになります。

  3. 末尾の CR 処理の変更: 変更前は return versionString[:len(versionString)-1], nil と、無条件に末尾の1バイト(期待される CR)を削除していました。 変更後は、if len(versionString) > 0 && versionString[len(versionString)-1] == '\r' という条件を追加し、バージョン文字列の末尾に CR が存在する場合にのみ削除するように変更されました。これにより、LF のみで終端された場合には CR が存在しないため、誤ってバージョン文字列の最後の文字が削除されることを防ぎます。

  4. エラーメッセージの変更: errors.New("failed to read version string") から errors.New("ssh: failed to read version string") に変更され、エラーメッセージに ssh: プレフィックスが追加されました。これは、Goの標準ライブラリにおけるエラーメッセージの慣習に合わせたものです。

src/pkg/exp/ssh/transport_test.go の変更点

  1. TestReadVersionWithJustLF テスト関数の追加: この新しいテスト関数は、LF のみで終端されるバージョン文字列が readVersion 関数によって正しく処理されることを検証します。

    • serverVersion (既存のテストで使用される標準的なバージョン文字列) を基に、末尾の CRLF に置き換えたバイトスライス buf を作成します。
    • readVersion を呼び出し、エラーが発生しないこと、および読み取られたバージョン文字列が期待される値(末尾の CR がない元のバージョン文字列)と一致することを確認します。
  2. []byte(serverVersion) の削除: 既存のテスト関数 TestReadVersionTestReadVersionWithoutCRLF で、buf := []byte(serverVersion) となっていた箇所が buf := serverVersion に変更されました。serverVersion は既にバイトスライスとして定義されているため、冗長な型変換が削除されました。これは機能的な変更ではなく、コードの簡素化です。

これらの変更により、readVersion 関数はより堅牢になり、SSHプロトコルのバージョン交換における現実世界の多様な実装に対応できるようになりました。

関連リンク

参考にした情報源リンク