gost/tests/e2e/utls_test.go

189 lines
6.2 KiB
Go

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)
}