[インデックス 13540] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http/httputil
パッケージ内の ReverseProxy
において、X-Forwarded-For
ヘッダーの処理を改善するものです。具体的には、リバースプロキシがリクエストを転送する際に、既存の X-Forwarded-For
ヘッダーを上書きするのではなく、クライアントのIPアドレスを既存の値に追記するように変更されています。これにより、複数のプロキシを経由するリクエストにおいて、元のクライアントIPアドレスを含むすべての経由IPアドレスが正しく記録されるようになります。
コミット
httputil: accumulate X-Forwarded-For header info
If the X-Forwarded-For header already exists on a request, we should append our client's IP to it after a comma+space instead of overwriting it.
Fixes #3846.
R=golang-dev, bradfitz CC=golang-dev https://golang.org/cl/6448053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7520f0b4aa3efe9b5eea5bb79fe47c81051d478d
元コミット内容
commit 7520f0b4aa3efe9b5eea5bb79fe47c81051d478d
Author: Bobby Powers <bobbypowers@gmail.com>
Date: Tue Jul 31 08:38:49 2012 +1000
httputil: accumulate X-Forwarded-For header info
If the X-Forwarded-For header already exists on a request, we
should append our client's IP to it after a comma+space instead
of overwriting it.
Fixes #3846.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6448053
変更の背景
この変更の背景には、リバースプロキシ環境における X-Forwarded-For
ヘッダーの不適切な処理がありました。従来の httputil.ReverseProxy
の実装では、リクエストに既に X-Forwarded-For
ヘッダーが存在する場合でも、プロキシ自身のクライアントIPアドレスでそのヘッダーを上書きしていました。
これは、以下のような問題を引き起こします。
- オリジナルのクライアントIPアドレスの喪失: 複数のプロキシ(例: ロードバランサー -> リバースプロキシ -> アプリケーションサーバー)を経由するリクエストの場合、途中のプロキシが
X-Forwarded-For
ヘッダーを上書きしてしまうと、最終的なアプリケーションサーバーは最初のクライアントのIPアドレスを知ることができなくなります。これは、アクセスログの正確性、地理的IPに基づくサービス、セキュリティ監査などに影響を与えます。 - プロキシチェーンの可視性の欠如: どのプロキシがリクエストを転送したかの情報が失われるため、ネットワークのデバッグやトラブルシューティングが困難になります。
このコミットは、これらの問題を解決し、X-Forwarded-For
ヘッダーがその本来の目的(リクエストが通過したプロキシのIPアドレスを順に記録する)を果たすようにするために行われました。Go Issue #3846 で報告された問題に対応しています。
前提知識の解説
リバースプロキシ (Reverse Proxy)
リバースプロキシは、クライアントからのリクエストを受け取り、それを一つ以上のバックエンドサーバーに転送するサーバーです。クライアントはリバースプロキシと直接通信しているように見えますが、実際にはリバースプロキシがバックエンドサーバーとの間の仲介役を務めます。
リバースプロキシの主な用途は以下の通りです。
- ロードバランシング: 複数のバックエンドサーバーにリクエストを分散し、負荷を均等に保ちます。
- セキュリティ: バックエンドサーバーのIPアドレスを隠蔽し、直接的な攻撃から保護します。
- SSL/TLS終端: SSL/TLS暗号化の処理をリバースプロキシで行い、バックエンドサーバーの負荷を軽減します。
- キャッシュ: 静的コンテンツをキャッシュし、バックエンドサーバーへのリクエストを減らします。
- URLルーティング: 特定のURLパターンに基づいてリクエストを異なるバックエンドサーバーにルーティングします。
Go言語の net/http/httputil.ReverseProxy
は、このようなリバースプロキシの機能を手軽に実装するための構造体です。
X-Forwarded-For
ヘッダー
X-Forwarded-For
(XFF) は、HTTPリクエストヘッダーの一つで、HTTPプロキシまたはロードバランサーを介してWebサーバーに接続するクライアントのオリジナルのIPアドレスを識別するために使用されます。
通常、クライアントがプロキシを経由してWebサーバーにアクセスすると、WebサーバーはプロキシのIPアドレスをクライアントのIPアドレスとして認識します。しかし、多くのアプリケーションでは、オリジナルのクライアントIPアドレスを知る必要があります(例: アクセスログ、地理的IPに基づくコンテンツ配信、不正アクセス対策など)。
X-Forwarded-For
ヘッダーは、この問題に対処するために導入されました。リクエストがプロキシを通過するたびに、そのプロキシは X-Forwarded-For
ヘッダーに、そのプロキシに接続してきたクライアント(または前のプロキシ)のIPアドレスを追記します。
形式は以下のようになります。
X-Forwarded-For: <client>, <proxy1>, <proxy2>
<client>
: オリジナルのクライアントのIPアドレス。<proxy1>
: 最初のプロキシのIPアドレス。<proxy2>
: 2番目のプロキシのIPアドレス。
このヘッダーは標準化されたものではなく、事実上の標準として広く使われています。RFC 7239 で定義されている Forwarded
ヘッダーがより新しい標準ですが、X-Forwarded-For
は依然として広く利用されています。
net.SplitHostPort
関数
Go言語の net
パッケージにある SplitHostPort
関数は、"host:port" 形式のネットワークアドレス文字列をホストとポートに分割します。
例:
host, port, err := net.SplitHostPort("192.168.1.100:8080")
この場合、host
は "192.168.1.100"、port
は "8080" になります。
このコミットでは、req.RemoteAddr
(クライアントのネットワークアドレス) からIPアドレス部分を抽出するために使用されています。
strings.Join
関数
Go言語の strings
パッケージにある Join
関数は、文字列のスライス(配列)を結合して一つの文字列を生成します。結合する際に、指定されたセパレータ文字列を各要素の間に挿入します。
例:
s := []string{"apple", "banana", "cherry"}
result := strings.Join(s, ", ")
この場合、result
は "apple, banana, cherry" になります。
このコミットでは、既存の X-Forwarded-For
ヘッダーの値(文字列のスライスとして取得される可能性がある)と新しいクライアントIPアドレスを結合するために使用されています。
技術的詳細
このコミットの技術的な核心は、net/http/httputil.ReverseProxy
がリクエストをバックエンドに転送する際に、X-Forwarded-For
ヘッダーをどのように構築するかという点にあります。
変更前は、ReverseProxy
は常に req.RemoteAddr
から取得したクライアントIPアドレスを X-Forwarded-For
ヘッダーの値として設定していました。これは、リクエストが最初のプロキシを通過する場合には問題ありませんが、既に X-Forwarded-For
ヘッダーを持つリクエスト(つまり、既に別のプロキシを経由してきたリクエスト)の場合には、既存の情報を上書きしてしまい、オリジナルのクライアントIPアドレスや途中のプロキシのIPアドレスが失われるという問題がありました。
変更後は、以下のロジックが導入されました。
- まず、
req.RemoteAddr
から現在のプロキシに接続してきたクライアント(または前のプロキシ)のIPアドレス (clientIP
) を抽出します。 - 次に、転送するリクエスト (
outreq
) のヘッダーにX-Forwarded-For
が既に存在するかどうかを確認します。outreq.Header["X-Forwarded-For"]
は、HTTPヘッダーが複数回出現する可能性があるため、文字列のスライス ([]string
) として返される可能性があります。
- もし
X-Forwarded-For
ヘッダーが既に存在する場合 (prior, ok := outreq.Header["X-Forwarded-For"]; ok
がtrue
の場合)、既存のすべてのX-Forwarded-For
の値をカンマとスペース (,
) で結合し、その後に現在のclientIP
をカンマとスペースで区切って追記します。clientIP = strings.Join(prior, ", ") + ", " + clientIP
X-Forwarded-For
ヘッダーが存在しない場合、または上記のように結合された新しい値がある場合、最終的にoutreq.Header.Set("X-Forwarded-For", clientIP)
を呼び出して、ヘッダーを設定します。
この変更により、X-Forwarded-For
ヘッダーは、リクエストが通過するすべてのプロキシのIPアドレスを順序立てて記録する、標準的な動作に準拠するようになりました。これにより、最終的なアプリケーションサーバーは、リクエストの完全な経路を把握できるようになります。
テストケース TestXForwardedFor
は、この新しい動作を検証するために追加されました。このテストでは、X-Forwarded-For
ヘッダーが事前に設定されたリクエストをプロキシに送信し、プロキシがそのヘッダーに自身のクライアントIPを正しく追記していることを確認します。
コアとなるコードの変更箇所
変更は主に以下の2つのファイルで行われています。
src/pkg/net/http/httputil/reverseproxy.go
(リバースプロキシの主要ロジック)src/pkg/net/http/httputil/reverseproxy_test.go
(テストコード)
src/pkg/net/http/httputil/reverseproxy.go
の変更
--- a/src/pkg/net/http/httputil/reverseproxy.go
+++ b/src/pkg/net/http/httputil/reverseproxy.go
@@ -106,8 +106,14 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
outreq.Header.Del("Connection")
}
- if clientIp, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
- outreq.Header.Set("X-Forwarded-For", clientIp)
+ if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
+ // If we aren't the first proxy retain prior
+ // X-Forwarded-For information as a comma+space
+ // separated list and fold multiple headers into one.
+ if prior, ok := outreq.Header["X-Forwarded-For"]; ok {
+ clientIP = strings.Join(prior, ", ") + ", " + clientIP
+ }
+ outreq.Header.Set("X-Forwarded-For", clientIP)
}
res, err := transport.RoundTrip(outreq)
src/pkg/net/http/httputil/reverseproxy_test.go
の変更
--- a/src/pkg/net/http/httputil/reverseproxy_test.go
+++ b/src/pkg/net/http/httputil/reverseproxy_test.go
@@ -11,6 +11,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
+ "strings"
"testing"
"time"
)
@@ -71,6 +72,47 @@ func TestReverseProxy(t *testing.T) {
}
}
+func TestXForwardedFor(t *testing.T) {
+ const prevForwardedFor = "client ip"
+ const backendResponse = "I am the backend"
+ const backendStatus = 404
+ backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get("X-Forwarded-For") == "" {
+ t.Errorf("didn't get X-Forwarded-For header")
+ }
+ if !strings.Contains(r.Header.Get("X-Forwarded-For"), prevForwardedFor) {
+ t.Errorf("X-Forwarded-For didn't contain prior data")
+ }
+ w.WriteHeader(backendStatus)
+ w.Write([]byte(backendResponse))
+ }))
+ defer backend.Close()
+ backendURL, err := url.Parse(backend.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ proxyHandler := NewSingleHostReverseProxy(backendURL)
+ frontend := httptest.NewServer(proxyHandler)
+ defer frontend.Close()
+
+ getReq, _ := http.NewRequest("GET", frontend.URL, nil)
+ getReq.Host = "some-name"
+ getReq.Header.Set("Connection", "close")
+ getReq.Header.Set("X-Forwarded-For", prevForwardedFor)
+ getReq.Close = true
+ res, err := http.DefaultClient.Do(getReq)
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if g, e := res.StatusCode, backendStatus; g != e {
+ t.Errorf("got res.StatusCode %d; expected %d", g, e)
+ }
+ bodyBytes, _ := ioutil.ReadAll(res.Body)
+ if g, e := string(bodyBytes), backendResponse; g != e {
+ t.Errorf("got body %q; expected %q", g, e)
+ }
+}
+
var proxyQueryTests = []struct {
baseSuffix string // suffix to add to backend URL
reqSuffix string // suffix to add to frontend's request URL
コアとなるコードの解説
src/pkg/net/http/httputil/reverseproxy.go
の変更点
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
// If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
if prior, ok := outreq.Header["X-Forwarded-For"]; ok {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
outreq.Header.Set("X-Forwarded-For", clientIP)
}
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { ... }
:- この行は、現在のリクエスト (
req
) のRemoteAddr
(クライアントのネットワークアドレス、例: "192.0.2.1:12345") からIPアドレス部分を抽出しています。 net.SplitHostPort
はホストとポートを分離し、ここではIPアドレス部分のみ (clientIP
) を取得しています。エラーがない場合のみ処理を続行します。
- この行は、現在のリクエスト (
if prior, ok := outreq.Header["X-Forwarded-For"]; ok { ... }
:outreq
は、バックエンドサーバーに転送される新しいリクエストオブジェクトです。- この
if
文は、outreq
のヘッダーにX-Forwarded-For
が既に存在するかどうかを確認しています。 outreq.Header["X-Forwarded-For"]
は、http.Header
型がmap[string][]string
であるため、X-Forwarded-For
ヘッダーの値が複数ある場合を考慮して[]string
(文字列のスライス) を返します。prior
には既存のX-Forwarded-For
の値のスライスが、ok
にはヘッダーが存在したかどうかの真偽値が入ります。
clientIP = strings.Join(prior, ", ") + ", " + clientIP
:- もし
X-Forwarded-For
ヘッダーが既に存在した場合(ok
がtrue
)、この行が実行されます。 strings.Join(prior, ", ")
は、既存のX-Forwarded-For
の値(prior
スライス内の各要素)をカンマとスペース (,
) で結合し、一つの文字列にします。- その結合された文字列の末尾に、さらにカンマとスペース (
,
) を追加し、最後に現在のプロキシに接続してきたクライアントのIPアドレス (clientIP
) を追記しています。 - これにより、
X-Forwarded-For
ヘッダーは既存のIP1, 既存のIP2, ..., 現在のクライアントIP
の形式で累積されます。
- もし
outreq.Header.Set("X-Forwarded-For", clientIP)
:- 最終的に、更新された
clientIP
の値(既存のIPアドレスが累積されたもの、または単一のIPアドレス)をoutreq
のX-Forwarded-For
ヘッダーに設定します。Set
メソッドは、既存のヘッダーがあれば上書きし、なければ新しく追加します。
- 最終的に、更新された
src/pkg/net/http/httputil/reverseproxy_test.go
の追加テスト TestXForwardedFor
このテストは、X-Forwarded-For
ヘッダーの累積動作を検証するために追加されました。
- バックエンドサーバーのセットアップ:
httptest.NewServer
を使用して、テスト用のバックエンドHTTPサーバーを起動します。- このバックエンドサーバーは、リクエストを受け取った際に
X-Forwarded-For
ヘッダーが存在するか、そして事前に設定したprevForwardedFor
(例: "client ip") が含まれているかを検証します。 - 検証に失敗した場合はテストエラーを報告し、成功した場合は特定のレスポンス (
backendResponse
,backendStatus
) を返します。
- プロキシのセットアップ:
NewSingleHostReverseProxy(backendURL)
を使用して、上記で作成したバックエンドサーバーをターゲットとするリバースプロキシハンドラーを作成します。- このプロキシハンドラーを
httptest.NewServer
でラップし、テスト用のフロントエンドHTTPサーバーとして起動します。
- リクエストの送信:
http.NewRequest
を使用して、フロントエンドプロキシに送信するGETリクエストを作成します。- 重要なのは、このリクエストのヘッダーに
getReq.Header.Set("X-Forwarded-For", prevForwardedFor)
を設定している点です。これにより、プロキシが処理する前に既にX-Forwarded-For
ヘッダーが存在する状況をシミュレートします。 http.DefaultClient.Do(getReq)
でリクエストを送信します。
- レスポンスの検証:
- プロキシからのレスポンスのステータスコードとボディが、バックエンドサーバーが返した期待値と一致するかを検証します。
- バックエンドサーバー側で
X-Forwarded-For
ヘッダーの検証が行われているため、このテストが成功すれば、プロキシがX-Forwarded-For
ヘッダーを正しく累積したことが保証されます。
このテストの追加により、X-Forwarded-For
ヘッダーの累積ロジックが意図通りに機能していることが自動的に検証されるようになり、将来の回帰を防ぐことができます。
関連リンク
- Go Issue #3846: https://github.com/golang/go/issues/3846
参考にした情報源リンク
- MDN Web Docs: X-Forwarded-For: https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-Forwarded-For
- GoDoc: net/http/httputil: https://pkg.go.dev/net/http/httputil
- GoDoc: net.SplitHostPort: https://pkg.go.dev/net#SplitHostPort
- GoDoc: strings.Join: https://pkg.go.dev/strings#Join
- RFC 7239 - Forwarded HTTP Extension: https://datatracker.ietf.org/doc/html/rfc7239