internal/checker: voice-quality + voice-srv tests for predictive voice diagnosis
Build / test (push) Has been cancelled
Build / build-windows (push) Has been cancelled

Replaces the single-packet `stun` test with two predictive voice tests:

  - voice-quality: 30-packet STUN burst through the SOCKS5 UDP relay.
    Computes loss%, jitter (RFC-3550-ish mean abs of inter-arrival
    delta), p50/p95 RTT. Three-tier gating: pass (loss≤5%, jitter≤30,
    p50≤250), warn (loss≤15%, jitter≤60, p50≤400 — voice glitches but
    works), fail (anything worse, including 100% loss).

  - voice-srv: parallel-DNS the 16-region <region>.discord.media
    hostnames, then SOCKS5 CONNECT to :443 on each through the proxy.
    Catches the very common Russian-DPI failure mode where the proxy
    passes generic Discord.com TCP but blocks the .discord.media voice
    CIDRs — a regression all 5 prior SOCKS5 sanity checks miss.

New StatusWarn = "warn" — soft pass with Hint kept visible. Counted as
passed in summary but flagged in UI.

Config gains VoiceBurstCount (default 30), VoiceBurstInterval (default
20ms), VoiceServerHostnames (default = built-in 16-region list).

Tests cover happy path, warn-tier (10% drop), fail-tier (100% drop),
voice-srv blocked, plus standalone unit tests on
runVoiceQualityBurst and runVoiceServerProbe with a fake UDP relay
and fake SOCKS5 server. Race + cover stays at 82.4%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 18:42:12 +03:00
parent ea4202d4a3
commit 0a85979142
6 changed files with 1264 additions and 137 deletions
+153 -19
View File
@@ -35,6 +35,12 @@ type fakeProxy struct {
udpRelayAddr *net.UDPAddr // announced in UDP ASSOCIATE reply
// udpDropEveryN, when > 0, drops every Nth packet through the relay
// (counted across the whole listener lifetime). N=2 → 50% loss; N=10
// → 10%; N=1 → 100% loss; 0 → no drops.
udpDropEveryN atomic.Int32
udpRelayCount atomic.Int32
// API-passthrough hook: when a CONNECT targets this host:port,
// the proxy dials apiTargetAddr and splices the conns instead of
// sending a fake REP=00 + close.
@@ -42,6 +48,11 @@ type fakeProxy struct {
apiTargetPort uint16
apiTargetAddr string
// blockVoiceCONNECT, when true, makes any CONNECT to a hostname
// not equal to apiTargetHost return REP=05. Used by voice-srv
// negative scenarios.
blockVoiceCONNECT atomic.Bool
// timeoutFirstAttempt stalls the first connection on greet to
// drive a timeout. Subsequent connections behave normally.
timeoutFirstAttempt atomic.Int32
@@ -84,7 +95,8 @@ func newFakeProxy(t *testing.T, scenario string) *fakeProxy {
func needsUDPRelay(scenario string) bool {
switch scenario {
case "happy_no_auth", "happy_with_auth", "udp_unsupported", "connect_refused", "timeout_then_ok":
case "happy_no_auth", "happy_with_auth", "udp_unsupported", "connect_refused", "timeout_then_ok",
"voice_quality_warn", "voice_quality_fail", "voice_srv_blocked":
return true
default:
return false
@@ -184,6 +196,23 @@ func (fp *fakeProxy) handle(conn net.Conn) {
switch cmdReq.cmd {
case 0x01: // CONNECT
// voice-srv block: refuse CONNECT to anything that isn't the
// gateway/api passthrough target. Only checked when the test
// explicitly sets blockVoiceCONNECT — keeps gateway+api happy.
if fp.blockVoiceCONNECT.Load() {
isAPITarget := fp.apiTargetHost != "" && cmdReq.host == fp.apiTargetHost && cmdReq.port == fp.apiTargetPort
isGatewayTarget := cmdReq.port == 443 && (cmdReq.host == "127.0.0.1" || cmdReq.host == "localhost") && fp.apiTargetHost == ""
_ = isGatewayTarget
if !isAPITarget {
// Refuse only :443 voice probes; allow gateway probe
// (which has its own configured port from stubGatewayAddr,
// not 443). Logic: target port == 443 → voice probe → refuse.
if cmdReq.port == 443 {
_, _ = conn.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
}
}
switch fp.scenario {
case "connect_refused":
_, _ = conn.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
@@ -255,6 +284,16 @@ func (fp *fakeProxy) runRelay(uconn *net.UDPConn) {
if err != nil {
return
}
// Optional packet-drop simulation. udpDropEveryN of value 1 drops
// everything; 2 drops every other packet; 10 drops 10%.
if dropN := fp.udpDropEveryN.Load(); dropN > 0 {
c := fp.udpRelayCount.Add(1)
if c%dropN == 0 {
continue
}
} else {
fp.udpRelayCount.Add(1)
}
if n < 10 {
continue
}
@@ -478,15 +517,25 @@ func hostPort(addr string) (string, int) {
// proxyConfig builds a Config pointed at the given fakeProxy with sane
// short timeouts for tests.
//
// Note on voice-srv: the default Config.VoiceServerHostnames would hit
// real Discord DNS during go test — we don't want that. Override with a
// single local hostname ("localhost") so DNS always resolves to 127.0.0.1
// and the SOCKS5 CONNECT goes back to the fake proxy itself, which
// returns REP=00 on a happy CONNECT or REP=05 when blockVoiceCONNECT is
// flipped.
func proxyConfig(fp *fakeProxy, useAuth bool) Config {
host, port := hostPort(fp.addr)
cfg := Config{
ProxyHost: host,
ProxyPort: port,
UseAuth: useAuth,
PerTestTimeout: 500 * time.Millisecond,
MaxRetries: 1,
RetryBackoff: 30 * time.Millisecond,
ProxyHost: host,
ProxyPort: port,
UseAuth: useAuth,
PerTestTimeout: 500 * time.Millisecond,
MaxRetries: 1,
RetryBackoff: 30 * time.Millisecond,
VoiceBurstCount: 10,
VoiceBurstInterval: 5 * time.Millisecond,
VoiceServerHostnames: []string{"localhost"},
}
if useAuth {
cfg.ProxyLogin = "u"
@@ -560,7 +609,7 @@ func TestRun_HappyNoAuth(t *testing.T) {
ch := Run(context.Background(), cfg)
results := drainResults(t, ch, 10*time.Second)
expected := []string{"tcp", "greet", "connect", "udp", "stun", "api"}
expected := []string{"tcp", "greet", "connect", "udp", "voice-quality", "voice-srv", "api"}
finals := map[string]Result{}
for _, id := range expected {
r, ok := finalByID(results, id)
@@ -579,6 +628,8 @@ func TestRun_HappyNoAuth(t *testing.T) {
// Metrics format spot-checks.
assert.Contains(t, finals["greet"].Metric, "no auth")
assert.Equal(t, "REP=00", finals["connect"].Metric)
assert.Contains(t, finals["voice-quality"].Metric, "loss=")
assert.Contains(t, finals["voice-srv"].Metric, "1/1 regions")
assert.Equal(t, "HTTP 200", finals["api"].Metric)
}
@@ -592,7 +643,7 @@ func TestRun_HappyWithAuth(t *testing.T) {
ch := Run(context.Background(), cfg)
results := drainResults(t, ch, 10*time.Second)
expected := []string{"tcp", "greet", "auth", "connect", "udp", "stun", "api"}
expected := []string{"tcp", "greet", "auth", "connect", "udp", "voice-quality", "voice-srv", "api"}
for _, id := range expected {
r, ok := finalByID(results, id)
require.True(t, ok, "missing %s; results=%+v", id, results)
@@ -623,7 +674,7 @@ func TestRun_AuthRejected(t *testing.T) {
assert.Equal(t, StatusFailed, rA.Status)
assert.NotEmpty(t, rA.Hint)
for _, id := range []string{"connect", "udp", "stun", "api"} {
for _, id := range []string{"connect", "udp", "voice-quality", "voice-srv", "api"} {
r, ok := finalByID(results, id)
require.True(t, ok, "missing %s", id)
assert.Equal(t, StatusSkipped, r.Status, "id=%s", id)
@@ -648,7 +699,7 @@ func TestRun_AllMethodsRejected(t *testing.T) {
assert.Equal(t, StatusFailed, rG.Status)
assert.NotEmpty(t, rG.Hint)
for _, id := range []string{"connect", "udp", "stun", "api"} {
for _, id := range []string{"connect", "udp", "voice-quality", "voice-srv", "api"} {
r, ok := finalByID(results, id)
require.True(t, ok, "missing %s", id)
assert.Equal(t, StatusSkipped, r.Status, "id=%s", id)
@@ -680,9 +731,13 @@ func TestRun_ConnectRefused(t *testing.T) {
rU, _ := finalByID(results, "udp")
assert.Equal(t, StatusPassed, rU.Status, "udp should pass independently of connect")
// stun depends on udp → passes too.
rS, _ := finalByID(results, "stun")
assert.Equal(t, StatusPassed, rS.Status)
// voice-quality depends on udp → passes too.
rVQ, _ := finalByID(results, "voice-quality")
assert.Equal(t, StatusPassed, rVQ.Status)
// voice-srv depends on connect → skipped.
rVS, _ := finalByID(results, "voice-srv")
assert.Equal(t, StatusSkipped, rVS.Status)
// api depends on connect → skipped.
rA, _ := finalByID(results, "api")
@@ -708,8 +763,13 @@ func TestRun_UDPUnsupported(t *testing.T) {
require.Equal(t, StatusFailed, rU.Status)
assert.NotEmpty(t, rU.Hint)
rS, _ := finalByID(results, "stun")
assert.Equal(t, StatusSkipped, rS.Status)
// voice-quality depends on udp → skipped.
rVQ, _ := finalByID(results, "voice-quality")
assert.Equal(t, StatusSkipped, rVQ.Status)
// voice-srv depends on connect (passed) → runs and passes.
rVS, _ := finalByID(results, "voice-srv")
assert.Equal(t, StatusPassed, rVS.Status)
rA, _ := finalByID(results, "api")
assert.Equal(t, StatusPassed, rA.Status)
@@ -747,7 +807,7 @@ func TestRun_TimeoutThenOK(t *testing.T) {
assert.Equal(t, 2, greetEvents[3].Attempt)
// All seven non-auth tests should ultimately pass.
for _, id := range []string{"tcp", "greet", "connect", "udp", "stun", "api"} {
for _, id := range []string{"tcp", "greet", "connect", "udp", "voice-quality", "voice-srv", "api"} {
r, ok := finalByID(results, id)
require.True(t, ok, "missing %s", id)
assert.Equal(t, StatusPassed, r.Status, "id=%s, got %+v", id, r)
@@ -813,8 +873,9 @@ func TestRun_CancelledMidFlight(t *testing.T) {
// Either: one cancelled-failed + rest cancelled-skipped, OR all
// cancelled-skipped (if cancellation hit before next test even
// started). Both are acceptable.
// Without auth, 5 tests remain after tcp (greet/connect/udp/stun/api).
// Cancel may race with greet completing successfully, so accept ≥4.
// Without auth, 6 tests remain after tcp (greet/connect/udp/
// voice-quality/voice-srv/api). Cancel may race with greet
// completing successfully, so accept ≥4.
assert.GreaterOrEqual(t, failed+skipped, 4, "expected at least 4 cancellation-marked results, got failed=%d skipped=%d all=%+v", failed, skipped, results)
}
@@ -866,6 +927,79 @@ func TestRun_NegativeRetryClamped(t *testing.T) {
assert.Equal(t, 500*time.Millisecond, out.RetryBackoff)
}
// TestRun_VoiceQualityWarn drives the relay to drop ~1 in 10 packets,
// which puts the burst into the warn band (loss in (5, 15]%, jitter and
// p50 typically tiny on localhost). Asserts StatusWarn and that the
// metric reports a non-zero loss.
func TestRun_VoiceQualityWarn(t *testing.T) {
fp := newFakeProxy(t, "voice_quality_warn")
cfg := proxyConfig(fp, false)
cfg.DiscordGateway = stubGatewayAddr(t)
cfg.DiscordAPI = stubAPIServer(t, fp, 200)
cfg.StunServer = "127.0.0.1:65000"
// Burst of 30 with 1-in-10 drop → ~3 lost ≈ 10%.
cfg.VoiceBurstCount = 30
cfg.VoiceBurstInterval = 5 * time.Millisecond
cfg.PerTestTimeout = 1 * time.Second
fp.udpDropEveryN.Store(10)
ch := Run(context.Background(), cfg)
results := drainResults(t, ch, 15*time.Second)
rVQ, ok := finalByID(results, "voice-quality")
require.True(t, ok)
assert.Equal(t, StatusWarn, rVQ.Status, "got %+v", rVQ)
assert.Contains(t, rVQ.Metric, "loss=")
assert.NotEmpty(t, rVQ.Hint)
}
// TestRun_VoiceQualityFail drives the relay to drop 4 of every 5 packets
// (~80% loss) — well past the fail threshold.
func TestRun_VoiceQualityFail(t *testing.T) {
fp := newFakeProxy(t, "voice_quality_fail")
cfg := proxyConfig(fp, false)
cfg.DiscordGateway = stubGatewayAddr(t)
cfg.DiscordAPI = stubAPIServer(t, fp, 200)
cfg.StunServer = "127.0.0.1:65000"
cfg.VoiceBurstCount = 30
cfg.VoiceBurstInterval = 3 * time.Millisecond
cfg.PerTestTimeout = 1 * time.Second
cfg.MaxRetries = 0
// Drop everything: dropEveryN=1 means EVERY packet dropped → 100%.
// Use 2 for ~50%, 1 for 100. We want fail-band — pick 1 to guarantee
// "no replies received".
fp.udpDropEveryN.Store(1)
ch := Run(context.Background(), cfg)
results := drainResults(t, ch, 15*time.Second)
rVQ, ok := finalByID(results, "voice-quality")
require.True(t, ok)
assert.Equal(t, StatusFailed, rVQ.Status, "got %+v", rVQ)
assert.NotEmpty(t, rVQ.Hint)
}
// TestRun_VoiceSrvAllResolvedNoneReachable: DNS resolves localhost but
// the proxy refuses CONNECT to :443. Expect StatusWarn with "0/1 regions
// reachable" metric.
func TestRun_VoiceSrvAllResolvedNoneReachable(t *testing.T) {
fp := newFakeProxy(t, "voice_srv_blocked")
cfg := proxyConfig(fp, false)
cfg.DiscordGateway = stubGatewayAddr(t)
cfg.DiscordAPI = stubAPIServer(t, fp, 200)
cfg.StunServer = "127.0.0.1:65000"
fp.blockVoiceCONNECT.Store(true)
ch := Run(context.Background(), cfg)
results := drainResults(t, ch, 15*time.Second)
rVS, ok := finalByID(results, "voice-srv")
require.True(t, ok)
assert.Equal(t, StatusWarn, rVS.Status, "got %+v", rVS)
assert.Equal(t, "0/1 regions reachable", rVS.Metric)
assert.NotEmpty(t, rVS.Hint)
}
func TestExtractRawHex(t *testing.T) {
cases := []struct {
in, want string