mirror of https://github.com/go-gost/gost.git
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
parent
ccf2989bc0
commit
64b71bd8a6
2
go.mod
2
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
services:
|
||||
- name: https-server
|
||||
addr: :8443
|
||||
handler:
|
||||
type: http
|
||||
listener:
|
||||
type: tls
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue