Files
gost/tests/e2e/sniffing_test.go
ginuerzh 55bb214a94 chore: bump x to v0.15.1, add e2e tests for chainGroup and sniffing
- Bump github.com/go-gost/x v0.15.0 → v0.15.1
- ChainGroupSuite: matcher routing by host, probe dead-chain pre-marking
- SniffingSuite: SOCKS5+HTTPS w/o SNI, debug-only log on empty SNI
- Add RunHTTPSEchoContainer and https_echo.py test helper
2026-07-29 23:06:15 +08:00

116 lines
3.4 KiB
Go

package e2e
import (
"context"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// SniffingSuite covers protocol sniffing behavior.
type SniffingSuite struct {
suite.Suite
ctx context.Context
httpsEchoC testcontainers.Container
httpsIP string
}
func (s *SniffingSuite) SetupSuite() {
s.ctx = context.Background()
c, err := RunHTTPSEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.httpsEchoC = c
ip, err := c.ContainerIP(s.ctx)
s.Require().NoError(err)
s.httpsIP = ip
}
func (s *SniffingSuite) TearDownSuite() {
if s.httpsEchoC != nil {
s.httpsEchoC.Terminate(s.ctx)
}
}
// curlThroughSOCKS5 runs curl through the GOST SOCKS5 proxy on the given
// container address and port, connecting to the HTTPS echo server by IP.
// Connecting by IP makes curl omit the TLS SNI extension.
func (s *SniffingSuite) curlThroughSOCKS5(c testcontainers.Container, proxyPort string) string {
s.T().Helper()
// curl -k: don't verify the self-signed cert on the echo server
// curl -x socks5://...: route through GOST SOCKS5 proxy
// Using the container IP as the host makes curl skip SNI entirely.
cmd := []string{"curl", "-k", "-s",
"-x", "socks5://127.0.0.1:" + proxyPort,
"https://" + s.httpsIP + ":8443",
}
code, out, err := c.Exec(s.ctx, cmd)
s.Require().NoError(err)
s.Require().Zero(code, "curl should exit 0")
b, _ := io.ReadAll(out)
return string(b)
}
// TestSOCKS5NoSNI verifies that a SOCKS5 proxy with sniffing enabled can
// successfully forward TLS connections that lack an SNI extension — such as
// when a client connects to an HTTPS server by IP address.
func (s *SniffingSuite) TestSOCKS5NoSNI() {
gostC, err := RunGostContainer(s.ctx, SharedNetworkName,
"testdata/sniffing/no_sni.yaml",
)
s.Require().NoError(err)
defer func() {
DumpLogs(s.T(), s.ctx, "gost-no-sni", gostC)
gostC.Terminate(s.ctx)
}()
body := s.curlThroughSOCKS5(gostC, "8080")
s.Require().Contains(body, "hello-gost",
"HTTPS response should pass through when SNI is empty")
}
// TestSOCKS5NoSNI_Callback verifies the same behavior a second time, serving
// as a sanity check that state doesn't leak between connections.
func (s *SniffingSuite) TestSOCKS5NoSNI_Callback() {
s.TestSOCKS5NoSNI()
}
// TestSOCKS5NoSNI_LogsOnlyDebug verifies the proxy logs a debug message
// rather than an error when SNI is missing.
func (s *SniffingSuite) TestSOCKS5NoSNI_LogsOnlyDebug() {
gostC, err := RunGostContainer(s.ctx, SharedNetworkName,
"testdata/sniffing/no_sni.yaml",
)
s.Require().NoError(err)
defer func() {
// Do NOT dump logs before the grep — DumpLogs consumes the reader
gostC.Terminate(s.ctx)
}()
body := s.curlThroughSOCKS5(gostC, "8080")
s.Require().Contains(body, "hello-gost")
// Verify the debug log message exists and no error-level SNI message.
logs, err := gostC.Logs(s.ctx)
s.Require().NoError(err)
defer logs.Close()
logBody, _ := io.ReadAll(logs)
logStr := string(logBody)
s.Require().Contains(logStr, "no sni in clienthello",
"should log a debug message when SNI is empty")
s.Require().NotContains(logStr, "tls: sni is empty",
"must not error on empty SNI")
// The log level for "no sni" should be debug, not error.
s.Require().False(strings.Contains(logStr, `"level":"error"`),
"should not log any error-level message")
}
func TestSniffingSuite(t *testing.T) {
suite.Run(t, new(SniffingSuite))
}