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.
This commit is contained in:
ginuerzh
2026-08-29 09:26:33 +08:00
parent 76467efc59
commit cb76f63754
7 changed files with 215 additions and 6 deletions
+2 -2
View File
@@ -3,8 +3,8 @@ module github.com/go-gost/gost
go 1.26.3 go 1.26.3
require ( require (
github.com/go-gost/core v0.6.0 github.com/go-gost/core v0.6.1
github.com/go-gost/x v0.15.7 github.com/go-gost/x v0.16.0
github.com/judwhite/go-svc v1.2.1 github.com/judwhite/go-svc v1.2.1
github.com/moby/moby/client v0.4.0 github.com/moby/moby/client v0.4.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
+4 -4
View File
@@ -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-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 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= 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.1 h1:mBBvZpxIbrspuRsksj7YLHEiBGEc+sQBV3wDs4rX/AE=
github.com/go-gost/core v0.6.0/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A= 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 h1:n2Po4TDKdLp1PsvhSiWFB/6S4e/YKZfsKJkA0PUa168=
github.com/go-gost/go-shadowsocks2 v0.1.4/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys= github.com/go-gost/go-shadowsocks2 v0.1.4/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys=
github.com/go-gost/gosocks4 v0.1.0 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk= 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/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 h1:gvOteWog5pjY/HCpc8l+gngmSi8Q6zl5rRrfK8gwRKA=
github.com/go-gost/tls-dissector v0.3.1/go.mod h1:vGfog053fIm93iXBtvmVzMQqEJo5YwbbNaPNVDjbiOc= 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.16.0 h1:u7m/B1e9YOQPOrUJUZ9U81b4dUg1ROcM0qUiwSE3Esg=
github.com/go-gost/x v0.15.7/go.mod h1:ZSKcbDLe6UulbWkDNCsqmVIHzda2CrxxzslIdmNt5O0= 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.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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+127
View File
@@ -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))
}
+22
View File
@@ -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()
+23
View File
@@ -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()
+19
View File
@@ -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
+18
View File
@@ -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