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

[インデックス 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アドレスでそのヘッダーを上書きしていました。

これは、以下のような問題を引き起こします。

  1. オリジナルのクライアントIPアドレスの喪失: 複数のプロキシ(例: ロードバランサー -> リバースプロキシ -> アプリケーションサーバー)を経由するリクエストの場合、途中のプロキシが X-Forwarded-For ヘッダーを上書きしてしまうと、最終的なアプリケーションサーバーは最初のクライアントのIPアドレスを知ることができなくなります。これは、アクセスログの正確性、地理的IPに基づくサービス、セキュリティ監査などに影響を与えます。
  2. プロキシチェーンの可視性の欠如: どのプロキシがリクエストを転送したかの情報が失われるため、ネットワークのデバッグやトラブルシューティングが困難になります。

このコミットは、これらの問題を解決し、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アドレスが失われるという問題がありました。

変更後は、以下のロジックが導入されました。

  1. まず、req.RemoteAddr から現在のプロキシに接続してきたクライアント(または前のプロキシ)のIPアドレス (clientIP) を抽出します。
  2. 次に、転送するリクエスト (outreq) のヘッダーに X-Forwarded-For が既に存在するかどうかを確認します。
    • outreq.Header["X-Forwarded-For"] は、HTTPヘッダーが複数回出現する可能性があるため、文字列のスライス ([]string) として返される可能性があります。
  3. もし X-Forwarded-For ヘッダーが既に存在する場合 (prior, ok := outreq.Header["X-Forwarded-For"]; oktrue の場合)、既存のすべての X-Forwarded-For の値をカンマとスペース (, ) で結合し、その後に現在の clientIP をカンマとスペースで区切って追記します。
    • clientIP = strings.Join(prior, ", ") + ", " + clientIP
  4. X-Forwarded-For ヘッダーが存在しない場合、または上記のように結合された新しい値がある場合、最終的に outreq.Header.Set("X-Forwarded-For", clientIP) を呼び出して、ヘッダーを設定します。

この変更により、X-Forwarded-For ヘッダーは、リクエストが通過するすべてのプロキシのIPアドレスを順序立てて記録する、標準的な動作に準拠するようになりました。これにより、最終的なアプリケーションサーバーは、リクエストの完全な経路を把握できるようになります。

テストケース TestXForwardedFor は、この新しい動作を検証するために追加されました。このテストでは、X-Forwarded-For ヘッダーが事前に設定されたリクエストをプロキシに送信し、プロキシがそのヘッダーに自身のクライアントIPを正しく追記していることを確認します。

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

変更は主に以下の2つのファイルで行われています。

  1. src/pkg/net/http/httputil/reverseproxy.go (リバースプロキシの主要ロジック)
  2. 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 ヘッダーが既に存在した場合(oktrue)、この行が実行されます。
    • 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アドレス)を outreqX-Forwarded-For ヘッダーに設定します。Set メソッドは、既存のヘッダーがあれば上書きし、なければ新しく追加します。

src/pkg/net/http/httputil/reverseproxy_test.go の追加テスト TestXForwardedFor

このテストは、X-Forwarded-For ヘッダーの累積動作を検証するために追加されました。

  1. バックエンドサーバーのセットアップ:
    • httptest.NewServer を使用して、テスト用のバックエンドHTTPサーバーを起動します。
    • このバックエンドサーバーは、リクエストを受け取った際に X-Forwarded-For ヘッダーが存在するか、そして事前に設定した prevForwardedFor (例: "client ip") が含まれているかを検証します。
    • 検証に失敗した場合はテストエラーを報告し、成功した場合は特定のレスポンス (backendResponse, backendStatus) を返します。
  2. プロキシのセットアップ:
    • NewSingleHostReverseProxy(backendURL) を使用して、上記で作成したバックエンドサーバーをターゲットとするリバースプロキシハンドラーを作成します。
    • このプロキシハンドラーを httptest.NewServer でラップし、テスト用のフロントエンドHTTPサーバーとして起動します。
  3. リクエストの送信:
    • http.NewRequest を使用して、フロントエンドプロキシに送信するGETリクエストを作成します。
    • 重要なのは、このリクエストのヘッダーに getReq.Header.Set("X-Forwarded-For", prevForwardedFor) を設定している点です。これにより、プロキシが処理する前に既に X-Forwarded-For ヘッダーが存在する状況をシミュレートします。
    • http.DefaultClient.Do(getReq) でリクエストを送信します。
  4. レスポンスの検証:
    • プロキシからのレスポンスのステータスコードとボディが、バックエンドサーバーが返した期待値と一致するかを検証します。
    • バックエンドサーバー側で X-Forwarded-For ヘッダーの検証が行われているため、このテストが成功すれば、プロキシが X-Forwarded-For ヘッダーを正しく累積したことが保証されます。

このテストの追加により、X-Forwarded-For ヘッダーの累積ロジックが意図通りに機能していることが自動的に検証されるようになり、将来の回帰を防ぐことができます。

関連リンク

参考にした情報源リンク