spec: add voice-quality (burst loss/jitter) + voice-srv (Discord media probe)
Old single-shot stun test only proved one UDP packet round-tripped
through the relay. To predict whether voice will actually work the
checker now does two stronger tests:
- voice-quality: 30-packet STUN burst with loss/jitter/p50 metrics,
with a "warn" tier between hard pass and hard fail.
- voice-srv: concurrent DNS resolve + SOCKS5 TCP probe to a list of
Discord voice region hostnames; passes if any region is reachable.
Adds StatusWarn ("soft pass — show hint anyway") so the GUI can
distinguish "voice will work but glitchy" from green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,8 +52,23 @@ type Config struct {
|
|||||||
DiscordGateway string
|
DiscordGateway string
|
||||||
DiscordAPI string
|
DiscordAPI string
|
||||||
StunServer string
|
StunServer string
|
||||||
|
|
||||||
|
// voice-quality burst tuning
|
||||||
|
VoiceBurstCount int // default 30
|
||||||
|
VoiceBurstInterval time.Duration // default 20ms
|
||||||
|
|
||||||
|
// voice-srv probe — empty list means "use the built-in default
|
||||||
|
// (russia/russia2/frankfurt/europe/singapore/japan/us-east/us-west/
|
||||||
|
// brazil/india/hongkong/southkorea/sydney/southafrica/dubai/atlanta).discord.media"
|
||||||
|
VoiceServerHostnames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StatusWarn is a "soft pass" — the test technically succeeded but
|
||||||
|
// the user should know about a degradation (e.g. voice quality at the
|
||||||
|
// upper end of acceptable). Frontend renders it like StatusPassed but
|
||||||
|
// keeps the Hint visible.
|
||||||
|
const StatusWarn Status = "warn"
|
||||||
|
|
||||||
// Run streams Results to the returned channel and closes it when finished
|
// Run streams Results to the returned channel and closes it when finished
|
||||||
// or when ctx is cancelled. The first event for each test is Status=running;
|
// or when ctx is cancelled. The first event for each test is Status=running;
|
||||||
// the next is the final state (passed/failed/skipped). On retry, another
|
// the next is the final state (passed/failed/skipped). On retry, another
|
||||||
@@ -77,7 +92,8 @@ Sequential. Each test reuses sockets opened by previous tests when sensible.
|
|||||||
| `auth` | Only emitted when UseAuth=true. RFC 1929 sub-negotiation: `01 LEN_LOGIN LOGIN LEN_PASS PASS`. Reads 2 bytes, expects `01 00`. | bad credentials (`01 != 00`) / short read | not in test list when UseAuth=false; skipped if `greet` failed |
|
| `auth` | Only emitted when UseAuth=true. RFC 1929 sub-negotiation: `01 LEN_LOGIN LOGIN LEN_PASS PASS`. Reads 2 bytes, expects `01 00`. | bad credentials (`01 != 00`) / short read | not in test list when UseAuth=false; skipped if `greet` failed |
|
||||||
| `connect` | SOCKS5 CONNECT to `gateway.discord.gg:443` (ATYP=03 domain). Reads 10 bytes. Pass = REP=0x00. | REP != 0 (0x05 = connection refused, etc) / timeout | skipped if `greet`/`auth` failed |
|
| `connect` | SOCKS5 CONNECT to `gateway.discord.gg:443` (ATYP=03 domain). Reads 10 bytes. Pass = REP=0x00. | REP != 0 (0x05 = connection refused, etc) / timeout | skipped if `greet`/`auth` failed |
|
||||||
| `udp` | UDP ASSOCIATE: opens **second** TCP control channel, redoes greeting+auth there, sends `05 03 00 01 00000000 0000`, reads 10-byte reply. Pass = REP=0x00 + valid relay endpoint in BND.ADDR/BND.PORT. | REP=0x07 (cmd unsupported), other REP, short read | skipped if `greet` failed |
|
| `udp` | UDP ASSOCIATE: opens **second** TCP control channel, redoes greeting+auth there, sends `05 03 00 01 00000000 0000`, reads 10-byte reply. Pass = REP=0x00 + valid relay endpoint in BND.ADDR/BND.PORT. | REP=0x07 (cmd unsupported), other REP, short read | skipped if `greet` failed |
|
||||||
| `stun` | Through the relay endpoint from the previous step: send STUN binding request (20-byte header, magic cookie 0x2112A442, random transaction ID), wait up to PerTestTimeout for XOR-MAPPED-ADDRESS reply. Metric = round-trip ms. | timeout / malformed response / no XOR-MAPPED-ADDRESS attribute | skipped if `udp` failed |
|
| `voice-quality` | Through the relay: send `VoiceBurstCount` (default 30) STUN binding requests to `cfg.StunServer`, spaced `VoiceBurstInterval` (default 20ms). Listen until `last_send + 1.5*PerTestTimeout`. Compute `loss%`, `jitter` (mean abs delta of inter-arrival deltas, à la RFC 3550 simplified), `p50 RTT`. Metric = `"loss=2% jitter=14ms p50=42ms"`. **Pass** = loss ≤ 5% AND jitter ≤ 30ms AND p50 ≤ 250ms. **Warn-pass** (status=passed but Hint set) = loss ≤ 15% AND jitter ≤ 60ms — voice will work with audible glitches. **Fail** = anything worse. | loss > 15% OR jitter > 60ms OR p50 > 400ms OR no replies at all | skipped if `udp` failed |
|
||||||
|
| `voice-srv` | Probe Discord voice servers. Concurrently DNS-resolve a hardcoded list of `<region>.discord.media` hostnames (`russia`, `russia2`, `frankfurt`, `europe`, `singapore`, `japan`, `us-east`, `us-west`, `brazil`, `india`, `hongkong`, `southkorea`, `sydney`, `southafrica`, `dubai`, `atlanta`) using OS resolver, 2s budget. For every resolved hostname: SOCKS5 CONNECT through proxy to `host:443` with 1s dial timeout, run them concurrently with a small worker pool (8). Metric = `"<N> regions reachable: russia, frankfurt, europe"` (top 3). **Pass** = ≥ 1 region reachable. **Warn-pass** = 0 reachable but ≥ 1 resolved (proxy filters Discord media IPs even though DNS works) — Hint will warn that voice may not work despite checks 1-5 passing. **Fail** = 0 hostnames resolved at all (DNS broken or Discord changed naming) | 0 hostnames resolved at all | skipped if `connect` failed |
|
||||||
| `api` | TCP CONNECT through the proxy to `discord.com:443`, do a tiny HTTPS GET `/api/v9/gateway`. Pass = HTTP 200 or 401 (Discord returns 401 unauthenticated, that still proves reachability). | non-200/401 / TLS handshake failed / connect refused | skipped if `connect` failed |
|
| `api` | TCP CONNECT through the proxy to `discord.com:443`, do a tiny HTTPS GET `/api/v9/gateway`. Pass = HTTP 200 or 401 (Discord returns 401 unauthenticated, that still proves reachability). | non-200/401 / TLS handshake failed / connect refused | skipped if `connect` failed |
|
||||||
|
|
||||||
For each fail, the `Hint` field carries a Russian explanation (the GUI is
|
For each fail, the `Hint` field carries a Russian explanation (the GUI is
|
||||||
|
|||||||
Reference in New Issue
Block a user