mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-27 00:04:59 +03:00
39935ed9ef
Keep the TUIC stream fragmentation check on a working UDP loopback path, allocate the AmneziaWG fixture ports dynamically, and wait for a real Xray stats response before starting API mutation assertions. Recreate failed readiness channels so an early connection cannot stall the probe. Verified host and offline parallel Go suites, repeated occupied-port and UDP tests, race/shuffle runs, and golangci-lint. Production behavior and dependency versions are unchanged. Split out of #6877 so that pull request's squash carries only the feature.
790 lines
26 KiB
Go
790 lines
26 KiB
Go
package tuic
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
serverquic "github.com/apernet/quic-go"
|
|
"github.com/google/uuid"
|
|
"github.com/quic-go/quic-go"
|
|
)
|
|
|
|
func generateTestCert(t *testing.T) (certPEM, keyPEM []byte) {
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("failed to generate private key: %v", err)
|
|
}
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"Test TUIC Server"},
|
|
},
|
|
NotBefore: time.Now().Add(-1 * time.Hour),
|
|
NotAfter: time.Now().Add(24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
DNSNames: []string{"localhost"},
|
|
}
|
|
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
if err != nil {
|
|
t.Fatalf("failed to create certificate: %v", err)
|
|
}
|
|
|
|
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
|
|
privBytes, err := x509.MarshalECPrivateKey(priv)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal private key: %v", err)
|
|
}
|
|
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
|
|
|
|
return certPEM, keyPEM
|
|
}
|
|
|
|
func TestServerTCPConnectE2E(t *testing.T) {
|
|
for _, controller := range []string{"bbr", "cubic", "new_reno"} {
|
|
t.Run(controller, func(t *testing.T) {
|
|
testServerTCPConnectE2E(t, controller)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testServerTCPConnectE2E(t *testing.T, controller string) {
|
|
certPEM, keyPEM := generateTestCert(t)
|
|
|
|
// Start mock SOCKS5 server on loopback
|
|
socksAddr, socksCleanup := startMockSocks5Server(t, "alice@example.com", "mock-socks-pass")
|
|
defer socksCleanup()
|
|
|
|
testUUID := uuid.New()
|
|
testPassword := "secret-client-password"
|
|
|
|
inst := Instance{
|
|
Id: 1,
|
|
Tag: "tuic-test",
|
|
Listen: "127.0.0.1",
|
|
Port: 0,
|
|
Certificate: string(certPEM),
|
|
PrivateKey: string(keyPEM),
|
|
CongestionControl: controller,
|
|
ALPN: []string{"h3"},
|
|
MaxIdleTime: 5,
|
|
AuthenticationTimeout: 2,
|
|
Clients: []TuicClientSettings{
|
|
{
|
|
UUID: testUUID.String(),
|
|
Password: testPassword,
|
|
Email: "alice@example.com",
|
|
},
|
|
},
|
|
}
|
|
|
|
relay := &SocksRelay{
|
|
Addr: socksAddr,
|
|
Password: "mock-socks-pass",
|
|
}
|
|
|
|
server, err := NewServer(inst, relay)
|
|
if err != nil {
|
|
t.Fatalf("NewServer failed: %v", err)
|
|
}
|
|
|
|
if err := server.Start(); err != nil {
|
|
t.Fatalf("Server.Start failed: %v", err)
|
|
}
|
|
defer server.Close()
|
|
|
|
serverAddr := server.packetConn.LocalAddr().String()
|
|
|
|
// Connect client to TUIC server via QUIC
|
|
clientTLS := &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{"h3"},
|
|
}
|
|
quicConfig := &quic.Config{
|
|
EnableDatagrams: true,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
conn, err := quic.DialAddr(ctx, serverAddr, clientTLS, quicConfig)
|
|
if err != nil {
|
|
t.Fatalf("quic.DialAddr failed: %v", err)
|
|
}
|
|
defer conn.CloseWithError(0, "")
|
|
|
|
// 1. Authenticate client on a uni stream
|
|
tlsState := conn.ConnectionState().TLS
|
|
token, err := tlsState.ExportKeyingMaterial(string(testUUID[:]), []byte(testPassword), 32)
|
|
if err != nil {
|
|
t.Fatalf("ExportKeyingMaterial failed: %v", err)
|
|
}
|
|
|
|
uniStream, err := conn.OpenUniStreamSync(ctx)
|
|
if err != nil {
|
|
t.Fatalf("OpenUniStreamSync failed: %v", err)
|
|
}
|
|
// Send: [VER (0x05)][0x00][UUID (16)][TOKEN (32)]
|
|
authPayload := make([]byte, 2+16+32)
|
|
authPayload[0] = ProtocolVersion
|
|
authPayload[1] = CmdAuthenticate
|
|
copy(authPayload[2:18], testUUID[:])
|
|
copy(authPayload[18:50], token)
|
|
|
|
if _, err := uniStream.Write(authPayload); err != nil {
|
|
t.Fatalf("write auth payload failed: %v", err)
|
|
}
|
|
_ = uniStream.Close()
|
|
|
|
// 2. Open bidirectional stream for TCP Connect
|
|
biStream, err := conn.OpenStreamSync(ctx)
|
|
if err != nil {
|
|
t.Fatalf("OpenStreamSync failed: %v", err)
|
|
}
|
|
defer biStream.Close()
|
|
|
|
// Send: [VER (0x05)][0x01][ADDR]
|
|
target := &Address{
|
|
Type: AddrTypeIPv4,
|
|
IP: net.ParseIP("1.1.1.1"),
|
|
Port: 80,
|
|
}
|
|
var connectBuf bytes.Buffer
|
|
connectBuf.WriteByte(ProtocolVersion)
|
|
connectBuf.WriteByte(CmdConnect)
|
|
if err := WriteAddress(&connectBuf, target); err != nil {
|
|
t.Fatalf("WriteAddress failed: %v", err)
|
|
}
|
|
if _, err := biStream.Write(connectBuf.Bytes()); err != nil {
|
|
t.Fatalf("write connect cmd failed: %v", err)
|
|
}
|
|
|
|
// 3. Send test data and read echo response back through SOCKS5 bridge
|
|
testMsg := []byte("ping pong over native go tuic!")
|
|
if _, err := biStream.Write(testMsg); err != nil {
|
|
t.Fatalf("write test message failed: %v", err)
|
|
}
|
|
|
|
recvBuf := make([]byte, len(testMsg))
|
|
if _, err := io.ReadFull(biStream, recvBuf); err != nil {
|
|
t.Fatalf("read echo failed: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(recvBuf, testMsg) {
|
|
t.Fatalf("expected %q, got %q", testMsg, recvBuf)
|
|
}
|
|
|
|
// 4. Verify traffic was recorded for alice@example.com
|
|
activeEmails := server.GetActiveEmails(10 * time.Second)
|
|
if len(activeEmails) == 0 || activeEmails[0] != "alice@example.com" {
|
|
t.Fatalf("expected active email alice@example.com, got %v", activeEmails)
|
|
}
|
|
|
|
waitForClientTraffic(t, server, "alice@example.com", int64(len(testMsg)))
|
|
}
|
|
|
|
// closeUniStream tolerates only the server's STOP_SENDING: it cancels the read side of a
|
|
// uni stream once the command is parsed, which can land before the client's FIN.
|
|
func closeUniStream(t *testing.T, stream interface {
|
|
Close() error
|
|
Context() context.Context
|
|
},
|
|
) {
|
|
t.Helper()
|
|
err := stream.Close()
|
|
if err == nil {
|
|
return
|
|
}
|
|
cause := context.Cause(stream.Context())
|
|
var clientErr *quic.StreamError
|
|
var serverErr *serverquic.StreamError
|
|
if (errors.As(cause, &clientErr) && clientErr.Remote) || (errors.As(cause, &serverErr) && serverErr.Remote) {
|
|
return
|
|
}
|
|
t.Fatalf("close uni stream: %v (cause %v)", err, cause)
|
|
}
|
|
|
|
// waitForClientTraffic accumulates drained deltas because the up and down counters are
|
|
// bumped on different relay goroutines, so the echo can arrive before the upload is counted.
|
|
func waitForClientTraffic(t *testing.T, server *Server, email string, minBytes int64) {
|
|
t.Helper()
|
|
var up, down int64
|
|
deadline := time.Now().Add(4 * time.Second)
|
|
for {
|
|
for _, delta := range server.CollectClientTraffic() {
|
|
if delta.Email != email {
|
|
t.Fatalf("unexpected traffic delta for %q: %+v", delta.Email, delta)
|
|
}
|
|
up += delta.Up
|
|
down += delta.Down
|
|
}
|
|
if up >= minBytes && down >= minBytes {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("traffic for %s = up %d, down %d; want both >= %d", email, up, down, minBytes)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestServerUDPDatagramE2E(t *testing.T) {
|
|
for _, controller := range []string{"bbr", "cubic", "new_reno"} {
|
|
t.Run(controller, func(t *testing.T) {
|
|
testServerUDPDatagramE2E(t, controller)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testServerUDPDatagramE2E(t *testing.T, controller string) {
|
|
certPEM, keyPEM := generateTestCert(t)
|
|
|
|
socksAddr, socksCleanup := startMockSocks5Server(t, "bob@example.com", "mock-socks-pass")
|
|
defer socksCleanup()
|
|
|
|
testUUID := uuid.New()
|
|
testPassword := "secret-bob-password"
|
|
|
|
inst := Instance{
|
|
Id: 2,
|
|
Tag: "tuic-udp-test",
|
|
Listen: "127.0.0.1",
|
|
Port: 0,
|
|
Certificate: string(certPEM),
|
|
PrivateKey: string(keyPEM),
|
|
CongestionControl: controller,
|
|
ALPN: []string{"h3"},
|
|
MaxIdleTime: 5,
|
|
AuthenticationTimeout: 2,
|
|
Clients: []TuicClientSettings{
|
|
{
|
|
UUID: testUUID.String(),
|
|
Password: testPassword,
|
|
Email: "bob@example.com",
|
|
},
|
|
},
|
|
}
|
|
|
|
relay := &SocksRelay{
|
|
Addr: socksAddr,
|
|
Password: "mock-socks-pass",
|
|
}
|
|
|
|
server, err := NewServer(inst, relay)
|
|
if err != nil {
|
|
t.Fatalf("NewServer failed: %v", err)
|
|
}
|
|
|
|
if err := server.Start(); err != nil {
|
|
t.Fatalf("Server.Start failed: %v", err)
|
|
}
|
|
defer server.Close()
|
|
|
|
serverAddr := server.packetConn.LocalAddr().String()
|
|
|
|
clientTLS := &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{"h3"},
|
|
}
|
|
quicConfig := &quic.Config{
|
|
EnableDatagrams: true,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
conn, err := quic.DialAddr(ctx, serverAddr, clientTLS, quicConfig)
|
|
if err != nil {
|
|
t.Fatalf("quic.DialAddr failed: %v", err)
|
|
}
|
|
defer conn.CloseWithError(0, "")
|
|
|
|
// 1. Authenticate via uni stream
|
|
tlsState := conn.ConnectionState().TLS
|
|
token, err := tlsState.ExportKeyingMaterial(string(testUUID[:]), []byte(testPassword), 32)
|
|
if err != nil {
|
|
t.Fatalf("ExportKeyingMaterial failed: %v", err)
|
|
}
|
|
|
|
uniStream, err := conn.OpenUniStreamSync(ctx)
|
|
if err != nil {
|
|
t.Fatalf("OpenUniStreamSync failed: %v", err)
|
|
}
|
|
authPayload := make([]byte, 2+16+32)
|
|
authPayload[0] = ProtocolVersion
|
|
authPayload[1] = CmdAuthenticate
|
|
copy(authPayload[2:18], testUUID[:])
|
|
copy(authPayload[18:50], token)
|
|
if _, err := uniStream.Write(authPayload); err != nil {
|
|
t.Fatalf("write auth payload failed: %v", err)
|
|
}
|
|
_ = uniStream.Close()
|
|
|
|
// 2. Send UDP datagram
|
|
target := &Address{
|
|
Type: AddrTypeIPv4,
|
|
IP: net.ParseIP("8.8.8.8"),
|
|
Port: 53,
|
|
}
|
|
udpMsg := bytes.Repeat([]byte("d"), 1300)
|
|
|
|
// Give a tiny moment for auth to register
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
fragmentTotal := (len(udpMsg) + maxDatagramFragmentSize - 1) / maxDatagramFragmentSize
|
|
for i := 0; i < fragmentTotal; i++ {
|
|
start := i * maxDatagramFragmentSize
|
|
end := min(start+maxDatagramFragmentSize, len(udpMsg))
|
|
addr := (*Address)(nil)
|
|
if i == 0 {
|
|
addr = target
|
|
}
|
|
var frame bytes.Buffer
|
|
if err := WritePacket(&frame, 100, 1, uint8(fragmentTotal), uint8(i), addr, udpMsg[start:end]); err != nil {
|
|
t.Fatalf("WritePacket failed: %v", err)
|
|
}
|
|
if err := conn.SendDatagram(frame.Bytes()); err != nil {
|
|
t.Fatalf("SendDatagram failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// 3. Receive and reassemble the echo reply via datagrams.
|
|
replyReassembler := newPacketReassembler(1500)
|
|
var replyPayload []byte
|
|
for replyPayload == nil {
|
|
recvDgram, err := conn.ReceiveDatagram(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ReceiveDatagram failed: %v", err)
|
|
}
|
|
if len(recvDgram) < 2 || recvDgram[0] != ProtocolVersion || recvDgram[1] != CmdPacket {
|
|
t.Fatalf("unexpected datagram reply: %x", recvDgram)
|
|
}
|
|
pktReader := bytes.NewReader(recvDgram[2:])
|
|
hdr, err := ReadPacketHeader(pktReader)
|
|
if err != nil {
|
|
t.Fatalf("ReadPacketHeader failed: %v", err)
|
|
}
|
|
fragment, err := readPacketPayload(pktReader, hdr)
|
|
if err != nil || pktReader.Len() != 0 {
|
|
t.Fatalf("read reply payload failed: %v", err)
|
|
}
|
|
_, assembled, complete := replyReassembler.feed(packetTransportDatagram, hdr, fragment)
|
|
if complete {
|
|
replyPayload = assembled
|
|
}
|
|
}
|
|
|
|
if !bytes.Equal(replyPayload, udpMsg) {
|
|
t.Fatalf("expected %q, got %q", udpMsg, replyPayload)
|
|
}
|
|
|
|
// 4. Verify traffic
|
|
waitForClientTraffic(t, server, "bob@example.com", int64(len(udpMsg)))
|
|
}
|
|
|
|
func TestServerUDPStreamE2E(t *testing.T) {
|
|
for _, controller := range []string{"bbr", "cubic", "new_reno"} {
|
|
t.Run(controller, func(t *testing.T) {
|
|
testServerUDPStreamE2E(t, controller)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testServerUDPStreamE2E(t *testing.T, controller string) {
|
|
certPEM, keyPEM := generateTestCert(t)
|
|
socksAddr, socksCleanup := startMockSocks5ServerWithUDPLoopback(t, udpEchoLoopback(t), "stream@example.com", "mock-socks-pass")
|
|
defer socksCleanup()
|
|
|
|
testUUID := uuid.New()
|
|
testPassword := "secret-stream-password"
|
|
server, err := NewServer(Instance{
|
|
Id: 3,
|
|
Tag: "tuic-udp-stream-test",
|
|
Listen: "127.0.0.1",
|
|
Certificate: string(certPEM),
|
|
PrivateKey: string(keyPEM),
|
|
ALPN: []string{"h3"},
|
|
MaxIdleTime: 5,
|
|
AuthenticationTimeout: 2,
|
|
MaxUdpRelayPacketSize: maxUdpRelayPacketSize,
|
|
CongestionControl: controller,
|
|
Clients: []TuicClientSettings{{
|
|
UUID: testUUID.String(),
|
|
Password: testPassword,
|
|
Email: "stream@example.com",
|
|
}},
|
|
}, &SocksRelay{Addr: socksAddr, Password: "mock-socks-pass"})
|
|
if err != nil {
|
|
t.Fatalf("NewServer failed: %v", err)
|
|
}
|
|
if err := server.Start(); err != nil {
|
|
t.Fatalf("Server.Start failed: %v", err)
|
|
}
|
|
defer server.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
conn, err := quic.DialAddr(ctx, server.packetConn.LocalAddr().String(), &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{"h3"},
|
|
}, &quic.Config{EnableDatagrams: true})
|
|
if err != nil {
|
|
t.Fatalf("quic.DialAddr failed: %v", err)
|
|
}
|
|
defer conn.CloseWithError(0, "")
|
|
|
|
tlsState := conn.ConnectionState().TLS
|
|
token, err := tlsState.ExportKeyingMaterial(string(testUUID[:]), []byte(testPassword), 32)
|
|
if err != nil {
|
|
t.Fatalf("ExportKeyingMaterial failed: %v", err)
|
|
}
|
|
authStream, err := conn.OpenUniStreamSync(ctx)
|
|
if err != nil {
|
|
t.Fatalf("OpenUniStreamSync for authentication failed: %v", err)
|
|
}
|
|
authPayload := make([]byte, 2+16+32)
|
|
authPayload[0] = ProtocolVersion
|
|
authPayload[1] = CmdAuthenticate
|
|
copy(authPayload[2:18], testUUID[:])
|
|
copy(authPayload[18:], token)
|
|
if _, err := authStream.Write(authPayload); err != nil {
|
|
t.Fatalf("write authentication payload failed: %v", err)
|
|
}
|
|
closeUniStream(t, authStream)
|
|
|
|
target := &Address{Type: AddrTypeIPv4, IP: net.ParseIP("8.8.8.8"), Port: 53}
|
|
udpMsg := bytes.Repeat([]byte("s"), 8500)
|
|
fragmentTotal := (len(udpMsg) + maxStreamFragmentSize - 1) / maxStreamFragmentSize
|
|
for i := 0; i < fragmentTotal; i++ {
|
|
start := i * maxStreamFragmentSize
|
|
end := min(start+maxStreamFragmentSize, len(udpMsg))
|
|
addr := (*Address)(nil)
|
|
if i == 0 {
|
|
addr = target
|
|
}
|
|
var frame bytes.Buffer
|
|
if err := WritePacket(&frame, 300, 1, uint8(fragmentTotal), uint8(i), addr, udpMsg[start:end]); err != nil {
|
|
t.Fatalf("WritePacket failed: %v", err)
|
|
}
|
|
packetStream, err := conn.OpenUniStreamSync(ctx)
|
|
if err != nil {
|
|
t.Fatalf("OpenUniStreamSync for packet failed: %v", err)
|
|
}
|
|
if _, err := packetStream.Write(frame.Bytes()); err != nil {
|
|
t.Fatalf("write packet frame failed: %v", err)
|
|
}
|
|
closeUniStream(t, packetStream)
|
|
}
|
|
|
|
replyReassembler := newPacketReassembler(maxUdpRelayPacketSize)
|
|
var reply []byte
|
|
for reply == nil {
|
|
responseStream, err := conn.AcceptUniStream(ctx)
|
|
if err != nil {
|
|
t.Fatalf("AcceptUniStream for response failed: %v", err)
|
|
}
|
|
_, command, err := ReadCommand(responseStream)
|
|
if err != nil {
|
|
t.Fatalf("read response command: %v", err)
|
|
}
|
|
if command != CmdPacket {
|
|
t.Fatalf("response command = %d, want %d", command, CmdPacket)
|
|
}
|
|
hdr, err := ReadPacketHeader(responseStream)
|
|
if err != nil {
|
|
t.Fatalf("ReadPacketHeader failed: %v", err)
|
|
}
|
|
fragment, err := readPacketPayload(responseStream, hdr)
|
|
if err != nil {
|
|
t.Fatalf("read response payload failed: %v", err)
|
|
}
|
|
_, assembled, complete := replyReassembler.feed(packetTransportStream, hdr, fragment)
|
|
if complete {
|
|
reply = assembled
|
|
}
|
|
}
|
|
if !bytes.Equal(reply, udpMsg) {
|
|
t.Fatalf("stream response size = %d, want %d", len(reply), len(udpMsg))
|
|
}
|
|
}
|
|
|
|
func TestNewServerRejectsOversizedMaxUdpRelayPacketSize(t *testing.T) {
|
|
certPEM, keyPEM := generateTestCert(t)
|
|
_, err := NewServer(Instance{
|
|
Listen: "127.0.0.1",
|
|
Port: 8443,
|
|
Certificate: string(certPEM),
|
|
PrivateKey: string(keyPEM),
|
|
MaxUdpRelayPacketSize: maxLegacyUdpRelayPacketSize + 1,
|
|
}, &SocksRelay{Addr: "127.0.0.1:1"})
|
|
if err == nil {
|
|
t.Fatal("expected oversized max UDP relay packet size to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestNewServerClampsLegacyUdpPayloadLimit(t *testing.T) {
|
|
certPEM, keyPEM := generateTestCert(t)
|
|
server, err := NewServer(Instance{
|
|
Listen: "127.0.0.1",
|
|
Port: 0,
|
|
Certificate: string(certPEM),
|
|
PrivateKey: string(keyPEM),
|
|
MaxUdpRelayPacketSize: maxLegacyUdpRelayPacketSize,
|
|
}, &SocksRelay{})
|
|
if err != nil {
|
|
t.Fatalf("NewServer: %v", err)
|
|
}
|
|
if server.maxUdpRelayPacketSize != maxSafeUdpRelayPacketSize {
|
|
t.Fatalf("legacy UDP limit = %d, want clamped limit %d", server.maxUdpRelayPacketSize, maxSafeUdpRelayPacketSize)
|
|
}
|
|
}
|
|
|
|
func TestPacketReassemblerInvalidatesAssemblyWhenFragmentTotalChanges(t *testing.T) {
|
|
reassembler := newPacketReassembler(64)
|
|
first := &PacketHeader{AssocID: 7, PktID: 9, FragTotal: 2, FragID: 0, Addr: &Address{Type: AddrTypeIPv4, IP: net.ParseIP("127.0.0.1"), Port: 53}, Size: 1}
|
|
if _, _, complete := reassembler.feed(packetTransportDatagram, first, []byte("A")); complete {
|
|
t.Fatal("first fragment unexpectedly completed")
|
|
}
|
|
single := &PacketHeader{AssocID: 7, PktID: 9, FragTotal: 1, FragID: 0, Addr: first.Addr, Size: 1}
|
|
if _, got, complete := reassembler.feed(packetTransportDatagram, single, []byte("Z")); !complete || string(got) != "Z" {
|
|
t.Fatalf("single packet = %q, complete=%v; want Z", got, complete)
|
|
}
|
|
last := &PacketHeader{AssocID: 7, PktID: 9, FragTotal: 2, FragID: 1, Size: 1}
|
|
if _, _, complete := reassembler.feed(packetTransportDatagram, last, []byte("B")); complete {
|
|
t.Fatal("stale first fragment was combined with a later packet")
|
|
}
|
|
}
|
|
|
|
func TestUdpAssociationPinsFirstPacketModeAndDissociateClearsFragments(t *testing.T) {
|
|
registry := newUdpAssociationRegistry(64)
|
|
addr := &Address{Type: AddrTypeIPv4, IP: net.ParseIP("127.0.0.1"), Port: 53}
|
|
first := &PacketHeader{AssocID: 3, PktID: 1, FragTotal: 2, FragID: 0, Addr: addr, Size: 1}
|
|
association, _, _, complete := registry.feed(packetTransportDatagram, first, []byte("A"))
|
|
if association == nil || complete {
|
|
t.Fatal("expected first native fragment to establish an incomplete association")
|
|
}
|
|
singleStream := &PacketHeader{AssocID: 3, PktID: 2, FragTotal: 1, FragID: 0, Addr: addr, Size: 1}
|
|
association, _, _, complete = registry.feed(packetTransportStream, singleStream, []byte("S"))
|
|
if association.responseTransport != packetTransportDatagram || !complete {
|
|
t.Fatalf("mixed-mode packet changed response mode: association=%+v complete=%v", association, complete)
|
|
}
|
|
|
|
if !registry.dissociate(3) {
|
|
t.Fatal("expected dissociate to remove association")
|
|
}
|
|
late := &PacketHeader{AssocID: 3, PktID: 1, FragTotal: 2, FragID: 1, Size: 1}
|
|
_, _, _, complete = registry.feed(packetTransportDatagram, late, []byte("B"))
|
|
if complete {
|
|
t.Fatal("late fragment completed an assembly from before dissociate")
|
|
}
|
|
}
|
|
|
|
func TestPacketReassembler(t *testing.T) {
|
|
pr := newPacketReassembler(1500)
|
|
targetAddr := &Address{Type: AddrTypeIPv4, IP: net.ParseIP("1.1.1.1"), Port: 53}
|
|
|
|
// 1. Unfragmented packet
|
|
hdrSingle := &PacketHeader{
|
|
AssocID: 1,
|
|
PktID: 1,
|
|
FragTotal: 1,
|
|
FragID: 0,
|
|
Size: uint16(len("hello single")),
|
|
Addr: targetAddr,
|
|
}
|
|
addr, payload, complete := pr.feed(packetTransportDatagram, hdrSingle, []byte("hello single"))
|
|
if addr == nil || !complete || string(payload) != "hello single" {
|
|
t.Fatalf("unexpected single packet result: %v, %s", addr, payload)
|
|
}
|
|
|
|
// 2. In-order fragments (3 parts)
|
|
hdr0 := &PacketHeader{AssocID: 2, PktID: 10, FragTotal: 3, FragID: 0, Size: 6, Addr: targetAddr}
|
|
hdr1 := &PacketHeader{AssocID: 2, PktID: 10, FragTotal: 3, FragID: 1, Size: 6, Addr: &Address{Type: AddrTypeNone}}
|
|
hdr2 := &PacketHeader{AssocID: 2, PktID: 10, FragTotal: 3, FragID: 2, Size: 5, Addr: &Address{Type: AddrTypeNone}}
|
|
|
|
_, p0, complete := pr.feed(packetTransportDatagram, hdr0, []byte("part0-"))
|
|
if p0 != nil || complete {
|
|
t.Fatalf("expected nil before all fragments arrive, got %s", p0)
|
|
}
|
|
_, p1, complete := pr.feed(packetTransportDatagram, hdr1, []byte("part1-"))
|
|
if p1 != nil || complete {
|
|
t.Fatalf("expected nil before all fragments arrive, got %s", p1)
|
|
}
|
|
a2, p2, complete := pr.feed(packetTransportDatagram, hdr2, []byte("part2"))
|
|
if a2 == nil || !complete || string(p2) != "part0-part1-part2" {
|
|
t.Fatalf("expected reassembled payload 'part0-part1-part2', got %v, %s", a2, p2)
|
|
}
|
|
|
|
// 3. Out-of-order fragments (parts 1, 2, 0)
|
|
hdrOO0 := &PacketHeader{AssocID: 3, PktID: 20, FragTotal: 3, FragID: 0, Size: 6, Addr: targetAddr}
|
|
hdrOO1 := &PacketHeader{AssocID: 3, PktID: 20, FragTotal: 3, FragID: 1, Size: 7, Addr: &Address{Type: AddrTypeNone}}
|
|
hdrOO2 := &PacketHeader{AssocID: 3, PktID: 20, FragTotal: 3, FragID: 2, Size: 3, Addr: &Address{Type: AddrTypeNone}}
|
|
|
|
if _, p, done := pr.feed(packetTransportDatagram, hdrOO1, []byte("MIDDLE-")); p != nil || done {
|
|
t.Fatalf("expected nil, got %s", p)
|
|
}
|
|
if _, p, done := pr.feed(packetTransportDatagram, hdrOO2, []byte("END")); p != nil || done {
|
|
t.Fatalf("expected nil, got %s", p)
|
|
}
|
|
aOO, pOO, done := pr.feed(packetTransportDatagram, hdrOO0, []byte("START-"))
|
|
if aOO == nil || !done || string(pOO) != "START-MIDDLE-END" {
|
|
t.Fatalf("expected 'START-MIDDLE-END', got %s", pOO)
|
|
}
|
|
|
|
// 4. Invalid FragID >= FragTotal
|
|
hdrInv := &PacketHeader{AssocID: 4, PktID: 30, FragTotal: 2, FragID: 2, Size: 7, Addr: targetAddr}
|
|
if _, p, done := pr.feed(packetTransportDatagram, hdrInv, []byte("invalid")); p != nil || done {
|
|
t.Fatalf("expected nil for invalid FragID, got %s", p)
|
|
}
|
|
|
|
// A changed fragment total for an in-flight packet must discard the packet safely.
|
|
hdrMixed0 := &PacketHeader{AssocID: 5, PktID: 40, FragTotal: 2, FragID: 0, Size: 1, Addr: targetAddr}
|
|
hdrMixed3 := &PacketHeader{AssocID: 5, PktID: 40, FragTotal: 4, FragID: 3, Size: 1, Addr: &Address{Type: AddrTypeNone}}
|
|
if _, _, done := pr.feed(packetTransportDatagram, hdrMixed0, []byte("a")); done {
|
|
t.Fatal("expected first mixed-total fragment to remain incomplete")
|
|
}
|
|
if _, _, done := pr.feed(packetTransportDatagram, hdrMixed3, []byte("b")); done {
|
|
t.Fatal("expected inconsistent fragment total to be discarded")
|
|
}
|
|
if _, ok := pr.packets[packetFragmentKey{assocID: 5, pktID: 40, transport: packetTransportDatagram}]; ok {
|
|
t.Fatal("inconsistent packet assembly was not discarded")
|
|
}
|
|
|
|
// Fragments from different transports cannot be combined into one packet.
|
|
streamFirst := &PacketHeader{AssocID: 6, PktID: 50, FragTotal: 2, FragID: 0, Size: 1, Addr: targetAddr}
|
|
datagramLast := &PacketHeader{AssocID: 6, PktID: 50, FragTotal: 2, FragID: 1, Size: 1, Addr: &Address{Type: AddrTypeNone}}
|
|
if _, _, done := pr.feed(packetTransportStream, streamFirst, []byte("a")); done {
|
|
t.Fatal("expected first stream fragment to remain incomplete")
|
|
}
|
|
if _, _, done := pr.feed(packetTransportDatagram, datagramLast, []byte("b")); done {
|
|
t.Fatal("fragments from different transports must not combine")
|
|
}
|
|
if _, _, done := pr.feed(packetTransportStream, datagramLast, []byte("b")); !done {
|
|
t.Fatal("expected stream fragments to reassemble")
|
|
}
|
|
|
|
// The configured size limit caps both complete packets and reassembly state.
|
|
limited := newPacketReassembler(3)
|
|
tooLarge0 := &PacketHeader{AssocID: 7, PktID: 60, FragTotal: 2, FragID: 0, Size: 2, Addr: targetAddr}
|
|
tooLarge1 := &PacketHeader{AssocID: 7, PktID: 60, FragTotal: 2, FragID: 1, Size: 2, Addr: &Address{Type: AddrTypeNone}}
|
|
if _, _, done := limited.feed(packetTransportDatagram, tooLarge0, []byte("ab")); done {
|
|
t.Fatal("expected first oversized packet fragment to remain incomplete")
|
|
}
|
|
if _, _, done := limited.feed(packetTransportDatagram, tooLarge1, []byte("cd")); done {
|
|
t.Fatal("oversized reassembled packet must be rejected")
|
|
}
|
|
if len(limited.packets) != 0 {
|
|
t.Fatal("oversized reassembly state was not discarded")
|
|
}
|
|
|
|
bounded := newPacketReassembler(1500)
|
|
for i := 0; i < maxPendingPacketAssemblies; i++ {
|
|
hdr := &PacketHeader{
|
|
AssocID: 8,
|
|
PktID: uint16(i),
|
|
FragTotal: 2,
|
|
FragID: 0,
|
|
Size: 1,
|
|
Addr: targetAddr,
|
|
}
|
|
if _, _, done := bounded.feed(packetTransportDatagram, hdr, []byte("a")); done {
|
|
t.Fatal("expected pending fragment to remain incomplete")
|
|
}
|
|
}
|
|
if len(bounded.packets) != maxPendingPacketAssemblies {
|
|
t.Fatalf("pending assembly count = %d, want %d", len(bounded.packets), maxPendingPacketAssemblies)
|
|
}
|
|
extra := &PacketHeader{AssocID: 8, PktID: 100, FragTotal: 2, FragID: 0, Size: 1, Addr: targetAddr}
|
|
if _, _, done := bounded.feed(packetTransportDatagram, extra, []byte("a")); done {
|
|
t.Fatal("expected new assembly to be rejected when the pending limit is reached")
|
|
}
|
|
if len(bounded.packets) != maxPendingPacketAssemblies {
|
|
t.Fatalf("pending assembly count after overflow = %d, want %d", len(bounded.packets), maxPendingPacketAssemblies)
|
|
}
|
|
|
|
for _, packet := range bounded.packets {
|
|
packet.updatedAt = time.Now().Add(-packetAssemblyTimeout - time.Second)
|
|
}
|
|
if _, _, done := bounded.feed(packetTransportDatagram, extra, []byte("a")); done {
|
|
t.Fatal("expected new fragment to remain incomplete after stale entries are evicted")
|
|
}
|
|
if len(bounded.packets) != 1 {
|
|
t.Fatalf("pending assembly count after stale cleanup = %d, want 1", len(bounded.packets))
|
|
}
|
|
}
|
|
|
|
func TestAuthenticationTimeoutClosesUnauthenticatedConnections(t *testing.T) {
|
|
for _, partial := range []bool{false, true} {
|
|
name := "no-authenticate"
|
|
if partial {
|
|
name = "partial-authenticate"
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
certPEM, keyPEM := generateTestCert(t)
|
|
server, err := NewServer(Instance{
|
|
Id: 99010,
|
|
Tag: "auth-timeout-test",
|
|
Listen: "127.0.0.1",
|
|
Port: 0,
|
|
Certificate: string(certPEM),
|
|
PrivateKey: string(keyPEM),
|
|
ALPN: []string{"h3"},
|
|
MaxIdleTime: 5,
|
|
AuthenticationTimeout: 1,
|
|
}, &SocksRelay{})
|
|
if err != nil {
|
|
t.Fatalf("NewServer: %v", err)
|
|
}
|
|
if err := server.Start(); err != nil {
|
|
t.Fatalf("Server.Start: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = server.Close() })
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
|
defer cancel()
|
|
conn, err := quic.DialAddr(ctx, server.packetConn.LocalAddr().String(), &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{"h3"},
|
|
}, &quic.Config{EnableDatagrams: true, KeepAlivePeriod: time.Second})
|
|
if err != nil {
|
|
t.Fatalf("quic.DialAddr: %v", err)
|
|
}
|
|
defer conn.CloseWithError(0, "")
|
|
|
|
if partial {
|
|
stream, err := conn.OpenUniStreamSync(ctx)
|
|
if err != nil {
|
|
t.Fatalf("OpenUniStreamSync: %v", err)
|
|
}
|
|
if _, err := stream.Write([]byte{ProtocolVersion, CmdAuthenticate, 1}); err != nil {
|
|
t.Fatalf("write partial Authenticate: %v", err)
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-conn.Context().Done():
|
|
case <-ctx.Done():
|
|
t.Fatalf("server left unauthenticated QUIC connection open: %v", ctx.Err())
|
|
}
|
|
})
|
|
}
|
|
}
|