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

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

このコミットは、Go言語の標準ライブラリnet/httpパッケージにおけるリバースプロキシ(ReverseProxy)の実装に関する修正です。具体的には、リバースプロキシがバックエンドサーバーにリクエストを転送する際に、クライアントから受け取ったConnectionヘッダーを削除するように変更されています。これにより、クライアントとリバースプロキシ間のコネクション管理と、リバースプロキシとバックエンドサーバー間のコネクション管理が独立し、予期せぬコネクション切断を防ぎ、より堅牢なプロキシ動作を実現します。また、copyHeaderというヘルパー関数が導入され、ヘッダーのコピー処理が共通化されています。

コミット

  • コミットハッシュ: f777be8f83edbeb065ceb9c394c5bd8ebcc67111
  • Author: Andrew Gerrand adg@golang.org
  • Date: Wed Oct 26 15:27:29 2011 +0900

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

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

元コミット内容

redo CL 5302057 / dac58d9c9e4a

««« original CL description
http: remove Connection header in ReverseProxy

Fixes #2342

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5302057
»»»

R=bradfitz, dsymonds
CC=golang-dev
https://golang.org/cl/5296055

変更の背景

このコミットは、以前のコミット(CL 5302057 / dac58d9c9e4a)のやり直し(redo)であり、Go言語のIssue #2342を修正することを目的としています。

Issue #2342は、Goのリバースプロキシがクライアントから受け取ったConnectionヘッダーをそのままバックエンドサーバーに転送してしまう問題に関するものです。HTTP/1.1では、Connectionヘッダーはホップバイホップヘッダー(hop-by-hop header)であり、単一のTCPコネクションにのみ適用されるべきです。つまり、プロキシを介してリクエストが転送される場合、クライアントとプロキシ間のConnectionヘッダーは、プロキシとバックエンドサーバー間のコネクションには影響を与えないように、プロキシによって削除されるべきです。

もしConnection: closeのようなヘッダーがクライアントからプロキシに送られ、それがそのままバックエンドに転送されると、バックエンドサーバーはコネクションを閉じようとします。これは、リバースプロキシがバックエンドとの間で永続的なコネクション(Keep-Alive)を維持しようとする意図と矛盾し、パフォーマンスの低下や予期せぬエラーを引き起こす可能性があります。

この問題を解決するため、リバースプロキシはバックエンドにリクエストを転送する前に、Connectionヘッダーを削除する必要がありました。

前提知識の解説

HTTP Connection ヘッダー

HTTP Connection ヘッダーは、現在のトランザクションが完了した後に、送信側がネットワーク接続を閉じるべきか、それとも開いたままにするべきかを制御するために使用されます。

  • Connection: close: 現在のトランザクションが完了したら、コネクションを閉じることを示します。
  • Connection: Keep-Alive: 現在のトランザクションが完了した後も、コネクションを開いたままにして、後続のリクエストに再利用できることを示します。これはHTTP/1.1のデフォルトの動作です。

重要なのは、Connectionヘッダーがホップバイホップヘッダーであるという点です。これは、プロキシやゲートウェイなどの仲介ノードを通過する際に、そのヘッダーが次のノードに転送されるべきではないことを意味します。各ホップ(クライアントからプロキシ、プロキシからサーバーなど)は、それぞれ独立したConnectionヘッダーを持つべきです。

リバースプロキシ (Reverse Proxy)

リバースプロキシは、クライアントからのリクエストを受け取り、それを一つ以上のバックエンドサーバーに転送するサーバーです。クライアントはリバースプロキシと通信していると認識しますが、実際にはリバースプロキシがバックエンドサーバーにリクエストを代理で送信し、その応答をクライアントに返します。

リバースプロキシの主な用途は以下の通りです。

  1. 負荷分散 (Load Balancing): 複数のバックエンドサーバーにリクエストを分散し、サーバーの負荷を均等にします。
  2. セキュリティ (Security): バックエンドサーバーのIPアドレスを隠蔽し、直接的な攻撃から保護します。
  3. SSL/TLS終端 (SSL/TLS Termination): SSL/TLSハンドシェイクをプロキシで行い、バックエンドサーバーの負荷を軽減します。
  4. キャッシュ (Caching): 静的コンテンツをキャッシュし、応答速度を向上させます。
  5. URL書き換え (URL Rewriting): クライアントに見せるURLと、バックエンドサーバーが処理するURLを変換します。

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

Go言語の標準ライブラリであるnet/httpパッケージは、HTTPクライアントとサーバーの実装を提供します。このパッケージには、HTTPリクエストの処理、レスポンスの生成、ルーティング、ミドルウェアのサポートなど、HTTP通信に必要な機能が豊富に含まれています。

net/http/httputilパッケージには、リバースプロキシを簡単に構築するためのReverseProxy構造体が提供されています。これは、HTTPリクエストを別のサーバーに転送し、その応答を元のクライアントに返す機能を提供します。

技術的詳細

このコミットの技術的な核心は、HTTPのホップバイホップヘッダーの適切な処理にあります。特にConnectionヘッダーは、プロキシを介する通信において、その意味合いが各コネクション間で独立している必要があります。

変更前は、ReverseProxyがクライアントから受け取ったリクエストヘッダーを、ほぼそのままバックエンドサーバーへのリクエストにコピーしていました。これにはConnectionヘッダーも含まれていました。もしクライアントがConnection: closeヘッダーを送信した場合、リバースプロキシはそれをバックエンドサーバーに転送してしまい、バックエンドサーバーはリクエスト処理後にコネクションを閉じてしまう可能性がありました。これは、リバースプロキシがバックエンドサーバーとの間で永続的なコネクション(Keep-Alive)を維持し、効率的な通信を行いたいという設計意図に反します。

この修正では、以下の2つの主要な変更が行われています。

  1. Connectionヘッダーの削除: バックエンドサーバーへのリクエストを構築する際に、クライアントから受け取ったConnectionヘッダーが存在する場合、それを明示的に削除します。これにより、リバースプロキシとバックエンドサーバー間のコネクションは、クライアントからのConnectionヘッダーの影響を受けなくなります。
  2. copyHeaderヘルパー関数の導入: ヘッダーをコピーする処理が複数箇所で必要となるため、copyHeader(dst, src Header)という新しいヘルパー関数が導入されました。これにより、コードの重複が排除され、可読性と保守性が向上しています。この関数は、srcヘッダーのすべてのキーと値をdstヘッダーに追加します。

テストコードも追加されており、リバースプロキシがバックエンドにConnectionヘッダーを転送しないことを検証しています。具体的には、バックエンドサーバー側でConnectionヘッダーが空であることを確認するアサーションが追加されています。

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

diff --git a/src/pkg/http/reverseproxy.go b/src/pkg/http/reverseproxy.go
index 3f8bfdc80c..3a63db009f 100644
--- a/src/pkg/http/reverseproxy.go
+++ b/src/pkg/http/reverseproxy.go
@@ -69,6 +69,14 @@ func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
 	return &ReverseProxy{Director: director}
 }
 
+func copyHeader(dst, src Header) {
+	for k, vv := range src {
+		for _, v := range vv {
+			dst.Add(k, v)
+		}
+	}
+}
+
 func (p *ReverseProxy) ServeHTTP(rw ResponseWriter, req *Request) {
 	transport := p.Transport
 	if transport == nil {
@@ -84,6 +92,16 @@ func (p *ReverseProxy) ServeHTTP(rw ResponseWriter, req *Request) {
 	outreq.ProtoMinor = 1
 	outreq.Close = false
 
+	// Remove the connection header to the backend.  We want a
+	// persistent connection, regardless of what the client sent
+	// to us.  This is modifying the same underlying map from req
+	// (shallow copied above) so we only copy it if necessary.
+	if outreq.Header.Get("Connection") != "" {
+		outreq.Header = make(Header)
+		copyHeader(outreq.Header, req.Header)
+		outreq.Header.Del("Connection")
+	}
+
 	if clientIp, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
 		outreq.Header.Set("X-Forwarded-For", clientIp)
 	}
@@ -95,12 +113,7 @@ func (p *ReverseProxy) ServeHTTP(rw ResponseWriter, req *Request) {
 		return
 	}
 
-	hdr := rw.Header()
-	for k, vv := range res.Header {
-		for _, v := range vv {
-			hdr.Add(k, v)
-		}
-	}
+	copyHeader(rw.Header(), res.Header)
 
 	rw.WriteHeader(res.StatusCode)
 
diff --git a/src/pkg/http/reverseproxy_test.go b/src/pkg/http/reverseproxy_test.go
index 8078c8d10d..663218d61b 100644
--- a/src/pkg/http/reverseproxy_test.go
+++ b/src/pkg/http/reverseproxy_test.go
@@ -24,6 +24,9 @@ func TestReverseProxy(t *testing.T) {
 		if r.Header.Get("X-Forwarded-For") == "" {
 			t.Errorf("didn't get X-Forwarded-For header")
 		}
+		if c := r.Header.Get("Connection"); c != "" {
+			t.Errorf("handler got Connection header value %q", c)
+		}
 		if g, e := r.Host, "some-name"; g != e {
 			t.Errorf("backend got Host header %q, want %q", g, e)
 		}
@@ -43,6 +46,8 @@ func TestReverseProxy(t *testing.T) {
 
 	getReq, _ := NewRequest("GET", frontend.URL, nil)
 	getReq.Host = "some-name"
+	getReq.Header.Set("Connection", "close")
+	getReq.Close = true
 	res, err := DefaultClient.Do(getReq)
 	if err != nil {
 		t.Fatalf("Get: %v", err)

コアとなるコードの解説

src/pkg/http/reverseproxy.go

  1. func copyHeader(dst, src Header) の追加: この新しいヘルパー関数は、src(ソース)ヘッダーマップからdst(デスティネーション)ヘッダーマップへ、すべてのヘッダーキーと値をコピーします。http.Headermap[string][]stringのエイリアスであり、同じキーに対して複数の値を持つことができるため、内部のループで各値をdst.Add(k, v)を使って追加しています。Addメソッドは、既存の値に新しい値を追加する形で動作します。

  2. ReverseProxy.ServeHTTP メソッド内の変更:

    • Connectionヘッダーの処理:

      	// Remove the connection header to the backend.  We want a
      	// persistent connection, regardless of what the client sent
      	// to us.  This is modifying the same underlying map from req
      	// (shallow copied above) so we only copy it if necessary.
      	if outreq.Header.Get("Connection") != "" {
      		outreq.Header = make(Header)
      		copyHeader(outreq.Header, req.Header)
      		outreq.Header.Del("Connection")
      	}
      

      このブロックが、Connectionヘッダーを削除する主要なロジックです。

      • outreq.Header.Get("Connection") != "":バックエンドに転送するリクエスト(outreq)のヘッダーにConnectionヘッダーが存在するかどうかを確認します。
      • outreq.Header = make(Header):もしConnectionヘッダーが存在する場合、outreq.Headerを新しい空のHeaderマップで初期化します。これは、元のreq.Headerがシャローコピーされているため、req.Header自体を変更しないようにするためです。
      • copyHeader(outreq.Header, req.Header):元のクライアントリクエスト(req)のヘッダーを、新しく作成したoutreq.Headerにコピーします。
      • outreq.Header.Del("Connection"):コピーが完了した後、outreq.HeaderからConnectionヘッダーを削除します。これにより、バックエンドサーバーにはConnectionヘッダーが転送されなくなります。
    • レスポンスヘッダーのコピーにcopyHeaderを使用:

      -	hdr := rw.Header()
      -	for k, vv := range res.Header {
      -		for _, v := range vv {
      -			hdr.Add(k, v)
      -		}
      -	}
      +	copyHeader(rw.Header(), res.Header)
      

      バックエンドサーバーからのレスポンスヘッダーをクライアントへのレスポンスにコピーする際にも、新しく定義されたcopyHeader関数が使用されるようになりました。これにより、コードの重複が解消され、より簡潔になっています。

src/pkg/http/reverseproxy_test.go

  1. バックエンドハンドラーでのConnectionヘッダーの検証:

    		if c := r.Header.Get("Connection"); c != "" {
    			t.Errorf("handler got Connection header value %q", c)
    		}
    

    これは、リバースプロキシのバックエンドとして機能するテストサーバーのハンドラーに追加されたアサーションです。バックエンドサーバーが受け取ったリクエストのConnectionヘッダーが空であることを確認します。もし空でなければ、エラーが報告され、リバースプロキシがConnectionヘッダーを正しく削除していないことを示します。

  2. テストリクエストにConnection: closeヘッダーを追加:

    	getReq.Header.Set("Connection", "close")
    	getReq.Close = true
    

    テストクライアントがリバースプロキシに送信するリクエストに、明示的にConnection: closeヘッダーを設定しています。これにより、リバースプロキシがこのヘッダーを適切に処理し、バックエンドに転送しないことを検証するためのシナリオが作成されます。getReq.Close = trueは、GoのHTTPクライアントがこのリクエストの完了後にコネクションを閉じるべきであることを示します。

これらの変更により、GoのリバースプロキシはHTTPの仕様に準拠し、より堅牢で予測可能なコネクション管理を行うことができるようになりました。

関連リンク

参考にした情報源リンク