[インデックス 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)
リバースプロキシは、クライアントからのリクエストを受け取り、それを一つ以上のバックエンドサーバーに転送するサーバーです。クライアントはリバースプロキシと通信していると認識しますが、実際にはリバースプロキシがバックエンドサーバーにリクエストを代理で送信し、その応答をクライアントに返します。
リバースプロキシの主な用途は以下の通りです。
- 負荷分散 (Load Balancing): 複数のバックエンドサーバーにリクエストを分散し、サーバーの負荷を均等にします。
- セキュリティ (Security): バックエンドサーバーのIPアドレスを隠蔽し、直接的な攻撃から保護します。
- SSL/TLS終端 (SSL/TLS Termination): SSL/TLSハンドシェイクをプロキシで行い、バックエンドサーバーの負荷を軽減します。
- キャッシュ (Caching): 静的コンテンツをキャッシュし、応答速度を向上させます。
- 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つの主要な変更が行われています。
Connection
ヘッダーの削除: バックエンドサーバーへのリクエストを構築する際に、クライアントから受け取ったConnection
ヘッダーが存在する場合、それを明示的に削除します。これにより、リバースプロキシとバックエンドサーバー間のコネクションは、クライアントからのConnection
ヘッダーの影響を受けなくなります。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
-
func copyHeader(dst, src Header)
の追加: この新しいヘルパー関数は、src
(ソース)ヘッダーマップからdst
(デスティネーション)ヘッダーマップへ、すべてのヘッダーキーと値をコピーします。http.Header
はmap[string][]string
のエイリアスであり、同じキーに対して複数の値を持つことができるため、内部のループで各値をdst.Add(k, v)
を使って追加しています。Add
メソッドは、既存の値に新しい値を追加する形で動作します。 -
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
-
バックエンドハンドラーでの
Connection
ヘッダーの検証:if c := r.Header.Get("Connection"); c != "" { t.Errorf("handler got Connection header value %q", c) }
これは、リバースプロキシのバックエンドとして機能するテストサーバーのハンドラーに追加されたアサーションです。バックエンドサーバーが受け取ったリクエストの
Connection
ヘッダーが空であることを確認します。もし空でなければ、エラーが報告され、リバースプロキシがConnection
ヘッダーを正しく削除していないことを示します。 -
テストリクエストに
Connection: close
ヘッダーを追加:getReq.Header.Set("Connection", "close") getReq.Close = true
テストクライアントがリバースプロキシに送信するリクエストに、明示的に
Connection: close
ヘッダーを設定しています。これにより、リバースプロキシがこのヘッダーを適切に処理し、バックエンドに転送しないことを検証するためのシナリオが作成されます。getReq.Close = true
は、GoのHTTPクライアントがこのリクエストの完了後にコネクションを閉じるべきであることを示します。
これらの変更により、GoのリバースプロキシはHTTPの仕様に準拠し、より堅牢で予測可能なコネクション管理を行うことができるようになりました。
関連リンク
- Go Issue #2342: https://github.com/golang/go/issues/2342
- Go CL 5302057: https://golang.org/cl/5302057 (元のコミット)
- Go CL 5296055: https://golang.org/cl/5296055 (このコミットのChange List)
- Go
net/http/httputil
パッケージドキュメント: https://pkg.go.dev/net/http/httputil
参考にした情報源リンク
- MDN Web Docs - Connection: https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Connection
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 14.10 Connection): https://www.rfc-editor.org/rfc/rfc2616#section-14.10
- Go
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go
net/http
ReverseProxyのConnectionヘッダーに関する議論 (Stack Overflowなど、一般的な情報源)- (具体的なURLは検索結果によるため省略しますが、
golang http reverseproxy connection header
などで検索すると関連情報が見つかります。) - 例: https://stackoverflow.com/questions/tagged/go+http+reverseproxy (Stack Overflowの関連タグ)
- https://github.com/golang/go/issues?q=is%3Aissue+connection+header+reverseproxy (Go GitHub Issuesの関連検索)
- https://github.com/golang/go/blob/master/src/net/http/httputil/reverseproxy.go (現在のGoソースコード)
- (具体的なURLは検索結果によるため省略しますが、