Files
Archive/sing-box/common/tlsspoof/integration_tls_test.go
T
2026-04-17 21:15:10 +02:00

121 lines
3.8 KiB
Go

//go:build linux || darwin
package tlsspoof
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"io"
"math/big"
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// generateSelfSignedCert returns a TLS certificate valid for the given SAN.
func generateSelfSignedCert(t *testing.T, commonName string, sans ...string) tls.Certificate {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: commonName},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: sans,
}
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyDER, err := x509.MarshalPKCS8PrivateKey(priv)
require.NoError(t, err)
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
cert, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
return cert
}
// TestIntegrationConn_RealTLSHandshake drives a real crypto/tls ClientHello
// through the spoofer and asserts the on-wire fake packet carries the fake SNI
// while the server receives the real SNI. This exercises the full
// `tls.Client(wrapped, config).Handshake()` path rather than a static hex
// payload, matching what user-facing code hits.
func TestIntegrationConn_RealTLSHandshake(t *testing.T) {
requireRoot(t)
const realSNI = "real.test"
const fakeSNI = "fake.test"
serverCert := generateSelfSignedCert(t, realSNI, realSNI)
tlsConfig := &tls.Config{Certificates: []tls.Certificate{serverCert}}
listener, err := tls.Listen("tcp4", "127.0.0.1:0", tlsConfig)
require.NoError(t, err)
t.Cleanup(func() { listener.Close() })
serverSNI := make(chan string, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
tlsConn := conn.(*tls.Conn)
_ = tlsConn.SetDeadline(time.Now().Add(3 * time.Second))
if handshakeErr := tlsConn.Handshake(); handshakeErr != nil {
serverSNI <- "handshake-error:" + handshakeErr.Error()
return
}
serverSNI <- tlsConn.ConnectionState().ServerName
_, _ = io.Copy(io.Discard, conn)
}()
addr := listener.Addr().(*net.TCPAddr)
serverPort := uint16(addr.Port)
raw, err := net.Dial("tcp4", addr.String())
require.NoError(t, err)
t.Cleanup(func() { raw.Close() })
spoofer, err := NewSpoofer(raw, MethodWrongSequence)
require.NoError(t, err)
wrapped, err := NewConn(raw, spoofer, fakeSNI)
require.NoError(t, err)
clientConfig := &tls.Config{
ServerName: realSNI,
InsecureSkipVerify: true,
}
tlsClient := tls.Client(wrapped, clientConfig)
t.Cleanup(func() { tlsClient.Close() })
seen := tcpdumpObserverMulti(t, loopbackInterface, serverPort,
[]string{realSNI, fakeSNI}, func() {
_ = tlsClient.SetDeadline(time.Now().Add(3 * time.Second))
err := tlsClient.Handshake()
require.NoError(t, err, "TLS handshake must succeed (wrong-sequence fake is dropped by peer)")
}, 4*time.Second)
require.True(t, seen[realSNI],
"real ClientHello on the wire must contain original SNI %q", realSNI)
require.True(t, seen[fakeSNI],
"fake ClientHello on the wire must contain fake SNI %q", fakeSNI)
select {
case sniOnServer := <-serverSNI:
require.Equal(t, realSNI, sniOnServer,
"TLS server must see the real SNI (fake packet dropped by peer TCP stack)")
case <-time.After(3 * time.Second):
t.Fatal("TLS server did not complete handshake")
}
}