From cb76f63754768c7b5d68895a0d51635b0141b80f Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sat, 29 Aug 2026 09:26:33 +0800 Subject: [PATCH] e2e: add PROXY protocol test (gost#677), bump core/x deps ProxyProtoSuite starts gost with metadata.proxyProtocol set and asserts the HAProxy PROXY header is prepended on outbound connections (v1/v2), backed by a raw-TCP capture script and configs. Add echo754_repro.py from the hot-reload EADDRINUSE investigation (gost#754). Bump github.com/go-gost/core to v0.6.1 and x to v0.16.0. --- go.mod | 4 +- go.sum | 8 +- tests/e2e/proxyproto_test.go | 127 ++++++++++++++++++++++++++ tests/e2e/scripts/echo754_repro.py | 22 +++++ tests/e2e/scripts/proxy_capture.py | 23 +++++ tests/e2e/testdata/proxyproto/v1.yaml | 19 ++++ tests/e2e/testdata/proxyproto/v2.yaml | 18 ++++ 7 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/proxyproto_test.go create mode 100644 tests/e2e/scripts/echo754_repro.py create mode 100644 tests/e2e/scripts/proxy_capture.py create mode 100644 tests/e2e/testdata/proxyproto/v1.yaml create mode 100644 tests/e2e/testdata/proxyproto/v2.yaml diff --git a/go.mod b/go.mod index 8b3518d..07e1439 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/go-gost/gost go 1.26.3 require ( - github.com/go-gost/core v0.6.0 - github.com/go-gost/x v0.15.7 + github.com/go-gost/core v0.6.1 + github.com/go-gost/x v0.16.0 github.com/judwhite/go-svc v1.2.1 github.com/moby/moby/client v0.4.0 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 1fdd474..833b2fa 100644 --- a/go.sum +++ b/go.sum @@ -83,8 +83,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= -github.com/go-gost/core v0.6.0 h1:Y7jG5bI0QxdzDlMOyEtAYxCz+0GxAHKmaei667pRPWc= -github.com/go-gost/core v0.6.0/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A= +github.com/go-gost/core v0.6.1 h1:mBBvZpxIbrspuRsksj7YLHEiBGEc+sQBV3wDs4rX/AE= +github.com/go-gost/core v0.6.1/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A= github.com/go-gost/go-shadowsocks2 v0.1.4 h1:n2Po4TDKdLp1PsvhSiWFB/6S4e/YKZfsKJkA0PUa168= github.com/go-gost/go-shadowsocks2 v0.1.4/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys= github.com/go-gost/gosocks4 v0.1.0 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk= @@ -99,8 +99,8 @@ github.com/go-gost/relay v0.7.0 h1:J8e3Sba6DtBJQotXY5j5EaZNWwtv6Z9CoCFQsnrHNcE= github.com/go-gost/relay v0.7.0/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI= github.com/go-gost/tls-dissector v0.3.1 h1:gvOteWog5pjY/HCpc8l+gngmSi8Q6zl5rRrfK8gwRKA= github.com/go-gost/tls-dissector v0.3.1/go.mod h1:vGfog053fIm93iXBtvmVzMQqEJo5YwbbNaPNVDjbiOc= -github.com/go-gost/x v0.15.7 h1:yuiEs7CoUO4FDFDMH11q2XgdUMJ97HXlqaPZjIY8k3I= -github.com/go-gost/x v0.15.7/go.mod h1:ZSKcbDLe6UulbWkDNCsqmVIHzda2CrxxzslIdmNt5O0= +github.com/go-gost/x v0.16.0 h1:u7m/B1e9YOQPOrUJUZ9U81b4dUg1ROcM0qUiwSE3Esg= +github.com/go-gost/x v0.16.0/go.mod h1:vijIgcAGgzfKhvPaV4NlPaoKX/C1C/8a3Zp1Yjjrok4= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/tests/e2e/proxyproto_test.go b/tests/e2e/proxyproto_test.go new file mode 100644 index 0000000..8a2bd3c --- /dev/null +++ b/tests/e2e/proxyproto_test.go @@ -0,0 +1,127 @@ +package e2e + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + + "github.com/moby/moby/client" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// ProxyProtoSuite verifies go-gost/gost#677: a forward/rtcp handler with +// metadata.proxyProtocol set prepends a HAProxy PROXY protocol header on the +// outbound connection before forwarding. The backend (proxy-capture) reads the +// first bytes and reflects the header line back so the test can assert it. +// +// This exercises the exact send path the issue asks for (forward/remote's +// proxyproto.WrapClientConn). A single gost instance is used so the header's +// source address is the real connecting client — in a full reverse tunnel the +// source would be the tunnel peer (relay CONNECT propagates only the +// destination); that limitation is out of scope here. +type ProxyProtoSuite struct { + suite.Suite + ctx context.Context + backendC testcontainers.Container + gostV1C testcontainers.Container + gostV2C testcontainers.Container +} + +func (s *ProxyProtoSuite) SetupSuite() { + s.ctx = context.Background() + + backendC, err := s.runBackend() + s.Require().NoError(err) + s.backendC = backendC + + gostV1C, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, + "testdata/proxyproto/v1.yaml", []string{"gost-v1"}, []string{"8080/tcp"}) + s.Require().NoError(err) + s.gostV1C = gostV1C + + gostV2C, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, + "testdata/proxyproto/v2.yaml", []string{"gost-v2"}, []string{"8080/tcp"}) + s.Require().NoError(err) + s.gostV2C = gostV2C +} + +func (s *ProxyProtoSuite) TearDownSuite() { + for _, c := range []testcontainers.Container{s.gostV1C, s.gostV2C, s.backendC} { + if c != nil { + c.Terminate(s.ctx) + } + } +} + +// runBackend starts the raw-TCP PROXY-header-capturing backend, aliased +// "proxy-capture" on the shared network, listening on 5678. It is also used as +// the client host (it has python3) to connect through gost. +func (s *ProxyProtoSuite) runBackend() (testcontainers.Container, error) { + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Context: ".", + Dockerfile: "Dockerfile", + Repo: "gost-e2e", + Tag: "latest", + KeepImage: true, + BuildOptionsModifier: func(opts *client.ImageBuildOptions) { + opts.NetworkMode = "host" + }, + }, + Networks: []string{SharedNetworkName}, + NetworkAliases: map[string][]string{ + SharedNetworkName: {"proxy-capture"}, + }, + Files: []testcontainers.ContainerFile{ + {HostFilePath: "scripts/proxy_capture.py", ContainerFilePath: "/scripts/proxy_capture.py", FileMode: 0644}, + }, + ExposedPorts: []string{"5678/tcp"}, + Cmd: []string{"python3", "/scripts/proxy_capture.py"}, + WaitingFor: wait.ForExposedPort(), + } + return testcontainers.GenericContainer(s.ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) +} + +// request connects from the backend container to gost on :8080, sends a payload, +// and returns the reflected response (which carries the PROXY header line). +func (s *ProxyProtoSuite) request(gostHost string) string { + cmd := []string{ + "sh", "-c", + fmt.Sprintf("python3 -c \"import socket,sys; s=socket.socket(); s.settimeout(5); s.connect(('%s',8080)); s.sendall(b'hello-gost'); sys.stdout.write(s.recv(4096).decode())\"", gostHost), + } + _, out, err := s.backendC.Exec(s.ctx, cmd) + s.Require().NoError(err) + body, err := io.ReadAll(out) + s.Require().NoError(err) + return string(body) +} + +// TestV1HeaderSent asserts gost prepends a text PROXY protocol v1 header. +func (s *ProxyProtoSuite) TestV1HeaderSent() { + body := s.request("gost-v1") + if !strings.Contains(body, "PROXY-RECEIVED") || !strings.Contains(body, "PROXY TCP") { + DumpLogs(s.T(), s.ctx, "gost-v1 logs", s.gostV1C) + } + s.Require().Contains(body, "PROXY-RECEIVED") + s.Require().Contains(body, "PROXY TCP") +} + +// TestV2HeaderSent asserts gost prepends a binary PROXY protocol v2 header. +func (s *ProxyProtoSuite) TestV2HeaderSent() { + body := s.request("gost-v2") + if !strings.Contains(body, "PROXY-V2-RECEIVED") { + DumpLogs(s.T(), s.ctx, "gost-v2 logs", s.gostV2C) + } + s.Require().Contains(body, "PROXY-V2-RECEIVED") +} + +func TestProxyProtoSuite(t *testing.T) { + suite.Run(t, new(ProxyProtoSuite)) +} diff --git a/tests/e2e/scripts/echo754_repro.py b/tests/e2e/scripts/echo754_repro.py new file mode 100644 index 0000000..011da8c --- /dev/null +++ b/tests/e2e/scripts/echo754_repro.py @@ -0,0 +1,22 @@ +import socket, threading + +s = socket.socket() +s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +s.bind(("127.0.0.1", 15738)) +s.listen(50) + +def loop(c): + try: + while True: + d = c.recv(65536) + if not d: + break + c.sendall(d) + except OSError: + pass + finally: + c.close() + +while True: + c, _ = s.accept() + threading.Thread(target=loop, args=(c,), daemon=True).start() diff --git a/tests/e2e/scripts/proxy_capture.py b/tests/e2e/scripts/proxy_capture.py new file mode 100644 index 0000000..574b977 --- /dev/null +++ b/tests/e2e/scripts/proxy_capture.py @@ -0,0 +1,23 @@ +# Raw-TCP backend for the PROXY-protocol e2e test (issue #677). +# Reads the first bytes of each connection and reflects whether a HAProxy +# PROXY protocol header was prepended by gost: +# - v1 ("PROXY TCP4 ...") -> "PROXY-RECEIVED: PROXY TCP4 ..." +# - v2 (12-byte signature) -> "PROXY-V2-RECEIVED" +# - neither -> "NO-PROXY" +import socket + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +s.bind(("0.0.0.0", 5678)) +s.listen(5) + +while True: + c, _ = s.accept() + d = c.recv(4096) + if d[:12] == b"\r\n\r\n\x00\r\nQUIT\n": + c.sendall(b"PROXY-V2-RECEIVED\n") + elif d[:6] == b"PROXY ": + c.sendall(b"PROXY-RECEIVED: " + d.split(b"\r\n", 1)[0] + b"\n") + else: + c.sendall(b"NO-PROXY\n") + c.close() diff --git a/tests/e2e/testdata/proxyproto/v1.yaml b/tests/e2e/testdata/proxyproto/v1.yaml new file mode 100644 index 0000000..553accb --- /dev/null +++ b/tests/e2e/testdata/proxyproto/v1.yaml @@ -0,0 +1,19 @@ +# go-gost/gost#677 — send-proxy (PROXY protocol v1) for the rtcp forward handler. +# gost accepts a TCP connection on :8080 and forwards it to the backend, +# prepending a "PROXY TCP4 ..." header because metadata.proxyProtocol is 1. +services: + - name: service-0 + addr: ":8080" + handler: + type: rtcp + metadata: + proxyProtocol: 1 + listener: + type: rtcp + forwarder: + nodes: + - name: target-0 + addr: proxy-capture:5678 + +log: + level: debug diff --git a/tests/e2e/testdata/proxyproto/v2.yaml b/tests/e2e/testdata/proxyproto/v2.yaml new file mode 100644 index 0000000..5dc8031 --- /dev/null +++ b/tests/e2e/testdata/proxyproto/v2.yaml @@ -0,0 +1,18 @@ +# go-gost/gost#677 — send-proxy (PROXY protocol v2) for the rtcp forward handler. +# Same as v1.yaml but emits the binary v2 header (metadata.proxyProtocol: 2). +services: + - name: service-0 + addr: ":8080" + handler: + type: rtcp + metadata: + proxyProtocol: 2 + listener: + type: rtcp + forwarder: + nodes: + - name: target-0 + addr: proxy-capture:5678 + +log: + level: debug