From 555dacd244b538bc843a2aa0360f40e3d10819b9 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Mon, 27 Jul 2026 22:13:34 +0800 Subject: [PATCH] chore: bump x to v0.15.0 (hopGroup, failCodes fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add e2e regression test for http.failCodes selector bug — FIFO selector + failCodes=429 on first node proves node-429 is excluded after first 429 response and all subsequent requests go to node-good. --- go.mod | 2 +- go.sum | 4 +- tests/e2e/failcodes_test.go | 143 ++++++++++++++++++++ tests/e2e/scripts/http_status_backend.py | 35 +++++ tests/e2e/testdata/failcodes/failcodes.yaml | 27 ++++ 5 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/failcodes_test.go create mode 100644 tests/e2e/scripts/http_status_backend.py create mode 100644 tests/e2e/testdata/failcodes/failcodes.yaml diff --git a/go.mod b/go.mod index 23c56f7..ce9dd9a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.3 require ( github.com/go-gost/core v0.6.0 - github.com/go-gost/x v0.14.2 + github.com/go-gost/x v0.15.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 ccdd477..a94156e 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,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.14.2 h1:CGzZEsP/lzhG6A3u67ulkw5t1oWtDr+RQTm5ZB7R1QY= -github.com/go-gost/x v0.14.2/go.mod h1:YQdy2hZ9nZ1w5GDacGKWrC4jIYXzthAKO/Er/K65+c4= +github.com/go-gost/x v0.15.0 h1:ugklRGp3fg8lveJ5JOp0wpnF2xxmP3qzb1TyWkEGDGc= +github.com/go-gost/x v0.15.0/go.mod h1:YQdy2hZ9nZ1w5GDacGKWrC4jIYXzthAKO/Er/K65+c4= 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/failcodes_test.go b/tests/e2e/failcodes_test.go new file mode 100644 index 0000000..f0b64fe --- /dev/null +++ b/tests/e2e/failcodes_test.go @@ -0,0 +1,143 @@ +package e2e + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/moby/moby/client" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +type FailCodesSuite struct { + suite.Suite + ctx context.Context + echoC testcontainers.Container + echoIP string + statusC testcontainers.Container +} + +func (s *FailCodesSuite) SetupSuite() { + s.ctx = context.Background() + + echoC, err := RunEchoContainer(s.ctx, SharedNetworkName) + s.Require().NoError(err) + s.echoC = echoC + + echoIP, err := echoC.ContainerIP(s.ctx) + s.Require().NoError(err) + s.echoIP = echoIP + + statusC, err := RunStatusBackendContainer(s.ctx, SharedNetworkName, 429) + s.Require().NoError(err) + s.statusC = statusC +} + +func (s *FailCodesSuite) TearDownSuite() { + if s.echoC != nil { + s.echoC.Terminate(s.ctx) + } + if s.statusC != nil { + s.statusC.Terminate(s.ctx) + } +} + +func (s *FailCodesSuite) sendRawHTTP(gostC testcontainers.Container, host, port string) string { + req := fmt.Sprintf( + "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", + s.echoIP, + ) + encoded := base64.StdEncoding.EncodeToString([]byte(req)) + cmd := []string{"sh", "-c", + fmt.Sprintf("echo %s | base64 -d | nc -w 5 %s %s", encoded, host, port)} + _, out, _ := gostC.Exec(s.ctx, cmd) + b, _ := io.ReadAll(out) + return string(b) +} + +// TestFailCodesConvergence: reverse proxy (tcp handler + sniffing) with FIFO. +// node-429 is first. After the first 429 response, failCodes marks it (Count=1). +// FailFilter (maxFails=1) excludes it. All subsequent requests go to node-good. +func (s *FailCodesSuite) TestFailCodesConvergence() { + gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, + "testdata/failcodes/failcodes.yaml", + nil, + "8080/tcp", + ) + s.Require().NoError(err) + defer func() { + DumpLogs(s.T(), s.ctx, "gost-failcodes", gostC) + gostC.Terminate(s.ctx) + }() + + time.Sleep(500 * time.Millisecond) + + const totalRequests = 20 + var successCount, failCount int + + for range totalRequests { + body := s.sendRawHTTP(gostC, "127.0.0.1", "8080") + + if strings.Contains(body, "hello-gost") { + successCount++ + s.T().Logf(" ✓ 200 (node-good)") + } else if strings.Contains(body, "status-429") { + failCount++ + s.T().Logf(" ✗ 429 (node-429)") + } else { + s.T().Logf(" ? %s", strings.TrimSpace(body)[:80]) + } + } + + s.T().Logf("Results: %d successes, %d failures out of %d requests", + successCount, failCount, totalRequests) + + // After the first 429 marks node-429, FailFilter excludes it for 10s. + // At most 1-2 requests may fail before the marker is set. + s.Require().LessOrEqual(failCount, 2, + "failCodes: node-429 must be excluded after first 429 response. "+ + "Got %d failures out of %d requests", failCount, totalRequests) +} + +func TestFailCodesSuite(t *testing.T) { + suite.Run(t, new(FailCodesSuite)) +} + +func RunStatusBackendContainer(ctx context.Context, networkName string, statusCode int) (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{networkName}, + NetworkAliases: map[string][]string{ + networkName: {"status-backend"}, + }, + Files: []testcontainers.ContainerFile{ + {HostFilePath: "scripts/http_status_backend.py", ContainerFilePath: "/scripts/http_status_backend.py", FileMode: 0644}, + }, + ExposedPorts: []string{"5680/tcp"}, + Cmd: []string{ + "python3", "/scripts/http_status_backend.py", + fmt.Sprintf("%d", statusCode), "5680", + }, + WaitingFor: wait.ForExposedPort(), + } + + return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) +} diff --git a/tests/e2e/scripts/http_status_backend.py b/tests/e2e/scripts/http_status_backend.py new file mode 100644 index 0000000..93768e0 --- /dev/null +++ b/tests/e2e/scripts/http_status_backend.py @@ -0,0 +1,35 @@ +"""HTTP server that always returns a fixed status code.""" +import sys +from http.server import BaseHTTPRequestHandler, HTTPServer + + +STATUS = int(sys.argv[1]) if len(sys.argv) > 1 else 429 +PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 5680 +BODY = f"status-{STATUS}".encode() + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(STATUS) + self.send_header("Content-Length", str(len(BODY))) + self.end_headers() + self.wfile.write(BODY) + + def do_POST(self): + self.send_response(STATUS) + self.send_header("Content-Length", str(len(BODY))) + self.end_headers() + self.wfile.write(BODY) + + # HTTP proxy CONNECT method + def do_CONNECT(self): + self.send_response(STATUS) + self.send_header("Content-Length", str(len(BODY))) + self.end_headers() + self.wfile.write(BODY) + + def log_message(self, format, *args): + return + + +HTTPServer(("0.0.0.0", PORT), Handler).serve_forever() diff --git a/tests/e2e/testdata/failcodes/failcodes.yaml b/tests/e2e/testdata/failcodes/failcodes.yaml new file mode 100644 index 0000000..ccbcf46 --- /dev/null +++ b/tests/e2e/testdata/failcodes/failcodes.yaml @@ -0,0 +1,27 @@ +services: +# Reverse proxy with sniffing: raw TCP listener inspects HTTP via the +# forwarder's sniffer. The hop has FIFO selector (always picks first node). +# node-429 is first in the list with failCodes=429 — it must be marked on +# the first 429 response and excluded by FailFilter (maxFails=1). +- name: reverse-proxy + addr: :8080 + handler: + type: tcp + metadata: + sniffing: true + sniffing.timeout: 2s + listener: + type: tcp + forwarder: + selector: + strategy: fifo + maxFails: 1 + nodes: + - name: node-429 + addr: status-backend:5680 + protocol: http + http: + failCodes: "429" + - name: node-good + addr: tcp-echo:5678 + protocol: http