mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
189 lines
5.5 KiB
Go
189 lines
5.5 KiB
Go
package sniff_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"errors"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sagernet/quic-go"
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/common/sniff"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
|
|
t.Parallel()
|
|
const testSNI = "test.example.com"
|
|
|
|
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
|
require.NoError(t, err)
|
|
defer udpConn.Close()
|
|
|
|
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
|
packetsChan := make(chan [][]byte, 1)
|
|
|
|
go func() {
|
|
var packets [][]byte
|
|
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
for i := 0; i < 10; i++ {
|
|
buf := make([]byte, 2048)
|
|
n, _, err := udpConn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
break
|
|
}
|
|
packets = append(packets, buf[:n])
|
|
}
|
|
packetsChan <- packets
|
|
}()
|
|
|
|
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
|
require.NoError(t, err)
|
|
defer clientConn.Close()
|
|
|
|
tlsConfig := &tls.Config{
|
|
ServerName: testSNI,
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{"h3"},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
|
|
|
select {
|
|
case packets := <-packetsChan:
|
|
t.Logf("Captured %d packets", len(packets))
|
|
|
|
var metadata adapter.InboundContext
|
|
for i, pkt := range packets {
|
|
err := sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
|
t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client)
|
|
if metadata.Domain != "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
t.Logf("\n=== quic-go TLS Fingerprint Analysis ===")
|
|
t.Logf("Domain: %s", metadata.Domain)
|
|
t.Logf("Client: %s", metadata.Client)
|
|
t.Logf("Protocol: %s", metadata.Protocol)
|
|
|
|
// The client should be identified as quic-go, not chromium
|
|
// Current issue: it's being identified as chromium
|
|
if metadata.Client == "chromium" {
|
|
t.Log("WARNING: quic-go is being misidentified as chromium!")
|
|
}
|
|
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Timeout")
|
|
}
|
|
}
|
|
|
|
func TestSniffQUICInitialFromQuicGo(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const testSNI = "test.example.com"
|
|
|
|
// Create UDP listener to capture ALL initial packets
|
|
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
|
require.NoError(t, err)
|
|
defer udpConn.Close()
|
|
|
|
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
|
|
|
// Channel to receive captured packets
|
|
packetsChan := make(chan [][]byte, 1)
|
|
|
|
// Start goroutine to capture packets
|
|
go func() {
|
|
var packets [][]byte
|
|
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
for i := 0; i < 5; i++ { // Capture up to 5 packets
|
|
buf := make([]byte, 2048)
|
|
n, _, err := udpConn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
break
|
|
}
|
|
packets = append(packets, buf[:n])
|
|
}
|
|
packetsChan <- packets
|
|
}()
|
|
|
|
// Create QUIC client connection (will fail but we capture the initial packet)
|
|
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
|
require.NoError(t, err)
|
|
defer clientConn.Close()
|
|
|
|
tlsConfig := &tls.Config{
|
|
ServerName: testSNI,
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{"h3"},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
// This will fail (no server) but sends initial packet
|
|
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
|
|
|
// Wait for captured packets
|
|
select {
|
|
case packets := <-packetsChan:
|
|
t.Logf("Captured %d QUIC packets", len(packets))
|
|
|
|
for i, packet := range packets {
|
|
t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))])
|
|
}
|
|
|
|
// Test sniffer with first packet
|
|
if len(packets) > 0 {
|
|
var metadata adapter.InboundContext
|
|
err := sniff.QUICClientHello(context.Background(), &metadata, packets[0])
|
|
|
|
t.Logf("First packet sniff error: %v", err)
|
|
t.Logf("Protocol: %s", metadata.Protocol)
|
|
t.Logf("Domain: %s", metadata.Domain)
|
|
t.Logf("Client: %s", metadata.Client)
|
|
|
|
// If first packet needs more data, try with subsequent packets
|
|
// IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext
|
|
if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 {
|
|
t.Log("First packet needs more data, trying subsequent packets with shared context...")
|
|
for i := 1; i < len(packets); i++ {
|
|
// Reuse same metadata to accumulate fragments
|
|
err = sniff.QUICClientHello(context.Background(), &metadata, packets[i])
|
|
t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil)
|
|
if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Print hex dump for debugging
|
|
t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))]))
|
|
|
|
// Log final results
|
|
t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client)
|
|
|
|
// Verify SNI extraction
|
|
if metadata.Domain == "" {
|
|
t.Errorf("Failed to extract SNI, expected: %s", testSNI)
|
|
} else {
|
|
require.Equal(t, testSNI, metadata.Domain, "SNI should match")
|
|
}
|
|
|
|
// Check client identification - quic-go should be identified as quic-go, not chromium
|
|
t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client)
|
|
}
|
|
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Timeout waiting for QUIC packets")
|
|
}
|
|
}
|