mirror of
https://github.com/go-gost/gost.git
synced 2026-10-08 12:05:46 +00:00
chore: bump x to v0.15.0 (hopGroup, failCodes fix)
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.
This commit is contained in:
@@ -4,7 +4,7 @@ go 1.26.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-gost/core v0.6.0
|
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/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
|
||||||
|
|||||||
@@ -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/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.14.2 h1:CGzZEsP/lzhG6A3u67ulkw5t1oWtDr+RQTm5ZB7R1QY=
|
github.com/go-gost/x v0.15.0 h1:ugklRGp3fg8lveJ5JOp0wpnF2xxmP3qzb1TyWkEGDGc=
|
||||||
github.com/go-gost/x v0.14.2/go.mod h1:YQdy2hZ9nZ1w5GDacGKWrC4jIYXzthAKO/Er/K65+c4=
|
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.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=
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
+27
@@ -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
|
||||||
Reference in New Issue
Block a user