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

211 lines
6.3 KiB
Go

//go:build linux || darwin
package tlsspoof
import (
"encoding/hex"
"io"
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestIntegrationSpoofer_WrongChecksum(t *testing.T) {
requireRoot(t)
client, serverPort := dialLocalEchoServer(t)
spoofer, err := NewSpoofer(client, MethodWrongChecksum)
require.NoError(t, err)
defer spoofer.Close()
fake, err := buildFakeClientHello("letsencrypt.org")
require.NoError(t, err)
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
require.NoError(t, spoofer.Inject(fake))
}, 3*time.Second)
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
}
func TestIntegrationSpoofer_WrongSequence(t *testing.T) {
requireRoot(t)
client, serverPort := dialLocalEchoServer(t)
spoofer, err := NewSpoofer(client, MethodWrongSequence)
require.NoError(t, err)
defer spoofer.Close()
fake, err := buildFakeClientHello("letsencrypt.org")
require.NoError(t, err)
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
require.NoError(t, spoofer.Inject(fake))
}, 3*time.Second)
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
}
func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) {
requireRoot(t)
client, serverPort := dialLocalEchoServerIPv6(t)
spoofer, err := NewSpoofer(client, MethodWrongChecksum)
require.NoError(t, err)
defer spoofer.Close()
fake, err := buildFakeClientHello("letsencrypt.org")
require.NoError(t, err)
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
require.NoError(t, spoofer.Inject(fake))
}, 3*time.Second)
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
}
func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) {
requireRoot(t)
client, serverPort := dialLocalEchoServerIPv6(t)
spoofer, err := NewSpoofer(client, MethodWrongSequence)
require.NoError(t, err)
defer spoofer.Close()
fake, err := buildFakeClientHello("letsencrypt.org")
require.NoError(t, err)
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
require.NoError(t, spoofer.Inject(fake))
}, 3*time.Second)
require.True(t, captured, "injected fake ClientHello must be observable on loopback")
}
// Loopback bypasses TCP checksum validation, so wrong-sequence is used instead.
func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) {
requireRoot(t)
runInjectsThenForwardsRealCH(t, "tcp4", "127.0.0.1:0")
}
func TestIntegrationConn_IPv6_InjectsThenForwardsRealCH(t *testing.T) {
requireRoot(t)
runInjectsThenForwardsRealCH(t, "tcp6", "[::1]:0")
}
// TestIntegrationConn_FakeAndRealHaveDistinctSNIs asserts that the on-wire fake
// packet carries the fake SNI (letsencrypt.org) AND the real packet still
// carries the original SNI (github.com). If the builder regresses to producing
// empty or mismatched bytes, the fake-SNI needle will be missing.
func TestIntegrationConn_FakeAndRealHaveDistinctSNIs(t *testing.T) {
requireRoot(t)
runFakeAndRealHaveDistinctSNIs(t, "tcp4", "127.0.0.1:0", "letsencrypt.org")
}
func TestIntegrationConn_IPv6_FakeAndRealHaveDistinctSNIs(t *testing.T) {
requireRoot(t)
runFakeAndRealHaveDistinctSNIs(t, "tcp6", "[::1]:0", "letsencrypt.org")
}
func runFakeAndRealHaveDistinctSNIs(t *testing.T, network, address, fakeSNI string) {
t.Helper()
const originalSNI = "github.com"
require.NotEqual(t, originalSNI, fakeSNI)
listener, err := net.Listen(network, address)
require.NoError(t, err)
serverReceived := make(chan []byte, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
got, _ := io.ReadAll(conn)
serverReceived <- got
}()
addr := listener.Addr().(*net.TCPAddr)
serverPort := uint16(addr.Port)
client, err := net.Dial(network, addr.String())
require.NoError(t, err)
t.Cleanup(func() {
client.Close()
listener.Close()
})
spoofer, err := NewSpoofer(client, MethodWrongSequence)
require.NoError(t, err)
wrapped, err := NewConn(client, spoofer, fakeSNI)
require.NoError(t, err)
payload, err := hex.DecodeString(realClientHello)
require.NoError(t, err)
seen := tcpdumpObserverMulti(t, loopbackInterface, serverPort,
[]string{originalSNI, fakeSNI}, func() {
n, err := wrapped.Write(payload)
require.NoError(t, err)
require.Equal(t, len(payload), n)
}, 3*time.Second)
require.True(t, seen[originalSNI],
"real ClientHello must carry original SNI %q on the wire", originalSNI)
require.True(t, seen[fakeSNI],
"fake ClientHello must carry fake SNI %q on the wire", fakeSNI)
_ = wrapped.Close()
select {
case got := <-serverReceived:
require.Equal(t, payload, got,
"server must receive real ClientHello unchanged (wrong-sequence fake must be dropped)")
case <-time.After(2 * time.Second):
t.Fatal("echo server did not receive real ClientHello")
}
}
func runInjectsThenForwardsRealCH(t *testing.T, network, address string) {
t.Helper()
listener, err := net.Listen(network, address)
require.NoError(t, err)
serverReceived := make(chan []byte, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
got, _ := io.ReadAll(conn)
serverReceived <- got
}()
addr := listener.Addr().(*net.TCPAddr)
serverPort := uint16(addr.Port)
client, err := net.Dial(network, addr.String())
require.NoError(t, err)
t.Cleanup(func() {
client.Close()
listener.Close()
})
spoofer, err := NewSpoofer(client, MethodWrongSequence)
require.NoError(t, err)
wrapped, err := NewConn(client, spoofer, "letsencrypt.org")
require.NoError(t, err)
payload, err := hex.DecodeString(realClientHello)
require.NoError(t, err)
captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() {
n, err := wrapped.Write(payload)
require.NoError(t, err)
require.Equal(t, len(payload), n)
}, 3*time.Second)
require.True(t, captured, "fake ClientHello with letsencrypt.org SNI must be on the wire")
_ = wrapped.Close()
select {
case got := <-serverReceived:
require.Equal(t, payload, got, "server must receive real ClientHello unchanged (wrong-sequence fake must be dropped)")
case <-time.After(2 * time.Second):
t.Fatal("echo server did not receive real ClientHello")
}
}