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

[インデックス 10490] ファイルの概要

このコミットは、Go言語の実験的なSSHパッケージ exp/ssh に対して、direct-tcpip 転送機能のシンプルな機能テストを追加するものです。具体的には、SSHトンネルを介してHTTPおよびHTTPSリクエストをプロキシするテストが導入されています。

コミット

commit f2c858749a7d5c50c85fad64e7bba2c34ecae3e9
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Nov 23 09:38:11 2011 -0500

    exp/ssh: add simple tcpip.go functional tests
    
    R=rsc, agl, n13m3y3r, bradfitz
    CC=golang-dev
    https://golang.org/cl/5385041

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f2c858749a7d5c50c85fad64e7bba2c34ecae3e9

元コミット内容

exp/ssh: add simple tcpip.go functional tests

R=rsc, agl, n13m3y3r, bradfitz
CC=golang-dev
https://golang.org/cl/5385041

変更の背景

このコミットの主な背景は、Go言語の exp/ssh パッケージにおける direct-tcpip 転送機能の堅牢性を確保することにあります。exp/ssh はGoでSSHクライアントおよびサーバーを実装するための実験的なパッケージであり、direct-tcpip はSSHプロトコルが提供する主要な機能の一つです。この機能は、SSHクライアントがSSHサーバーに対して、特定の宛先ホストとポートへのTCP接続を確立するよう要求し、その接続をSSHトンネルを介してクライアントに転送することを可能にします。

このような重要な機能が正しく動作することを保証するためには、実際のネットワーク環境に近い形でテストを行う機能テストが不可欠です。特に、HTTPやHTTPSのような一般的なアプリケーションプロトコルがSSHトンネルを介して正しく動作するかどうかを確認することは、パッケージの実用性を評価する上で重要です。このコミットは、そのための基本的な機能テストを追加することで、将来的なバグの発見やリグレッションの防止に貢献することを目的としています。

前提知識の解説

SSH (Secure Shell)

SSHは、ネットワークを介してコンピュータを安全に操作するためのプロトコルです。主にリモートログインやコマンド実行、ファイル転送などに使用されますが、ポートフォワーディング(トンネリング)機能も提供します。SSHは、公開鍵暗号方式や共通鍵暗号方式を用いて通信を暗号化し、認証を行うことで、盗聴や改ざんを防ぎます。

SSHポートフォワーディング (トンネリング)

SSHのポートフォワーディングは、ネットワークトラフィックをSSH接続を介して安全に転送する技術です。これにより、ファイアウォールを迂回したり、暗号化されていないプロトコルを安全に利用したりすることが可能になります。ポートフォワーディングには主に以下の3種類があります。

  1. ローカルポートフォワーディング (Local Port Forwarding): クライアント側のポートをSSHサーバー経由でリモートの宛先に転送します。
  2. リモートポートフォワーディング (Remote Port Forwarding): SSHサーバー側のポートをクライアント経由でリモートの宛先に転送します。
  3. ダイナミックポートフォワーディング (Dynamic Port Forwarding): クライアント側にSOCKSプロキシを立て、SSHサーバー経由で任意の宛先に接続します。

このコミットでテストされている direct-tcpip は、SSHプロトコルにおける「チャンネル」の一種で、クライアントがSSHサーバーに対して直接TCP接続を要求するものです。これは、ローカルポートフォワーディングの内部的なメカニズムとして利用されることが多く、SSHクライアントが特定の宛先(例: google.com:80)への接続をSSHサーバーに依頼し、その接続をSSHトンネル経由でクライアントに提供するものです。

Go言語の exp/ssh パッケージ

exp/ssh は、Go言語の標準ライブラリの一部として提供されている、SSHプロトコルを実装するための実験的なパッケージです。exp というプレフィックスは「experimental(実験的)」を意味し、APIがまだ安定しておらず、将来的に変更される可能性があることを示唆しています。このパッケージは、SSHクライアントとサーバーの両方をGoで構築するための機能を提供します。

Go言語の net/http および net パッケージ

  • net/http: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。ウェブアプリケーションの構築やHTTPリクエストの送信に広く使用されます。
  • net: Go言語の標準ライブラリで、ネットワークI/Oのプリミティブを提供します。TCP/UDP接続の確立、IPアドレスの解決など、低レベルのネットワーク操作を扱います。

機能テスト (Functional Tests)

機能テストは、ソフトウェアの特定の機能が要件通りに動作するかどうかを検証するテストの一種です。単体テストが個々のコンポーネントや関数を独立してテストするのに対し、機能テストはシステム全体または主要なサブシステムが連携して正しく動作することを確認します。この場合、SSHトンネルを介したHTTP/HTTPS通信という、exp/ssh パッケージの主要な機能がエンドツーエンドで動作するかを検証しています。

技術的詳細

このコミットで追加されたテスト tcpip_func_test.go は、exp/ssh パッケージの direct-tcpip 転送機能を利用して、HTTPおよびHTTPSリクエストをプロキシするシナリオを検証します。

テストの核心は、Goの net/http パッケージが提供する http.ClientTransport フィールドをカスタマイズする点にあります。http.Client は通常、内部的に http.DefaultTransport を使用してHTTPリクエストを送信しますが、Transport フィールドにカスタムの http.RoundTripper インターフェースを実装したオブジェクトを設定することで、リクエストの送信方法を制御できます。

このテストでは、http.Transport 構造体をカスタムの Dial 関数と共に使用しています。http.TransportDial フィールドは、HTTPリクエストを送信する際にTCP接続を確立するために呼び出される関数を指定します。通常、この関数は net.Dial を呼び出して直接ネットワーク接続を行いますが、ここではSSH接続オブジェクト connDial メソッドを使用するようにオーバーライドされています。

	tr := &http.Transport{
		Dial: func(n, addr string) (net.Conn, error) {
			return conn.Dial(n, addr)
		},
	}
	client := &http.Client{
		Transport: tr,
	}

これにより、client.Get(url) が呼び出された際に、HTTPリクエストの基盤となるTCP接続がSSHトンネルを介して確立されるようになります。具体的には、conn.Dial(n, addr) が呼び出されると、exp/ssh パッケージはSSHサーバーに対して direct-tcpip チャンネルを開くよう要求し、指定された addr(例: google.com:80)への接続をSSHサーバーに確立させます。SSHサーバーはその接続からのデータをSSHトンネル経由でクライアントに転送し、クライアントはそれを net/http クライアントに提供します。

テストは、TestTCPIPHTTPTestTCPIPHTTPS の2つの関数で構成されており、それぞれ http://google.comhttps://encrypted.google.com/ へのリクエストをSSHトンネル経由で送信し、エラーが発生しないことを確認します。テストの実行には、sshusersshpass というフラグを通じてSSHのユーザー名とパスワードが提供される必要があります。これは、テストが実際のSSHサーバーへの接続を試みるためです。

コアとなるコードの変更箇所

追加されたファイルは src/pkg/exp/ssh/tcpip_func_test.go です。

diff --git a/src/pkg/exp/ssh/tcpip_func_test.go b/src/pkg/exp/ssh/tcpip_func_test.go
new file mode 100644
index 0000000000..261297241e
--- /dev/null
+++ b/src/pkg/exp/ssh/tcpip_func_test.go
@@ -0,0 +1,59 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ssh
+
+// direct-tcpip functional tests
+
+import (
+	"net"
+	"net/http"
+	"testing"
+)
+
+func TestTCPIPHTTP(t *testing.T) {
+	if *sshuser == "" {
+		t.Log("ssh.user not defined, skipping test")
+		return
+	}
+	// google.com will generate at least one redirect, possibly three
+	// depending on your location.
+	doTest(t, "http://google.com")
+}
+
+func TestTCPIPHTTPS(t *testing.T) {
+	if *sshuser == "" {
+		t.Log("ssh.user not defined, skipping test")
+		return
+	}
+	doTest(t, "https://encrypted.google.com/")
+}
+
+func doTest(t *testing.T, url string) {
+	config := &ClientConfig{
+		User: *sshuser,
+		Auth: []ClientAuth{
+			ClientAuthPassword(password(*sshpass)),
+		},
+	}
+	conn, err := Dial("tcp", "localhost:22", config)
+	if err != nil {
+		t.Fatalf("Unable to connect: %s", err)
+	}
+	defer conn.Close()
+	tr := &http.Transport{
+		Dial: func(n, addr string) (net.Conn, error) {
+			return conn.Dial(n, addr)
+		},
+	}
+	client := &http.Client{
+		Transport: tr,
+	}
+	resp, err := client.Get(url)
+	if err != nil {
+		t.Fatalf("unable to proxy: %s", err)
+	}
+	// got a body without error
+	t.Log(resp)
+}

コアとなるコードの解説

新しく追加された src/pkg/exp/ssh/tcpip_func_test.go ファイルには、以下の主要な要素が含まれています。

  1. パッケージ宣言とインポート:

    • package ssh: exp/ssh パッケージの一部としてテストが定義されています。
    • import ("net", "net/http", "testing"): ネットワーク操作、HTTPクライアント、テストフレームワークに必要なパッケージをインポートしています。
  2. TestTCPIPHTTP 関数:

    • if *sshuser == "": テストを実行するためにSSHユーザー名が設定されているかを確認します。設定されていない場合はテストをスキップします。これは、実際のSSHサーバーへの接続が必要なためです。
    • doTest(t, "http://google.com"): doTest ヘルパー関数を呼び出し、http://google.com へのHTTPリクエストをSSHトンネル経由で送信します。
  3. TestTCPIPHTTPS 関数:

    • if *sshuser == "": 同様にSSHユーザー名の存在を確認します。
    • doTest(t, "https://encrypted.google.com/"): doTest ヘルパー関数を呼び出し、https://encrypted.google.com/ へのHTTPSリクエストをSSHトンネル経由で送信します。
  4. doTest ヘルパー関数:

    • config := &ClientConfig{...}: SSHクライアントの設定を定義します。ユーザー名 (*sshuser) とパスワード (password(*sshpass)) を使用して認証を行います。password 関数は、おそらくパスワード文字列を ClientAuthPassword が期待する形式に変換するためのヘルパーです。
    • conn, err := Dial("tcp", "localhost:22", config): exp/ssh パッケージの Dial 関数を使用して、ローカルホストのポート22(標準的なSSHポート)にSSH接続を確立します。エラーが発生した場合はテストを失敗させます。
    • defer conn.Close(): 関数が終了する際にSSH接続を確実にクローズするための defer ステートメントです。
    • tr := &http.Transport{ Dial: func(n, addr string) (net.Conn, error) { return conn.Dial(n, addr) }, }: ここが最も重要な部分です。http.Transport を初期化し、その Dial フィールドをカスタム関数でオーバーライドしています。このカスタム関数は、通常のTCP接続の代わりに、確立されたSSH接続 (conn) の Dial メソッドを使用してネットワーク接続を確立します。これにより、net/http クライアントからのすべての接続要求がSSHトンネルを介してルーティングされるようになります。
    • client := &http.Client{ Transport: tr, }: カスタムの Transport を持つ http.Client を作成します。
    • resp, err := client.Get(url): 作成したクライアントを使用して、指定されたURL (http://google.com または https://encrypted.google.com/) にGETリクエストを送信します。このリクエストはSSHトンネルを介して行われます。
    • if err != nil { t.Fatalf("unable to proxy: %s", err) }: リクエスト中にエラーが発生した場合、テストを失敗させます。
    • t.Log(resp): 成功した場合、レスポンスオブジェクトをログに出力します。これは、テストが成功したことを示すためのものです。

このテストコードは、exp/ssh パッケージが提供する direct-tcpip 転送機能が、一般的なHTTP/HTTPSトラフィックに対して正しく機能することを確認するための、シンプルかつ効果的な機能テストの例となっています。

関連リンク

参考にした情報源リンク

  • Go言語の net/http パッケージドキュメント: https://pkg.go.dev/net/http
  • Go言語の net パッケージドキュメント: https://pkg.go.dev/net
  • SSHプロトコルに関する一般的な情報 (RFC 4254 - The Secure Shell (SSH) Connection Protocol): https://datatracker.ietf.org/doc/html/rfc4254 (特に direct-tcpip チャンネルに関するセクション)
  • Go言語の exp/ssh パッケージドキュメント (当時のバージョンに基づく): 現在は golang.org/x/crypto/ssh に統合されていますが、当時の exp/ssh の概念は共通しています。
  • Go言語のテストに関するドキュメント: https://go.dev/doc/code#testing
  • Dave Cheney氏のブログやGoに関する記事: このコミットの作者であるDave Cheney氏はGoコミュニティで著名な人物であり、彼のブログ記事はGoのベストプラクティスや内部動作について多くの情報を提供しています。