diff --git a/docs/config.yaml b/docs/config.yaml index d51936114c..3ec245b85c 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -917,7 +917,7 @@ proxies: # socks5 # max-connections: 1 # Maximum connections. Conflict with max-streams. # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. - + reality-opts: public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE short-id: 10f897e26c4b9478 @@ -1440,7 +1440,7 @@ proxies: # socks5 - name: sudoku type: sudoku server: server_ip/domain # 1.2.3.4 or domain - port: 443 + port: 443 key: "" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;none 不提供 AEAD 保护) padding-min: 2 # 最小填充率(0-100) @@ -1955,6 +1955,7 @@ listeners: # - 0123456789abcdef # server-names: # - test.com + # proxy-protocol: 0 # 可选,默认 0 为关闭;1/2 会在连接 Reality dest 后立即发送 Proxy Protocol v1/v2,向后端传递原始来源/目标地址,后端必须启用 Proxy Protocol # #下列两个 limit 为选填,可对未通过验证的回落连接限速,bytesPerSec 默认为 0 即不启用 # #回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化 # limit-fallback-upload: @@ -2093,6 +2094,7 @@ listeners: - 0123456789abcdef server-names: - test.com + proxy-protocol: 0 # 可选,默认 0 为关闭;1/2 会在连接 Reality dest 后立即发送 Proxy Protocol v1/v2,向后端传递原始来源/目标地址,后端必须启用 Proxy Protocol #下列两个 limit 为选填,可对未通过验证的回落连接限速,bytesPerSec 默认为 0 即不启用 #回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化 limit-fallback-upload: @@ -2202,6 +2204,7 @@ listeners: # - 0123456789abcdef # server-names: # - test.com + # proxy-protocol: 0 # 可选,默认 0 为关闭;1/2 会在连接 Reality dest 后立即发送 Proxy Protocol v1/v2,向后端传递原始来源/目标地址,后端必须启用 Proxy Protocol # #下列两个 limit 为选填,可对未通过验证的回落连接限速,bytesPerSec 默认为 0 即不启用 # #回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化 # limit-fallback-upload: diff --git a/go.mod b/go.mod index a6893cccb6..4ca9713444 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f github.com/mroth/weightedrand/v2 v2.1.0 github.com/openacid/low v0.1.21 + github.com/pires/go-proxyproto v0.8.0 github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e github.com/samber/lo v1.53.0 github.com/sirupsen/logrus v1.9.4 @@ -122,7 +123,6 @@ require ( github.com/mitchellh/go-ps v1.0.0 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/pierrec/lz4/v4 v4.1.27 // indirect - github.com/pires/go-proxyproto v0.8.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect diff --git a/listener/inbound/reality.go b/listener/inbound/reality.go index 7932b06029..ef419c2557 100644 --- a/listener/inbound/reality.go +++ b/listener/inbound/reality.go @@ -9,6 +9,7 @@ type RealityConfig struct { ServerNames []string `inbound:"server-names"` MaxTimeDifference int `inbound:"max-time-difference,omitempty"` Proxy string `inbound:"proxy,omitempty"` + ProxyProtocol int `inbound:"proxy-protocol,omitempty"` LimitFallbackUpload RealityLimitFallback `inbound:"limit-fallback-upload,omitempty"` LimitFallbackDownload RealityLimitFallback `inbound:"limit-fallback-download,omitempty"` @@ -28,6 +29,7 @@ func (c RealityConfig) Build() reality.Config { ServerNames: c.ServerNames, MaxTimeDifference: c.MaxTimeDifference, Proxy: c.Proxy, + ProxyProtocol: c.ProxyProtocol, LimitFallbackUpload: reality.LimitFallback{ AfterBytes: c.LimitFallbackUpload.AfterBytes, diff --git a/listener/reality/reality.go b/listener/reality/reality.go index 256dbcf6a9..33f7115f4a 100644 --- a/listener/reality/reality.go +++ b/listener/reality/reality.go @@ -17,6 +17,7 @@ import ( "github.com/metacubex/mihomo/ntp" utls "github.com/metacubex/utls" + proxyproto "github.com/pires/go-proxyproto" ) type Conn = utls.Conn @@ -29,12 +30,17 @@ type Config struct { ServerNames []string MaxTimeDifference int Proxy string + ProxyProtocol int LimitFallbackUpload LimitFallback LimitFallbackDownload LimitFallback } func (c Config) Build(tunnel C.Tunnel) (*Builder, error) { + if c.ProxyProtocol < 0 || c.ProxyProtocol > 2 { + return nil, fmt.Errorf("invalid proxy-protocol version: %d", c.ProxyProtocol) + } + realityConfig := &utls.RealityConfig{} realityConfig.SessionTicketsDisabled = true realityConfig.Type = "tcp" @@ -74,7 +80,15 @@ func (c Config) Build(tunnel C.Tunnel) (*Builder, error) { } realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { - return inner.HandleTcp(tunnel, address, c.Proxy) + target, err := inner.HandleTcp(tunnel, address, c.Proxy) + if err != nil { + return nil, err + } + if err := writeProxyProtocolHeader(ctx, target, c.ProxyProtocol); err != nil { + _ = target.Close() + return nil, err + } + return target, nil } realityConfig.LimitFallbackUpload = c.LimitFallbackUpload @@ -89,6 +103,8 @@ type Builder struct { func (b Builder) NewListener(l net.Listener) net.Listener { return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) { + ctx = context.WithValue(ctx, sourceAddrContextKey{}, conn.RemoteAddr()) + ctx = context.WithValue(ctx, destinationAddrContextKey{}, conn.LocalAddr()) c, err := utls.RealityServer(ctx, conn, b.realityConfig) if err != nil { return nil, err @@ -121,3 +137,75 @@ func (c realityConnWrapper) ReaderReplaceable() bool { func (c realityConnWrapper) WriterReplaceable() bool { return true } + +type sourceAddrContextKey struct{} + +type destinationAddrContextKey struct{} + +func writeProxyProtocolHeader(ctx context.Context, conn net.Conn, version int) error { + switch version { + case 0: + return nil + case 1, 2: + default: + return fmt.Errorf("invalid proxy-protocol version: %d", version) + } + + sourceAddr, destinationAddr := sourceAndDestinationAddrsFromContext(ctx) + header, degraded := buildProxyProtocolHeader(byte(version), sourceAddr, destinationAddr) + if degraded { + log.Warnln("REALITY proxy-protocol degraded to UNKNOWN/LOCAL for source=%T(%v) destination=%T(%v)", sourceAddr, sourceAddr, destinationAddr, destinationAddr) + } + _, err := header.WriteTo(conn) + if err != nil { + return fmt.Errorf("write proxy-protocol header: %w", err) + } + return nil +} + +func sourceAndDestinationAddrsFromContext(ctx context.Context) (net.Addr, net.Addr) { + sourceAddr, _ := ctx.Value(sourceAddrContextKey{}).(net.Addr) + destinationAddr, _ := ctx.Value(destinationAddrContextKey{}).(net.Addr) + return sourceAddr, destinationAddr +} + +func buildProxyProtocolHeader(version byte, sourceAddr, destinationAddr net.Addr) (*proxyproto.Header, bool) { + sourceTCPAddr, destinationTCPAddr, ok := proxyProtocolTCPAddrPair(sourceAddr, destinationAddr) + if !ok { + return unknownProxyProtocolHeader(version), true + } + + return proxyproto.HeaderProxyFromAddrs(version, sourceTCPAddr, destinationTCPAddr), false +} + +func proxyProtocolTCPAddrPair(sourceAddr, destinationAddr net.Addr) (*net.TCPAddr, *net.TCPAddr, bool) { + sourceTCPAddr, ok := sourceAddr.(*net.TCPAddr) + if !ok || sourceTCPAddr == nil { + return nil, nil, false + } + destinationTCPAddr, ok := destinationAddr.(*net.TCPAddr) + if !ok || destinationTCPAddr == nil { + return nil, nil, false + } + if sourceTCPAddr.Port < 0 || sourceTCPAddr.Port > 65535 || destinationTCPAddr.Port < 0 || destinationTCPAddr.Port > 65535 { + return nil, nil, false + } + + sourceIPv4 := sourceTCPAddr.IP.To4() + destinationIPv4 := destinationTCPAddr.IP.To4() + if sourceIPv4 != nil || destinationIPv4 != nil { + return sourceTCPAddr, destinationTCPAddr, sourceIPv4 != nil && destinationIPv4 != nil + } + + sourceIPv6 := sourceTCPAddr.IP.To16() + destinationIPv6 := destinationTCPAddr.IP.To16() + return sourceTCPAddr, destinationTCPAddr, sourceIPv6 != nil && destinationIPv6 != nil +} + +func unknownProxyProtocolHeader(version byte) *proxyproto.Header { + return &proxyproto.Header{ + Version: version, + Command: proxyproto.LOCAL, + TransportProtocol: proxyproto.UNSPEC, + } +} diff --git a/listener/reality/reality_test.go b/listener/reality/reality_test.go new file mode 100644 index 0000000000..001e1a22a5 --- /dev/null +++ b/listener/reality/reality_test.go @@ -0,0 +1,468 @@ +package reality + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "io" + "net" + "testing" + "time" + + C "github.com/metacubex/mihomo/constant" + + proxyproto "github.com/pires/go-proxyproto" +) + +func TestWriteProxyProtocolHeaderDisabled(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + if err := writeProxyProtocolHeader(context.Background(), client, 0); err != nil { + t.Fatalf("writeProxyProtocolHeader() error = %v", err) + } + + if err := server.SetReadDeadline(time.Now().Add(30 * time.Millisecond)); err != nil { + t.Fatalf("SetReadDeadline() error = %v", err) + } + + buf := make([]byte, 1) + _, err := server.Read(buf) + if err == nil { + t.Fatal("expected no payload when proxy-protocol is disabled") + } + if ne, ok := err.(net.Error); !ok || !ne.Timeout() { + t.Fatalf("expected timeout read error, got %v", err) + } +} + +func TestWriteProxyProtocolHeader(t *testing.T) { + tests := []struct { + name string + version int + source net.Addr + destination net.Addr + wantCommand proxyproto.ProtocolVersionAndCommand + wantTransport proxyproto.AddressFamilyAndProtocol + wantSource string + wantSourcePort int + wantDestination string + wantDestPort int + wantVersion1Bytes []byte + }{ + { + name: "v1 tcp4", + version: 1, + source: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + destination: &net.TCPAddr{IP: net.ParseIP("198.51.100.2"), Port: 443}, + wantCommand: proxyproto.PROXY, + wantTransport: proxyproto.TCPv4, + wantSource: "192.0.2.1", + wantSourcePort: 12345, + wantDestination: "198.51.100.2", + wantDestPort: 443, + }, + { + name: "v2 tcp4", + version: 2, + source: &net.TCPAddr{IP: net.ParseIP("192.0.2.10"), Port: 4567}, + destination: &net.TCPAddr{IP: net.ParseIP("198.51.100.20"), Port: 8443}, + wantCommand: proxyproto.PROXY, + wantTransport: proxyproto.TCPv4, + wantSource: "192.0.2.10", + wantSourcePort: 4567, + wantDestination: "198.51.100.20", + wantDestPort: 8443, + }, + { + name: "v1 tcp6", + version: 1, + source: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345}, + destination: &net.TCPAddr{IP: net.ParseIP("2001:db8::2"), Port: 443}, + wantCommand: proxyproto.PROXY, + wantTransport: proxyproto.TCPv6, + wantSource: "2001:db8::1", + wantSourcePort: 12345, + wantDestination: "2001:db8::2", + wantDestPort: 443, + }, + { + name: "v2 tcp6", + version: 2, + source: &net.TCPAddr{IP: net.ParseIP("2001:db8::10"), Port: 4567}, + destination: &net.TCPAddr{IP: net.ParseIP("2001:db8::20"), Port: 8443}, + wantCommand: proxyproto.PROXY, + wantTransport: proxyproto.TCPv6, + wantSource: "2001:db8::10", + wantSourcePort: 4567, + wantDestination: "2001:db8::20", + wantDestPort: 8443, + }, + { + name: "v1 mismatched families degrade to unknown", + version: 1, + source: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + destination: &net.TCPAddr{IP: net.ParseIP("2001:db8::2"), Port: 443}, + wantCommand: proxyproto.LOCAL, + wantTransport: proxyproto.UNSPEC, + wantVersion1Bytes: []byte("PROXY UNKNOWN\r\n"), + }, + { + name: "v2 mismatched families degrade to unspec", + version: 2, + source: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + destination: &net.TCPAddr{IP: net.ParseIP("2001:db8::2"), Port: 443}, + wantCommand: proxyproto.LOCAL, + wantTransport: proxyproto.UNSPEC, + }, + { + name: "v1 non-tcp addresses degrade to unknown", + version: 1, + source: pipeAddr("source"), + destination: pipeAddr("destination"), + wantCommand: proxyproto.LOCAL, + wantTransport: proxyproto.UNSPEC, + wantVersion1Bytes: []byte("PROXY UNKNOWN\r\n"), + }, + { + name: "v2 non-tcp addresses degrade to unspec", + version: 2, + source: pipeAddr("source"), + destination: pipeAddr("destination"), + wantCommand: proxyproto.LOCAL, + wantTransport: proxyproto.UNSPEC, + }, + { + name: "v1 invalid ip degrades to unknown", + version: 1, + source: &net.TCPAddr{Port: 12345}, + destination: &net.TCPAddr{IP: net.ParseIP("198.51.100.2"), Port: 443}, + wantCommand: proxyproto.LOCAL, + wantTransport: proxyproto.UNSPEC, + wantVersion1Bytes: []byte("PROXY UNKNOWN\r\n"), + }, + { + name: "v2 invalid port degrades to unspec", + version: 2, + source: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 70000}, + destination: &net.TCPAddr{IP: net.ParseIP("198.51.100.2"), Port: 443}, + wantCommand: proxyproto.LOCAL, + wantTransport: proxyproto.UNSPEC, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + payload := writeHeaderToBuffer(t, tt.version, tt.source, tt.destination) + if tt.wantVersion1Bytes != nil && !bytes.Equal(payload, tt.wantVersion1Bytes) { + t.Fatalf("unexpected v1 header: got %q want %q", payload, tt.wantVersion1Bytes) + } + + header, err := proxyproto.Read(bufio.NewReader(bytes.NewReader(payload))) + if err != nil { + t.Fatalf("proxyproto.Read() error = %v", err) + } + assertProxyProtocolHeader(t, header, byte(tt.version), tt.wantCommand, tt.wantTransport, tt.wantSource, tt.wantSourcePort, tt.wantDestination, tt.wantDestPort) + }) + } +} + +func TestWriteProxyProtocolHeaderMissingAddressesDegradesToUnknown(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + headerCh := make(chan *proxyproto.Header, 1) + errCh := make(chan error, 1) + go readProxyProtocolHeader(server, headerCh, errCh) + + if err := writeProxyProtocolHeader(context.Background(), client, 1); err != nil { + t.Fatalf("writeProxyProtocolHeader() error = %v", err) + } + + header := receiveProxyProtocolHeader(t, headerCh, errCh) + assertProxyProtocolHeader(t, header, 1, proxyproto.LOCAL, proxyproto.UNSPEC, "", 0, "", 0) +} + +func TestWriteProxyProtocolHeaderInvalidVersion(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + ctx := context.Background() + ctx = context.WithValue(ctx, sourceAddrContextKey{}, &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}) + ctx = context.WithValue(ctx, destinationAddrContextKey{}, &net.TCPAddr{IP: net.ParseIP("198.51.100.2"), Port: 443}) + + err := writeProxyProtocolHeader(ctx, client, 3) + if err == nil { + t.Fatal("expected error for invalid version") + } +} + +func TestConfigBuildInvalidProxyProtocol(t *testing.T) { + config := Config{ProxyProtocol: 3} + + _, err := config.Build(nil) + if err == nil { + t.Fatal("expected error for invalid proxy-protocol value") + } +} + +func TestConfigBuildDialContextWritesProxyProtocolHeader(t *testing.T) { + tests := []struct { + name string + proxyProtocol int + source net.Addr + destination net.Addr + wantCommand proxyproto.ProtocolVersionAndCommand + wantTransport proxyproto.AddressFamilyAndProtocol + wantSource string + wantSourcePort int + wantDestination string + wantDestPort int + }{ + { + name: "v2 tcp4", + proxyProtocol: 2, + source: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + destination: &net.TCPAddr{IP: net.ParseIP("198.51.100.2"), Port: 443}, + wantCommand: proxyproto.PROXY, + wantTransport: proxyproto.TCPv4, + wantSource: "192.0.2.1", + wantSourcePort: 12345, + wantDestination: "198.51.100.2", + wantDestPort: 443, + }, + { + name: "v1 missing addresses degrade to unknown", + proxyProtocol: 1, + wantCommand: proxyproto.LOCAL, + wantTransport: proxyproto.UNSPEC, + }, + { + name: "v2 mismatched families degrade to unspec", + proxyProtocol: 2, + source: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 12345}, + destination: &net.TCPAddr{IP: net.ParseIP("2001:db8::2"), Port: 443}, + wantCommand: proxyproto.LOCAL, + wantTransport: proxyproto.UNSPEC, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tunnel := &captureTunnel{ + headerCh: make(chan *proxyproto.Header, 1), + errCh: make(chan error, 1), + } + config := Config{ + Dest: "example.com:443", + PrivateKey: testRealityPrivateKey(t), + ProxyProtocol: tt.proxyProtocol, + } + builder, err := config.Build(tunnel) + if err != nil { + t.Fatalf("Config.Build() error = %v", err) + } + + ctx := context.Background() + if tt.source != nil { + ctx = context.WithValue(ctx, sourceAddrContextKey{}, tt.source) + } + if tt.destination != nil { + ctx = context.WithValue(ctx, destinationAddrContextKey{}, tt.destination) + } + + conn, err := builder.realityConfig.DialContext(ctx, "tcp", "example.com:443") + if err != nil { + t.Fatalf("DialContext() error = %v", err) + } + defer conn.Close() + + header := receiveProxyProtocolHeader(t, tunnel.headerCh, tunnel.errCh) + assertProxyProtocolHeader(t, header, byte(tt.proxyProtocol), tt.wantCommand, tt.wantTransport, tt.wantSource, tt.wantSourcePort, tt.wantDestination, tt.wantDestPort) + }) + } +} + +func TestNewListenerWritesProxyProtocolHeaderFromAcceptedConnection(t *testing.T) { + tunnel := &captureTunnel{ + headerCh: make(chan *proxyproto.Header, 1), + errCh: make(chan error, 1), + } + config := Config{ + Dest: "example.com:443", + PrivateKey: testRealityPrivateKey(t), + ProxyProtocol: 2, + } + builder, err := config.Build(tunnel) + if err != nil { + t.Fatalf("Config.Build() error = %v", err) + } + + rawListener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen() error = %v", err) + } + listener := builder.NewListener(rawListener) + defer listener.Close() + + acceptErrCh := make(chan error, 1) + go func() { + conn, err := listener.Accept() + if err == nil { + _ = conn.Close() + } + acceptErrCh <- err + }() + + client, err := net.Dial("tcp", rawListener.Addr().String()) + if err != nil { + t.Fatalf("Dial() error = %v", err) + } + defer client.Close() + + header := receiveProxyProtocolHeader(t, tunnel.headerCh, tunnel.errCh) + sourceAddr := client.LocalAddr().(*net.TCPAddr) + destinationAddr := client.RemoteAddr().(*net.TCPAddr) + wantTransport := proxyproto.TCPv4 + if sourceAddr.IP.To4() == nil { + wantTransport = proxyproto.TCPv6 + } + assertProxyProtocolHeader(t, header, 2, proxyproto.PROXY, wantTransport, sourceAddr.IP.String(), sourceAddr.Port, destinationAddr.IP.String(), destinationAddr.Port) + + _ = listener.Close() + select { + case <-acceptErrCh: + case <-time.After(time.Second): + t.Fatal("timed out waiting for Accept() to exit") + } +} + +func writeHeaderToBuffer(t *testing.T, version int, source, destination net.Addr) []byte { + t.Helper() + + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + ctx := context.Background() + ctx = context.WithValue(ctx, sourceAddrContextKey{}, source) + ctx = context.WithValue(ctx, destinationAddrContextKey{}, destination) + + payloadCh := make(chan []byte, 1) + errCh := make(chan error, 1) + go func() { + var buf bytes.Buffer + _, err := io.Copy(&buf, server) + if err != nil { + errCh <- err + return + } + payloadCh <- buf.Bytes() + }() + + if err := writeProxyProtocolHeader(ctx, client, version); err != nil { + t.Fatalf("writeProxyProtocolHeader() error = %v", err) + } + if err := client.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } + + select { + case err := <-errCh: + t.Fatalf("read header error: %v", err) + case payload := <-payloadCh: + return payload + case <-time.After(time.Second): + t.Fatal("timed out reading proxy-protocol header") + } + return nil +} + +func assertProxyProtocolHeader(t *testing.T, header *proxyproto.Header, version byte, command proxyproto.ProtocolVersionAndCommand, transport proxyproto.AddressFamilyAndProtocol, source string, sourcePort int, destination string, destinationPort int) { + t.Helper() + + if header.Version != version { + t.Fatalf("header version = %d, want %d", header.Version, version) + } + if header.Command != command { + t.Fatalf("header command = %#x, want %#x", header.Command, command) + } + if header.TransportProtocol != transport { + t.Fatalf("header transport = %#x, want %#x", header.TransportProtocol, transport) + } + if command == proxyproto.LOCAL || transport == proxyproto.UNSPEC { + return + } + + sourceTCPAddr, destinationTCPAddr, ok := header.TCPAddrs() + if !ok { + t.Fatal("expected TCP addresses") + } + if got := sourceTCPAddr.IP.String(); got != source { + t.Fatalf("source IP = %s, want %s", got, source) + } + if sourceTCPAddr.Port != sourcePort { + t.Fatalf("source port = %d, want %d", sourceTCPAddr.Port, sourcePort) + } + if got := destinationTCPAddr.IP.String(); got != destination { + t.Fatalf("destination IP = %s, want %s", got, destination) + } + if destinationTCPAddr.Port != destinationPort { + t.Fatalf("destination port = %d, want %d", destinationTCPAddr.Port, destinationPort) + } +} + +func testRealityPrivateKey(t *testing.T) string { + t.Helper() + + privateKey := []byte("0123456789abcdef0123456789abcdef") + return base64.RawURLEncoding.EncodeToString(privateKey) +} + +type pipeAddr string + +func (a pipeAddr) Network() string { return "pipe" } +func (a pipeAddr) String() string { return string(a) } + +type captureTunnel struct { + headerCh chan *proxyproto.Header + errCh chan error +} + +func (t *captureTunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { + defer conn.Close() + readProxyProtocolHeader(conn, t.headerCh, t.errCh) +} + +func (t *captureTunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) {} + +func (t *captureTunnel) NatTable() C.NatTable { return nil } + +var _ C.Tunnel = (*captureTunnel)(nil) + +func readProxyProtocolHeader(conn net.Conn, headerCh chan<- *proxyproto.Header, errCh chan<- error) { + header, err := proxyproto.Read(bufio.NewReader(conn)) + if err != nil { + errCh <- err + return + } + headerCh <- header +} + +func receiveProxyProtocolHeader(t *testing.T, headerCh <-chan *proxyproto.Header, errCh <-chan error) *proxyproto.Header { + t.Helper() + + select { + case header := <-headerCh: + return header + case err := <-errCh: + t.Fatalf("read proxy-protocol header error: %v", err) + case <-time.After(time.Second): + t.Fatal("timed out reading proxy-protocol header") + } + return nil +}