mirror of
https://github.com/go-gost/gost.git
synced 2026-10-08 03:55:45 +00:00
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:
@@ -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))
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user