test(e2e): add utls dialer regression suite (#888)

Adds an e2e suite (tests/e2e/utls_test.go + testdata/utls/*)
that exercises the utls dialer in a forward-proxy chain:

  curl -> client gost (http proxy :8080)
       -> chain node (http connector + utls dialer)
         -> server gost (http over TLS listener :8443)
           -> tcp-echo

Two cases:

- TestUTLSInsecure: regression for go-gost/gost#887. A utls
  dialer with `secure: false` must still complete the handshake
  (InsecureSkipVerify must be honoured). The old unsafe cast read
  garbage for InsecureSkipVerify and the handshake failed.
- TestUTLSSecureWithCA: exercises the converter's RootCAs /
  ServerName path with a CA-signed server cert.

Uses the deterministic `Chrome` fingerprint (not `randomized`, which
randomises the ClientHello and is flaky against a standard Go TLS
server).

Bumps github.com/go-gost/x to v0.13.11, which fixes the
second half of #887: the curve-preference enum divergence between
crypto/tls and utls (Go 1.24+ appends PQC hybrid curves that
utls does not define).

Related: go-gost/gost#887, go-gost/x#111, go-gost/x#112
master
ginuerzh 2026-07-11 15:46:39 +08:00 committed by GitHub
parent ccf2989bc0
commit 64b71bd8a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 260 additions and 1 deletions

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.26.3
require (
github.com/go-gost/core v0.5.3
github.com/go-gost/x v0.13.9
github.com/go-gost/x v0.13.11
github.com/judwhite/go-svc v1.2.1
github.com/moby/moby/client v0.4.0
github.com/stretchr/testify v1.11.1

4
go.sum
View File

@ -99,6 +99,10 @@ github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc
github.com/go-gost/tls-dissector v0.2.0/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
github.com/go-gost/x v0.13.9 h1:/pK8rOiueV239i0ntesDnU15UeYrq8vL6iVeGPJTzJo=
github.com/go-gost/x v0.13.9/go.mod h1:etkIOM5JjH9pnb3UOvLKexodznKcK3oRNGniPk0AGxk=
github.com/go-gost/x v0.13.10 h1:RCd65A5KuB7ZLDw317tt9O//sjrlxijXXTO0jbpkfkY=
github.com/go-gost/x v0.13.10/go.mod h1:etkIOM5JjH9pnb3UOvLKexodznKcK3oRNGniPk0AGxk=
github.com/go-gost/x v0.13.11 h1:508vT37VjgeGqQPBj+DG3yw0Z1TMLwKLl90wLeVknWI=
github.com/go-gost/x v0.13.11/go.mod h1:etkIOM5JjH9pnb3UOvLKexodznKcK3oRNGniPk0AGxk=
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=

View File

@ -0,0 +1,24 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: utls-chain
listener:
type: tcp
chains:
- name: utls-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: utls
metadata:
fingerprint: Chrome
tls:
secure: false

View File

@ -0,0 +1,26 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: utls-chain
listener:
type: tcp
chains:
- name: utls-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: utls
metadata:
fingerprint: Chrome
tls:
secure: true
serverName: utls-server
caFile: /certs/ca.pem

View File

@ -0,0 +1,7 @@
services:
- name: https-server
addr: :8443
handler:
type: http
listener:
type: tls

View File

@ -0,0 +1,10 @@
services:
- name: https-server
addr: :8443
handler:
type: http
listener:
type: tls
tls:
certFile: /certs/server.pem
keyFile: /certs/server-key.pem

View File

@ -0,0 +1,188 @@
package e2e
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"math/big"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// UTLSSuite exercises the utls dialer in a forward-proxy chain.
//
// Topology:
//
// curl -> client gost (http proxy, :8080)
// -> chain node (http connector + utls dialer)
// -> server gost (http proxy over TLS listener, :8443)
// -> tcp-echo
type UTLSSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
certDir string
}
func (s *UTLSSuite) SetupSuite() {
s.ctx = context.Background()
certDir, err := os.MkdirTemp("", "gost-utls-certs-*")
s.Require().NoError(err)
s.certDir = certDir
s.generateCerts()
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
}
func (s *UTLSSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
os.RemoveAll(s.certDir)
}
// generateCerts creates a self-signed CA and a server cert for the
// "utls-server" hostname, writing PEM files to s.certDir.
func (s *UTLSSuite) generateCerts() {
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
s.Require().NoError(err)
caTmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
}
caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey)
s.Require().NoError(err)
s.writeCertPEM(s.certDir+"/ca.pem", "CERTIFICATE", caDER)
s.writeKeyPEM(s.certDir+"/ca-key.pem", "EC PRIVATE KEY", caKey)
serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
s.Require().NoError(err)
serverTmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "utls-server"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"utls-server"},
}
serverDER, err := x509.CreateCertificate(rand.Reader, serverTmpl, caTmpl, &serverKey.PublicKey, caKey)
s.Require().NoError(err)
s.writeCertPEM(s.certDir+"/server.pem", "CERTIFICATE", serverDER)
s.writeKeyPEM(s.certDir+"/server-key.pem", "EC PRIVATE KEY", serverKey)
}
// TestUTLSInsecure is the regression test for go-gost/gost#887.
//
// A utls dialer with `secure: false` must still complete the handshake
// (InsecureSkipVerify must be honoured). The previous unsafe.Pointer cast
// read garbage for InsecureSkipVerify, so it came back false and the
// handshake failed with "at least one of ServerName, InsecureSkipVerify or
// InsecureServerNameToVerify must be specified".
func (s *UTLSSuite) TestUTLSInsecure() {
rendered, err := RenderConfig("testdata/utls/client_insecure.yaml",
ConfigData{ServerAddr: "utls-server:8443"})
s.Require().NoError(err)
defer os.Remove(rendered)
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/utls/server_auto.yaml", []string{"utls-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
s.requireProxyWorks(clientC, serverC)
}
// TestUTLSSecureWithCA verifies the converter's RootCAs/ServerName path:
// with `secure: true` and a CA file, the utls dialer must verify the
// server's CA-signed certificate and still reach the echo server.
func (s *UTLSSuite) TestUTLSSecureWithCA() {
rendered, err := RenderConfig("testdata/utls/client_secure.yaml",
ConfigData{ServerAddr: "utls-server:8443"})
s.Require().NoError(err)
defer os.Remove(rendered)
serverC, err := runGostContainer(s.ctx, SharedNetworkName,
"testdata/utls/server_ca.yaml",
[]string{"utls-server"}, []string{"8443/tcp"},
[]testcontainers.ContainerFile{
{HostFilePath: s.certDir + "/server.pem", ContainerFilePath: "/certs/server.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/server-key.pem", ContainerFilePath: "/certs/server-key.pem", FileMode: 0644},
})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
clientC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, rendered,
[]testcontainers.ContainerFile{
{HostFilePath: s.certDir + "/ca.pem", ContainerFilePath: "/certs/ca.pem", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
s.requireProxyWorks(clientC, serverC)
}
// requireProxyWorks drives curl through the client proxy and asserts the
// echo server's "hello-gost" response is returned.
func (s *UTLSSuite) requireProxyWorks(clientC, serverC testcontainers.Container) {
cmd := []string{"curl", "-v", "-s", "--connect-timeout", "5",
"-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
body, err2 := io.ReadAll(out)
if err != nil || err2 != nil || code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "utls client logs", clientC)
DumpLogs(s.T(), s.ctx, "utls server logs", serverC)
}
s.Require().NoError(err)
s.Require().NoError(err2)
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
func TestUTLSSuite(t *testing.T) {
suite.Run(t, new(UTLSSuite))
}
// --- helpers (mirror mtls_test.go) ---
func (s *UTLSSuite) writeCertPEM(path, blockType string, der []byte) {
err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: der}), 0644)
s.Require().NoError(err)
}
func (s *UTLSSuite) writeKeyPEM(path, blockType string, key *ecdsa.PrivateKey) {
b, err := x509.MarshalECPrivateKey(key)
s.Require().NoError(err)
err = os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: b}), 0600)
s.Require().NoError(err)
}