package checker import ( "context" "encoding/binary" "net" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // fakeUDPRelay listens on a UDP socket and echoes SOCKS5-wrapped STUN // binding requests as a synthetic Binding Success Response, just like // fakeProxy.runRelay in checker_test.go but standalone (no SOCKS5 TCP // control channel needed). dropEveryN > 0 drops every Nth packet. type fakeUDPRelay struct { conn *net.UDPConn addr *net.UDPAddr dropEveryN atomic.Int32 count atomic.Int32 } func newFakeUDPRelay(t *testing.T) *fakeUDPRelay { t.Helper() pc, err := net.ListenPacket("udp", "127.0.0.1:0") require.NoError(t, err) uconn := pc.(*net.UDPConn) r := &fakeUDPRelay{ conn: uconn, addr: uconn.LocalAddr().(*net.UDPAddr), } t.Cleanup(func() { _ = uconn.Close() }) go r.serve() return r } func (r *fakeUDPRelay) serve() { buf := make([]byte, 2048) for { n, src, err := r.conn.ReadFromUDP(buf) if err != nil { return } if dropN := r.dropEveryN.Load(); dropN > 0 { c := r.count.Add(1) if c%dropN == 0 { continue } } else { r.count.Add(1) } if n < 10 { continue } if buf[0] != 0x00 || buf[1] != 0x00 || buf[2] != 0x00 { continue } var hdrLen int switch buf[3] { case 0x01: hdrLen = 10 case 0x04: hdrLen = 22 case 0x03: if n < 5 { continue } hdrLen = 4 + 1 + int(buf[4]) + 2 default: continue } if n < hdrLen+20 { continue } stunReq := buf[hdrLen:n] var txID [12]byte copy(txID[:], stunReq[8:20]) ip4 := src.IP.To4() if ip4 == nil { continue } xport := uint16(src.Port) ^ uint16(stunMagicCookie>>16) xaddr := binary.BigEndian.Uint32(ip4) ^ stunMagicCookie stunResp := make([]byte, 20+12) binary.BigEndian.PutUint16(stunResp[0:2], stunBindingSuccessResponse) binary.BigEndian.PutUint16(stunResp[2:4], 12) binary.BigEndian.PutUint32(stunResp[4:8], stunMagicCookie) copy(stunResp[8:20], txID[:]) binary.BigEndian.PutUint16(stunResp[20:22], stunAttrXORMappedAddress) binary.BigEndian.PutUint16(stunResp[22:24], 8) stunResp[24] = 0 stunResp[25] = 0x01 binary.BigEndian.PutUint16(stunResp[26:28], xport) binary.BigEndian.PutUint32(stunResp[28:32], xaddr) out := make([]byte, 0, 10+len(stunResp)) out = append(out, 0x00, 0x00, 0x00, 0x01) out = append(out, ip4...) var portBuf [2]byte binary.BigEndian.PutUint16(portBuf[:], uint16(src.Port)) out = append(out, portBuf[:]...) out = append(out, stunResp...) _, _ = r.conn.WriteToUDP(out, src) } } // TestVoiceQualityBurst_Math: full 30-of-30 reception on localhost, all // RTTs in single-digit milliseconds. func TestVoiceQualityBurst_Math(t *testing.T) { relay := newFakeUDPRelay(t) clientPC, err := net.ListenPacket("udp", "127.0.0.1:0") require.NoError(t, err) defer clientPC.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() res, err := runVoiceQualityBurst(ctx, clientPC, relay.addr, "localhost", 19302, 30, 5*time.Millisecond) require.NoError(t, err) assert.Equal(t, 30, res.Sent) assert.Equal(t, 30, res.Received) assert.InDelta(t, 0.0, res.LossPct, 0.001) assert.Less(t, res.P50RTTMS, 50.0, "loopback p50 should be tiny") } // TestVoiceQualityBurst_HalfLoss verifies the loss-percentage math when // the relay drops half the packets. func TestVoiceQualityBurst_HalfLoss(t *testing.T) { relay := newFakeUDPRelay(t) relay.dropEveryN.Store(2) // every other packet → 50% loss clientPC, err := net.ListenPacket("udp", "127.0.0.1:0") require.NoError(t, err) defer clientPC.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() res, err := runVoiceQualityBurst(ctx, clientPC, relay.addr, "localhost", 19302, 20, 3*time.Millisecond) require.NoError(t, err) assert.Equal(t, 20, res.Sent) assert.InDelta(t, 50.0, res.LossPct, 5.0, "expected ~50%% loss got %+v", res) } // TestVoiceQualityBurst_AllDropped: dropEveryN=1 → 100% loss. Should NOT // return an error; should report Sent=N, Received=0. func TestVoiceQualityBurst_AllDropped(t *testing.T) { relay := newFakeUDPRelay(t) relay.dropEveryN.Store(1) clientPC, err := net.ListenPacket("udp", "127.0.0.1:0") require.NoError(t, err) defer clientPC.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() res, err := runVoiceQualityBurst(ctx, clientPC, relay.addr, "localhost", 19302, 10, 3*time.Millisecond) require.NoError(t, err) assert.Equal(t, 10, res.Sent) assert.Equal(t, 0, res.Received) assert.InDelta(t, 100.0, res.LossPct, 0.001) assert.Equal(t, 0.0, res.P50RTTMS) assert.Equal(t, 0.0, res.JitterMS) } // TestVoiceQualityBurst_ZeroCount: count=0 → error (defensive). func TestVoiceQualityBurst_ZeroCount(t *testing.T) { relay := newFakeUDPRelay(t) clientPC, err := net.ListenPacket("udp", "127.0.0.1:0") require.NoError(t, err) defer clientPC.Close() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() _, err = runVoiceQualityBurst(ctx, clientPC, relay.addr, "localhost", 19302, 0, 5*time.Millisecond) assert.Error(t, err) } // TestVoiceServerProbe_HappyAndBlocked: against a fake SOCKS5 server, one // hostname (localhost) succeeds (REP=00) and another (127.0.0.1) gets // blocked (REP=05). Both resolve at the OS level, so Resolved should be // {localhost, 127.0.0.1}; Reachable should be {localhost}; Unreachable // should be {127.0.0.1}. func TestVoiceServerProbe_HappyAndBlocked(t *testing.T) { // Stand up a tiny fake SOCKS5 server that completes greet+CONNECT // only for hostname "localhost"; CONNECT to "127.0.0.1" is refused // with REP=05. We don't need full RFC 1928 — just enough to drive // the probe path. ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) defer ln.Close() go func() { for { c, err := ln.Accept() if err != nil { return } go func(c net.Conn) { defer c.Close() _ = c.SetDeadline(time.Now().Add(2 * time.Second)) // Read greeting: VER NMETHODS METHODS... hdr := make([]byte, 2) if _, err := readFull(c, hdr); err != nil { return } if hdr[0] != 0x05 { return } methods := make([]byte, hdr[1]) if _, err := readFull(c, methods); err != nil { return } // Reply: no-auth. _, _ = c.Write([]byte{0x05, 0x00}) // Read CONNECT request: VER CMD RSV ATYP... h2 := make([]byte, 4) if _, err := readFull(c, h2); err != nil { return } var host string switch h2[3] { case 0x01: ip := make([]byte, 4) _, _ = readFull(c, ip) host = net.IP(ip).String() case 0x03: l := make([]byte, 1) _, _ = readFull(c, l) name := make([]byte, int(l[0])) _, _ = readFull(c, name) host = string(name) } port := make([]byte, 2) _, _ = readFull(c, port) // Reply REP=00 only for hostname "localhost"; refuse otherwise. if host == "localhost" { _, _ = c.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) } else { _, _ = c.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) } }(c) } }() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() res, err := runVoiceServerProbe(ctx, []string{"localhost", "127.0.0.1"}, ln.Addr().String(), false, "", "", 1*time.Second) require.NoError(t, err) assert.ElementsMatch(t, []string{"localhost", "127.0.0.1"}, res.Resolved) assert.Equal(t, []string{"localhost"}, res.Reachable) assert.Equal(t, []string{"127.0.0.1"}, res.UnreachableButResolved) assert.Empty(t, res.Unresolved) } // TestVoiceServerProbe_EmptyHostnameList: zero-length input → zero-length // Resolved/Reachable, no error. func TestVoiceServerProbe_EmptyHostnameList(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() res, err := runVoiceServerProbe(ctx, []string{}, "127.0.0.1:1", false, "", "", 100*time.Millisecond) require.NoError(t, err) assert.Empty(t, res.Resolved) assert.Empty(t, res.Reachable) assert.Empty(t, res.Unresolved) } // readFull is a tiny helper to avoid importing io just for this. func readFull(c net.Conn, buf []byte) (int, error) { got := 0 for got < len(buf) { n, err := c.Read(buf[got:]) got += n if err != nil { return got, err } } return got, nil }