PROXY protocol を使ってNLB配下のサーバーでクライアントの Remote IP を得る
背景
- goproxy を使って HTTP Proxy server を作った
- その Proxy server は AWS Fargate で動作していてNLBでロードバランシングされている
- NLBはターゲットタイプがインスタンス以外だとRemote IPがNLBのものになってしまう
- Proxy server でクライアントのRemote IPがわからず困る
どうするか
goproxy をEC2で動かして、NLBのターゲットタイプをインスタンスにする、というのが手っ取り早いのですが、どうやら PROXY protocol というものがあり、 それを使うとこの問題を解決できるようです。
PROXY protocol ってなに
要はHTTPにおける X-Forwarded-For ヘッダーに相当するもので、TCP/IPベースの任意のプロトコルでクライアントのRemote IPなどの情報を付与するものです。 PROXY Protocolにはバージョン1とバージョン2があり、前者はテキスト、後者はバイナリです。両者ともTCP(UDP)パケットのペイロードに挿入されます。
PROXY protocol v1
以下の形式のプレーンテキストです。
PROXY_STRING + single space + INET_PROTOCOL + single space + CLIENT_IP + single space + PROXY_IP + single space + CLIENT_PORT + single space + PROXY_PORT + "\r\n"
— https://developers.cloudflare.com/spectrum/getting-started/proxy-protocol/
たとえば以下のようになります。
PROXY TCP4 192.0.2.0 192.0.2.255 42300 443\r\n
- INET_PROTOCOL: TCP over IPv4
- CLIENT_IP: 192.0.2.0
- PROXY_IP: 192.0.2.255
- CLIENT_PORT: 42300
- PROXY_PORT: 443
PROXY protocol v2
バイナリで、プロトコルが TCP over IPv4 の場合は以下のようなレイアウトになっています。
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | Proxy Protocol v2 Signature | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version|Command| AF | Proto.| Address Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | IPv4 Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | IPv4 Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
— https://developers.cloudflare.com/spectrum/getting-started/proxy-protocol/
たとえば以下のようになります。
00000000 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A 21 11 00 0C |.......QUIT.!...| 00000010 C0 A8 64 C8 C0 A8 64 C9 D8 6F 1F 90 |..d...d..o..|
- Proxy Protocol v2 Signature:
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A
- Version: 2(
0x2
) - Command: PROXY(
0x1
) - AF: IPv4(
0x1
) - Proto.: TCP over IPv4(
0x1
) - Address Length: 12 byte(
0x00, 0x0C
) - IPv4 Source Address: 192.168.100.200(
0xC0, 0xA8, 0x64, 0xC8
) - IPv4 Destination Address: 192.168.100.201(
0xC0, 0xA8, 0x64, 0xC9
) - Source Port: 55407(
0xD8, 0x6F
) - Destination Port: 8080(
0x1F, 0x90
)
Go言語で PROXY protocol をパースする
Go言語の場合、pires/go-proxyproto というライブラリを使うのが楽です。
server.go:
package main import ( "log" "net" "github.com/pires/go-proxyproto" ) func main() { addr := "127.0.0.1:8080" listener, err := net.Listen("tcp", addr) if err != nil { log.Fatalf("couldn't listen to %q: %q\n", addr, err.Error()) } ppListener := &proxyproto.Listener{Listener: listener} defer ppListener.Close() conn, _ := ppListener.Accept() defer conn.Close() if conn.LocalAddr() == nil { log.Fatal("couldn't retrieve local address") } log.Printf("local address: %q", conn.LocalAddr().String()) if conn.RemoteAddr() == nil { log.Fatal("couldn't retrieve remote address") } log.Printf("remote address: %q", conn.RemoteAddr().String()) }
ppListener := &proxyproto.Listener{Listener: listener}
で生の net.Listener
をラップすることで PROXY protocol を解析できます。
ppListener
は net.Listener
互換のインターフェイスを持っているため、後続のコードでは proxyproto
を使っているかどうかを気にせず使用できます(conn.RemoteAddr()
などが IPヘッダのものではなく、PROXY Protocol によって指定されたものになる)。
このサーバーに PROXY protocol のヘッダを付与して通信してみます。
client.go:
package main import "net" func main() { ppv2 := []byte{0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A} // \r\n \r\n \0 \r\n QUIT \n ppv2 = append(ppv2, []byte{0x21}...) // Version 2, PROXY command ppv2 = append(ppv2, []byte{0x11}...) // AF_INET, SOCK_STREAM ppv2 = append(ppv2, []byte{0x00, 0x0C}...) // Src/Dst addr/port length ppv2 = append(ppv2, []byte{0xC0, 0xA8, 0x64, 0xC8}...) // Src addr: 192.168.100.200 ppv2 = append(ppv2, []byte{0xC0, 0xA8, 0x64, 0xC9}...) // Dst addr: 192.168.100.201 ppv2 = append(ppv2, []byte{0xD8, 0x6F}...) // Src port: 55407 ppv2 = append(ppv2, []byte{0x1F, 0x90}...) // Dst port: 8080 addr := "127.0.0.1:8080" conn, err := net.Dial("tcp", addr) defer conn.Close() if err != nil { panic(err) } conn.Write(ppv2) }
結果:
2020/07/31 15:40:19 local address: "192.168.100.201:8080" 2020/07/31 15:40:19 remote address: "192.168.100.200:55407"
PROXY protocol で指定したアドレスになっていることが確認できました。
NLBで PROXY protocol を有効にする
NLBではターゲットグループで以下の設定を有効にすることにより、NLB配下のサーバーに PROXY protocol を付与することができます。
注意点としては、ヘルスチェックするときも PROXY protocol のヘッダを付与するので、ヘルスチェックで指定したエンドポイントでも PROXY protocol を考慮する必要があります。
まとめ
PROXY protocol を利用することによって、TCP/IP上の任意のプロトコルでクライアントのIPを取得することができます。 しかし、PROXY protocol を有効にしたサーバーをインターネット上に公開すると外部からアクセス元が改竄できてしまうので注意が必要です。 また、Go言語で PROXY protocol を使うサンプルは https://github.com/mrk21/sandbox/tree/master/go-go-proxyproto にあります。