internal/checker+gui: remove voice-srv test (Discord doesn't expose regional voice servers via public DNS)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,11 +56,6 @@ type Config struct {
|
|||||||
// voice-quality burst tuning
|
// voice-quality burst tuning
|
||||||
VoiceBurstCount int // default 30
|
VoiceBurstCount int // default 30
|
||||||
VoiceBurstInterval time.Duration // default 20ms
|
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
|
// StatusWarn is a "soft pass" — the test technically succeeded but
|
||||||
@@ -93,7 +88,6 @@ Sequential. Each test reuses sockets opened by previous tests when sensible.
|
|||||||
| `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 |
|
||||||
| `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-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
|
||||||
|
|||||||
+1
-104
@@ -8,7 +8,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,13 +62,6 @@ type Config struct {
|
|||||||
// packets, 20ms between sends.
|
// packets, 20ms between sends.
|
||||||
VoiceBurstCount int
|
VoiceBurstCount int
|
||||||
VoiceBurstInterval time.Duration
|
VoiceBurstInterval time.Duration
|
||||||
|
|
||||||
// VoiceServerHostnames is the list of Discord voice-domain hostnames
|
|
||||||
// probed in the voice-srv test. Empty means "use the built-in 16-region
|
|
||||||
// default" (russia, russia2, frankfurt, europe, singapore, japan,
|
|
||||||
// us-east, us-west, brazil, india, hongkong, southkorea, sydney,
|
|
||||||
// southafrica, dubai, atlanta — all under .discord.media).
|
|
||||||
VoiceServerHostnames []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyDefaults returns a copy of cfg with zero-valued knobs filled in.
|
// applyDefaults returns a copy of cfg with zero-valued knobs filled in.
|
||||||
@@ -107,9 +99,6 @@ func applyDefaults(cfg Config) Config {
|
|||||||
if cfg.VoiceBurstInterval <= 0 {
|
if cfg.VoiceBurstInterval <= 0 {
|
||||||
cfg.VoiceBurstInterval = 20 * time.Millisecond
|
cfg.VoiceBurstInterval = 20 * time.Millisecond
|
||||||
}
|
}
|
||||||
if len(cfg.VoiceServerHostnames) == 0 {
|
|
||||||
cfg.VoiceServerHostnames = defaultVoiceHostnames
|
|
||||||
}
|
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +124,6 @@ func Run(ctx context.Context, cfg Config) <-chan Result {
|
|||||||
e.runConnect()
|
e.runConnect()
|
||||||
e.runUDP()
|
e.runUDP()
|
||||||
e.runVoiceQuality()
|
e.runVoiceQuality()
|
||||||
e.runVoiceSrv()
|
|
||||||
e.runAPI()
|
e.runAPI()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -165,7 +153,7 @@ type executor struct {
|
|||||||
udpClient net.PacketConn
|
udpClient net.PacketConn
|
||||||
|
|
||||||
// Step gating: each xOK is set true on success (or "soft pass"
|
// Step gating: each xOK is set true on success (or "soft pass"
|
||||||
// warn for voice-quality / voice-srv).
|
// warn for voice-quality).
|
||||||
tcpOK, greetOK, authOK, connectOK, udpOK, voiceQualityOK bool
|
tcpOK, greetOK, authOK, connectOK, udpOK, voiceQualityOK bool
|
||||||
|
|
||||||
// Cancellation latch. Once any test emits a "cancelled" failure,
|
// Cancellation latch. Once any test emits a "cancelled" failure,
|
||||||
@@ -761,97 +749,6 @@ func (e *executor) runVoiceQuality() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runVoiceSrv — Test 7: probe Discord voice-domain reachability through
|
|
||||||
// the SOCKS5 proxy. Single attempt (DNS+connect bursts are slow and
|
|
||||||
// idempotent — retry budget better spent on transient handshake steps).
|
|
||||||
func (e *executor) runVoiceSrv() {
|
|
||||||
if e.shouldSkip("voice-srv", e.connectOK) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
attempt := 1
|
|
||||||
e.emit(Result{ID: "voice-srv", Status: StatusRunning, Attempt: attempt})
|
|
||||||
attemptCtx, cancel := context.WithTimeout(e.ctx, e.cfg.PerTestTimeout)
|
|
||||||
defer cancel()
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
// Per-host dial timeout — fits inside PerTestTimeout but isn't the
|
|
||||||
// whole budget (we run 8 in parallel, so total wall-clock is roughly
|
|
||||||
// ceil(N/8) * dialTimeout + DNS).
|
|
||||||
dialTimeout := e.cfg.PerTestTimeout / 4
|
|
||||||
if dialTimeout < 500*time.Millisecond {
|
|
||||||
dialTimeout = 500 * time.Millisecond
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := runVoiceServerProbe(
|
|
||||||
attemptCtx, e.cfg.VoiceServerHostnames, e.proxyAddr(),
|
|
||||||
e.cfg.UseAuth, e.cfg.ProxyLogin, e.cfg.ProxyPassword, dialTimeout,
|
|
||||||
)
|
|
||||||
dur := time.Since(start)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if e.ctx.Err() != nil {
|
|
||||||
e.emitCancelled("voice-srv", attempt, dur)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.emit(Result{
|
|
||||||
ID: "voice-srv",
|
|
||||||
Status: StatusFailed,
|
|
||||||
Error: err.Error(),
|
|
||||||
Hint: hintFor("voice-srv", err),
|
|
||||||
Attempt: attempt,
|
|
||||||
Duration: dur,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(res.Resolved) == 0 {
|
|
||||||
e.emit(Result{
|
|
||||||
ID: "voice-srv",
|
|
||||||
Status: StatusFailed,
|
|
||||||
Error: "no Discord voice domain resolved",
|
|
||||||
Hint: "DNS не возвращает A-записи для *.discord.media — проверь системный DNS.",
|
|
||||||
Attempt: attempt,
|
|
||||||
Duration: dur,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(res.Reachable) == 0 {
|
|
||||||
e.emit(Result{
|
|
||||||
ID: "voice-srv",
|
|
||||||
Status: StatusWarn,
|
|
||||||
Metric: fmt.Sprintf("0/%d regions reachable", len(res.Resolved)),
|
|
||||||
Hint: "Discord voice-домены резолвятся, но прокси не пропускает к ним TCP — голос с большой вероятностью не заработает.",
|
|
||||||
Attempt: attempt,
|
|
||||||
Duration: dur,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top-3 region prefixes for the metric line.
|
|
||||||
top := res.Reachable
|
|
||||||
if len(top) > 3 {
|
|
||||||
top = top[:3]
|
|
||||||
}
|
|
||||||
prefixes := make([]string, 0, len(top))
|
|
||||||
for _, h := range top {
|
|
||||||
prefix := h
|
|
||||||
if i := strings.Index(h, ".discord.media"); i > 0 {
|
|
||||||
prefix = h[:i]
|
|
||||||
}
|
|
||||||
prefixes = append(prefixes, prefix)
|
|
||||||
}
|
|
||||||
metric := fmt.Sprintf("%d/%d regions: %s", len(res.Reachable), len(res.Resolved), strings.Join(prefixes, ", "))
|
|
||||||
e.emit(Result{
|
|
||||||
ID: "voice-srv",
|
|
||||||
Status: StatusPassed,
|
|
||||||
Metric: metric,
|
|
||||||
Attempt: attempt,
|
|
||||||
Duration: dur,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// runAPI — Test 7: HTTP GET Discord API gateway URL through the proxy.
|
// runAPI — Test 7: HTTP GET Discord API gateway URL through the proxy.
|
||||||
func (e *executor) runAPI() {
|
func (e *executor) runAPI() {
|
||||||
if e.shouldSkip("api", e.connectOK) {
|
if e.shouldSkip("api", e.connectOK) {
|
||||||
|
|||||||
@@ -48,11 +48,6 @@ type fakeProxy struct {
|
|||||||
apiTargetPort uint16
|
apiTargetPort uint16
|
||||||
apiTargetAddr string
|
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
|
// timeoutFirstAttempt stalls the first connection on greet to
|
||||||
// drive a timeout. Subsequent connections behave normally.
|
// drive a timeout. Subsequent connections behave normally.
|
||||||
timeoutFirstAttempt atomic.Int32
|
timeoutFirstAttempt atomic.Int32
|
||||||
@@ -96,7 +91,7 @@ func newFakeProxy(t *testing.T, scenario string) *fakeProxy {
|
|||||||
func needsUDPRelay(scenario string) bool {
|
func needsUDPRelay(scenario string) bool {
|
||||||
switch scenario {
|
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":
|
"voice_quality_warn", "voice_quality_fail":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -196,23 +191,6 @@ func (fp *fakeProxy) handle(conn net.Conn) {
|
|||||||
|
|
||||||
switch cmdReq.cmd {
|
switch cmdReq.cmd {
|
||||||
case 0x01: // CONNECT
|
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 {
|
switch fp.scenario {
|
||||||
case "connect_refused":
|
case "connect_refused":
|
||||||
_, _ = conn.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
|
_, _ = conn.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
|
||||||
@@ -517,13 +495,6 @@ func hostPort(addr string) (string, int) {
|
|||||||
|
|
||||||
// proxyConfig builds a Config pointed at the given fakeProxy with sane
|
// proxyConfig builds a Config pointed at the given fakeProxy with sane
|
||||||
// short timeouts for tests.
|
// 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 {
|
func proxyConfig(fp *fakeProxy, useAuth bool) Config {
|
||||||
host, port := hostPort(fp.addr)
|
host, port := hostPort(fp.addr)
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
@@ -535,7 +506,6 @@ func proxyConfig(fp *fakeProxy, useAuth bool) Config {
|
|||||||
RetryBackoff: 30 * time.Millisecond,
|
RetryBackoff: 30 * time.Millisecond,
|
||||||
VoiceBurstCount: 10,
|
VoiceBurstCount: 10,
|
||||||
VoiceBurstInterval: 5 * time.Millisecond,
|
VoiceBurstInterval: 5 * time.Millisecond,
|
||||||
VoiceServerHostnames: []string{"localhost"},
|
|
||||||
}
|
}
|
||||||
if useAuth {
|
if useAuth {
|
||||||
cfg.ProxyLogin = "u"
|
cfg.ProxyLogin = "u"
|
||||||
@@ -609,7 +579,7 @@ func TestRun_HappyNoAuth(t *testing.T) {
|
|||||||
ch := Run(context.Background(), cfg)
|
ch := Run(context.Background(), cfg)
|
||||||
results := drainResults(t, ch, 10*time.Second)
|
results := drainResults(t, ch, 10*time.Second)
|
||||||
|
|
||||||
expected := []string{"tcp", "greet", "connect", "udp", "voice-quality", "voice-srv", "api"}
|
expected := []string{"tcp", "greet", "connect", "udp", "voice-quality", "api"}
|
||||||
finals := map[string]Result{}
|
finals := map[string]Result{}
|
||||||
for _, id := range expected {
|
for _, id := range expected {
|
||||||
r, ok := finalByID(results, id)
|
r, ok := finalByID(results, id)
|
||||||
@@ -629,7 +599,6 @@ func TestRun_HappyNoAuth(t *testing.T) {
|
|||||||
assert.Contains(t, finals["greet"].Metric, "no auth")
|
assert.Contains(t, finals["greet"].Metric, "no auth")
|
||||||
assert.Equal(t, "REP=00", finals["connect"].Metric)
|
assert.Equal(t, "REP=00", finals["connect"].Metric)
|
||||||
assert.Contains(t, finals["voice-quality"].Metric, "loss=")
|
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)
|
assert.Equal(t, "HTTP 200", finals["api"].Metric)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,7 +612,7 @@ func TestRun_HappyWithAuth(t *testing.T) {
|
|||||||
ch := Run(context.Background(), cfg)
|
ch := Run(context.Background(), cfg)
|
||||||
results := drainResults(t, ch, 10*time.Second)
|
results := drainResults(t, ch, 10*time.Second)
|
||||||
|
|
||||||
expected := []string{"tcp", "greet", "auth", "connect", "udp", "voice-quality", "voice-srv", "api"}
|
expected := []string{"tcp", "greet", "auth", "connect", "udp", "voice-quality", "api"}
|
||||||
for _, id := range expected {
|
for _, id := range expected {
|
||||||
r, ok := finalByID(results, id)
|
r, ok := finalByID(results, id)
|
||||||
require.True(t, ok, "missing %s; results=%+v", id, results)
|
require.True(t, ok, "missing %s; results=%+v", id, results)
|
||||||
@@ -674,7 +643,7 @@ func TestRun_AuthRejected(t *testing.T) {
|
|||||||
assert.Equal(t, StatusFailed, rA.Status)
|
assert.Equal(t, StatusFailed, rA.Status)
|
||||||
assert.NotEmpty(t, rA.Hint)
|
assert.NotEmpty(t, rA.Hint)
|
||||||
|
|
||||||
for _, id := range []string{"connect", "udp", "voice-quality", "voice-srv", "api"} {
|
for _, id := range []string{"connect", "udp", "voice-quality", "api"} {
|
||||||
r, ok := finalByID(results, id)
|
r, ok := finalByID(results, id)
|
||||||
require.True(t, ok, "missing %s", id)
|
require.True(t, ok, "missing %s", id)
|
||||||
assert.Equal(t, StatusSkipped, r.Status, "id=%s", id)
|
assert.Equal(t, StatusSkipped, r.Status, "id=%s", id)
|
||||||
@@ -699,7 +668,7 @@ func TestRun_AllMethodsRejected(t *testing.T) {
|
|||||||
assert.Equal(t, StatusFailed, rG.Status)
|
assert.Equal(t, StatusFailed, rG.Status)
|
||||||
assert.NotEmpty(t, rG.Hint)
|
assert.NotEmpty(t, rG.Hint)
|
||||||
|
|
||||||
for _, id := range []string{"connect", "udp", "voice-quality", "voice-srv", "api"} {
|
for _, id := range []string{"connect", "udp", "voice-quality", "api"} {
|
||||||
r, ok := finalByID(results, id)
|
r, ok := finalByID(results, id)
|
||||||
require.True(t, ok, "missing %s", id)
|
require.True(t, ok, "missing %s", id)
|
||||||
assert.Equal(t, StatusSkipped, r.Status, "id=%s", id)
|
assert.Equal(t, StatusSkipped, r.Status, "id=%s", id)
|
||||||
@@ -735,10 +704,6 @@ func TestRun_ConnectRefused(t *testing.T) {
|
|||||||
rVQ, _ := finalByID(results, "voice-quality")
|
rVQ, _ := finalByID(results, "voice-quality")
|
||||||
assert.Equal(t, StatusPassed, rVQ.Status)
|
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.
|
// api depends on connect → skipped.
|
||||||
rA, _ := finalByID(results, "api")
|
rA, _ := finalByID(results, "api")
|
||||||
assert.Equal(t, StatusSkipped, rA.Status)
|
assert.Equal(t, StatusSkipped, rA.Status)
|
||||||
@@ -767,10 +732,6 @@ func TestRun_UDPUnsupported(t *testing.T) {
|
|||||||
rVQ, _ := finalByID(results, "voice-quality")
|
rVQ, _ := finalByID(results, "voice-quality")
|
||||||
assert.Equal(t, StatusSkipped, rVQ.Status)
|
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")
|
rA, _ := finalByID(results, "api")
|
||||||
assert.Equal(t, StatusPassed, rA.Status)
|
assert.Equal(t, StatusPassed, rA.Status)
|
||||||
}
|
}
|
||||||
@@ -806,8 +767,8 @@ func TestRun_TimeoutThenOK(t *testing.T) {
|
|||||||
assert.Equal(t, StatusPassed, greetEvents[3].Status)
|
assert.Equal(t, StatusPassed, greetEvents[3].Status)
|
||||||
assert.Equal(t, 2, greetEvents[3].Attempt)
|
assert.Equal(t, 2, greetEvents[3].Attempt)
|
||||||
|
|
||||||
// All seven non-auth tests should ultimately pass.
|
// All non-auth tests should ultimately pass.
|
||||||
for _, id := range []string{"tcp", "greet", "connect", "udp", "voice-quality", "voice-srv", "api"} {
|
for _, id := range []string{"tcp", "greet", "connect", "udp", "voice-quality", "api"} {
|
||||||
r, ok := finalByID(results, id)
|
r, ok := finalByID(results, id)
|
||||||
require.True(t, ok, "missing %s", id)
|
require.True(t, ok, "missing %s", id)
|
||||||
assert.Equal(t, StatusPassed, r.Status, "id=%s, got %+v", id, r)
|
assert.Equal(t, StatusPassed, r.Status, "id=%s, got %+v", id, r)
|
||||||
@@ -873,10 +834,10 @@ func TestRun_CancelledMidFlight(t *testing.T) {
|
|||||||
// Either: one cancelled-failed + rest cancelled-skipped, OR all
|
// Either: one cancelled-failed + rest cancelled-skipped, OR all
|
||||||
// cancelled-skipped (if cancellation hit before next test even
|
// cancelled-skipped (if cancellation hit before next test even
|
||||||
// started). Both are acceptable.
|
// started). Both are acceptable.
|
||||||
// Without auth, 6 tests remain after tcp (greet/connect/udp/
|
// Without auth, 5 tests remain after tcp (greet/connect/udp/
|
||||||
// voice-quality/voice-srv/api). Cancel may race with greet
|
// voice-quality/api). Cancel may race with greet
|
||||||
// completing successfully, so accept ≥4.
|
// completing successfully, so accept ≥3.
|
||||||
assert.GreaterOrEqual(t, failed+skipped, 4, "expected at least 4 cancellation-marked results, got failed=%d skipped=%d all=%+v", failed, skipped, results)
|
assert.GreaterOrEqual(t, failed+skipped, 3, "expected at least 3 cancellation-marked results, got failed=%d skipped=%d all=%+v", failed, skipped, results)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRun_AppliesDefaults(t *testing.T) {
|
func TestRun_AppliesDefaults(t *testing.T) {
|
||||||
@@ -979,27 +940,6 @@ func TestRun_VoiceQualityFail(t *testing.T) {
|
|||||||
assert.NotEmpty(t, rVQ.Hint)
|
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) {
|
func TestExtractRawHex(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
in, want string
|
in, want string
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ func tcpFriendlyName(testID string) string {
|
|||||||
return "UDP ASSOCIATE"
|
return "UDP ASSOCIATE"
|
||||||
case "voice-quality":
|
case "voice-quality":
|
||||||
return "качество UDP-канала"
|
return "качество UDP-канала"
|
||||||
case "voice-srv":
|
|
||||||
return "доступность voice-серверов Discord"
|
|
||||||
case "api":
|
case "api":
|
||||||
return "Discord API"
|
return "Discord API"
|
||||||
default:
|
default:
|
||||||
@@ -147,16 +145,6 @@ func hintFor(testID string, err error) string {
|
|||||||
}
|
}
|
||||||
return genericFallback(testID, err)
|
return genericFallback(testID, err)
|
||||||
|
|
||||||
case "voice-srv":
|
|
||||||
var dnsErr *net.DNSError
|
|
||||||
if errors.As(err, &dnsErr) {
|
|
||||||
return "DNS не резолвит voice-домены Discord."
|
|
||||||
}
|
|
||||||
if isTimeout {
|
|
||||||
return "Не удалось проверить voice-серверы Discord вовремя — таймаут."
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("Не удалось проверить доступность Discord voice-серверов: %s.", err.Error())
|
|
||||||
|
|
||||||
case "api":
|
case "api":
|
||||||
switch {
|
switch {
|
||||||
case isTimeout:
|
case isTimeout:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func TestHintFor(t *testing.T) {
|
|||||||
t.Run("context_canceled_uniform", func(t *testing.T) {
|
t.Run("context_canceled_uniform", func(t *testing.T) {
|
||||||
// Cancellation is always reported as «Проверка отменена.» across
|
// Cancellation is always reported as «Проверка отменена.» across
|
||||||
// all testIDs.
|
// all testIDs.
|
||||||
for _, id := range []string{"tcp", "greet", "auth", "connect", "udp", "voice-quality", "voice-srv", "api", "unknown"} {
|
for _, id := range []string{"tcp", "greet", "auth", "connect", "udp", "voice-quality", "api", "unknown"} {
|
||||||
assert.Equal(t, "Проверка отменена.", hintFor(id, context.Canceled), "id=%s", id)
|
assert.Equal(t, "Проверка отменена.", hintFor(id, context.Canceled), "id=%s", id)
|
||||||
assert.Equal(t, "Проверка отменена.", hintFor(id, context.DeadlineExceeded), "id=%s", id)
|
assert.Equal(t, "Проверка отменена.", hintFor(id, context.DeadlineExceeded), "id=%s", id)
|
||||||
}
|
}
|
||||||
@@ -121,10 +121,6 @@ func TestHintFor_PerStepBranches(t *testing.T) {
|
|||||||
{"voice_quality_unsupported_family", "voice-quality", ErrSTUNUnsupportedFamily, "мусор"},
|
{"voice_quality_unsupported_family", "voice-quality", ErrSTUNUnsupportedFamily, "мусор"},
|
||||||
{"voice_quality_fallback", "voice-quality", errors.New("weird"), "качество"},
|
{"voice_quality_fallback", "voice-quality", errors.New("weird"), "качество"},
|
||||||
|
|
||||||
// voice-srv: DNS error, timeout, generic
|
|
||||||
{"voice_srv_timeout", "voice-srv", &timeoutOnlyError{}, "таймаут"},
|
|
||||||
{"voice_srv_generic", "voice-srv", errors.New("boom"), "boom"},
|
|
||||||
|
|
||||||
// api: timeout vs generic
|
// api: timeout vs generic
|
||||||
{"api_timeout", "api", &timeoutOnlyError{}, "таймаут"},
|
{"api_timeout", "api", &timeoutOnlyError{}, "таймаут"},
|
||||||
{"api_generic", "api", errors.New("tls boom"), "TLS"},
|
{"api_generic", "api", errors.New("tls boom"), "TLS"},
|
||||||
@@ -158,7 +154,6 @@ func TestTcpFriendlyName(t *testing.T) {
|
|||||||
"connect": "TCP-туннель к Discord",
|
"connect": "TCP-туннель к Discord",
|
||||||
"udp": "UDP ASSOCIATE",
|
"udp": "UDP ASSOCIATE",
|
||||||
"voice-quality": "качество UDP-канала",
|
"voice-quality": "качество UDP-канала",
|
||||||
"voice-srv": "доступность voice-серверов Discord",
|
|
||||||
"api": "Discord API",
|
"api": "Discord API",
|
||||||
"weirdo": "weirdo",
|
"weirdo": "weirdo",
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-164
@@ -2,20 +2,11 @@ package checker
|
|||||||
|
|
||||||
// voice.go — predictive voice diagnostics.
|
// voice.go — predictive voice diagnostics.
|
||||||
//
|
//
|
||||||
// Two primitives sit on top of the SOCKS5/STUN building blocks already
|
// runVoiceQualityBurst fires a burst of STUN binding requests through
|
||||||
// in this package:
|
// an open SOCKS5 UDP relay, then derives packet-loss / jitter /
|
||||||
//
|
|
||||||
// - runVoiceQualityBurst: fire a burst of STUN binding requests through
|
|
||||||
// an open SOCKS5 UDP relay, then derive packet-loss / jitter /
|
|
||||||
// percentile-RTT from the replies. A single round-trip says the relay
|
// percentile-RTT from the replies. A single round-trip says the relay
|
||||||
// accepts UDP; a 30-packet burst tells you whether voice will actually
|
// accepts UDP; a 30-packet burst tells you whether voice will actually
|
||||||
// hold together.
|
// hold together.
|
||||||
//
|
|
||||||
// - runVoiceServerProbe: parallel-DNS a list of <region>.discord.media
|
|
||||||
// hostnames, then SOCKS5 CONNECT to :443 on each, recording which
|
|
||||||
// regions are reachable through the proxy. DNS-resolves-but-CONNECT-fails
|
|
||||||
// is a very common Russian-DPI failure mode that all five preceding
|
|
||||||
// SOCKS5 sanity checks miss.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -283,156 +274,3 @@ sendLoop:
|
|||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VoiceServerProbeResult lists Discord voice-domain hostnames split by
|
|
||||||
// outcome. Resolved is ordered by input position; Reachable is too.
|
|
||||||
type VoiceServerProbeResult struct {
|
|
||||||
Resolved []string // hostnames that got at least one A record
|
|
||||||
Reachable []string // hostnames that succeeded SOCKS5 CONNECT
|
|
||||||
Unresolved []string // hostnames that didn't resolve at all
|
|
||||||
UnreachableButResolved []string // resolved but proxy CONNECT failed
|
|
||||||
}
|
|
||||||
|
|
||||||
// runVoiceServerProbe resolves each hostname and, for those that resolved
|
|
||||||
// to ≥ 1 IPv4, performs a SOCKS5 CONNECT to host:443 through proxyAddr.
|
|
||||||
// REP=00 is enough — we don't actually do TLS.
|
|
||||||
//
|
|
||||||
// DNS goes through the OS resolver (not the proxy). For tests, pass
|
|
||||||
// hostnames that always resolve locally (e.g. "localhost").
|
|
||||||
//
|
|
||||||
// Returns an error only when ctx is cancelled.
|
|
||||||
func runVoiceServerProbe(
|
|
||||||
ctx context.Context,
|
|
||||||
hostnames []string,
|
|
||||||
proxyAddr string,
|
|
||||||
useAuth bool,
|
|
||||||
login, password string,
|
|
||||||
dialTimeout time.Duration,
|
|
||||||
) (VoiceServerProbeResult, error) {
|
|
||||||
if dialTimeout <= 0 {
|
|
||||||
dialTimeout = 1 * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
const dnsConcurrency = 16
|
|
||||||
const connectConcurrency = 8
|
|
||||||
|
|
||||||
// Phase 1: parallel DNS.
|
|
||||||
type dnsOut struct {
|
|
||||||
idx int
|
|
||||||
host string
|
|
||||||
resolved bool
|
|
||||||
}
|
|
||||||
dnsResults := make([]dnsOut, len(hostnames))
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
dnsSem := make(chan struct{}, dnsConcurrency)
|
|
||||||
for i, h := range hostnames {
|
|
||||||
wg.Add(1)
|
|
||||||
dnsSem <- struct{}{}
|
|
||||||
go func(i int, h string) {
|
|
||||||
defer wg.Done()
|
|
||||||
defer func() { <-dnsSem }()
|
|
||||||
ips, err := net.DefaultResolver.LookupIP(ctx, "ip4", h)
|
|
||||||
ok := err == nil && len(ips) > 0
|
|
||||||
dnsResults[i] = dnsOut{idx: i, host: h, resolved: ok}
|
|
||||||
}(i, h)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if cerr := ctx.Err(); cerr != nil {
|
|
||||||
return VoiceServerProbeResult{}, cerr
|
|
||||||
}
|
|
||||||
|
|
||||||
res := VoiceServerProbeResult{}
|
|
||||||
// Build resolved/unresolved lists in input order.
|
|
||||||
resolvedHosts := make([]string, 0, len(hostnames))
|
|
||||||
for _, dr := range dnsResults {
|
|
||||||
if dr.resolved {
|
|
||||||
res.Resolved = append(res.Resolved, dr.host)
|
|
||||||
resolvedHosts = append(resolvedHosts, dr.host)
|
|
||||||
} else {
|
|
||||||
res.Unresolved = append(res.Unresolved, dr.host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resolvedHosts) == 0 {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: parallel SOCKS5 CONNECT.
|
|
||||||
reachable := make([]bool, len(resolvedHosts))
|
|
||||||
connectSem := make(chan struct{}, connectConcurrency)
|
|
||||||
wg = sync.WaitGroup{}
|
|
||||||
for i, h := range resolvedHosts {
|
|
||||||
wg.Add(1)
|
|
||||||
connectSem <- struct{}{}
|
|
||||||
go func(i int, h string) {
|
|
||||||
defer wg.Done()
|
|
||||||
defer func() { <-connectSem }()
|
|
||||||
reachable[i] = probeSocks5Connect(ctx, proxyAddr, useAuth, login, password, h, 443, dialTimeout)
|
|
||||||
}(i, h)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if cerr := ctx.Err(); cerr != nil {
|
|
||||||
return res, cerr
|
|
||||||
}
|
|
||||||
for i, h := range resolvedHosts {
|
|
||||||
if reachable[i] {
|
|
||||||
res.Reachable = append(res.Reachable, h)
|
|
||||||
} else {
|
|
||||||
res.UnreachableButResolved = append(res.UnreachableButResolved, h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// probeSocks5Connect opens one fresh TCP connection to proxyAddr, runs
|
|
||||||
// the SOCKS5 handshake and CONNECT request to host:port, and returns
|
|
||||||
// true iff the proxy replied REP=00. Always closes the conn before
|
|
||||||
// returning.
|
|
||||||
func probeSocks5Connect(
|
|
||||||
parent context.Context,
|
|
||||||
proxyAddr string,
|
|
||||||
useAuth bool,
|
|
||||||
login, password string,
|
|
||||||
host string,
|
|
||||||
port uint16,
|
|
||||||
dialTimeout time.Duration,
|
|
||||||
) bool {
|
|
||||||
ctx, cancel := context.WithTimeout(parent, dialTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var d net.Dialer
|
|
||||||
conn, err := d.DialContext(ctx, "tcp", proxyAddr)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
if _, _, gerr := socks5Greeting(ctx, conn, useAuth); gerr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if useAuth {
|
|
||||||
if _, aerr := socks5Auth(ctx, conn, login, password); aerr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, cerr := socks5Connect(ctx, conn, host, port); cerr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultVoiceHostnames is the built-in list used when
|
|
||||||
// Config.VoiceServerHostnames is empty. Order matters — the GUI shows
|
|
||||||
// the first three reachable regions in the metric line.
|
|
||||||
var defaultVoiceHostnames = []string{
|
|
||||||
"russia.discord.media", "russia2.discord.media",
|
|
||||||
"frankfurt.discord.media", "europe.discord.media",
|
|
||||||
"singapore.discord.media", "japan.discord.media",
|
|
||||||
"us-east.discord.media", "us-west.discord.media",
|
|
||||||
"brazil.discord.media", "india.discord.media",
|
|
||||||
"hongkong.discord.media", "southkorea.discord.media",
|
|
||||||
"sydney.discord.media", "southafrica.discord.media",
|
|
||||||
"dubai.discord.media", "atlanta.discord.media",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -190,111 +190,3 @@ func TestVoiceQualityBurst_ZeroCount(t *testing.T) {
|
|||||||
"localhost", 19302, 0, 5*time.Millisecond)
|
"localhost", 19302, 0, 5*time.Millisecond)
|
||||||
assert.Error(t, err)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ type Config struct {
|
|||||||
// for them on the "check:result" event. Mirrors checker.Result but with
|
// for them on the "check:result" event. Mirrors checker.Result but with
|
||||||
// Duration converted to milliseconds (int) for the JS side.
|
// Duration converted to milliseconds (int) for the JS side.
|
||||||
type CheckResult struct {
|
type CheckResult struct {
|
||||||
ID string `json:"id"` // tcp / greet / auth / connect / udp / voice-quality / voice-srv / api
|
ID string `json:"id"` // tcp / greet / auth / connect / udp / voice-quality / api
|
||||||
Status string `json:"status"` // running | passed | warn | failed | skipped
|
Status string `json:"status"` // running | passed | warn | failed | skipped
|
||||||
Metric string `json:"metric,omitempty"`
|
Metric string `json:"metric,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export const ALL_TESTS = [
|
|||||||
{ id: 'connect', label: 'TCP CONNECT to Discord', desc: 'Прокси проксирует TCP к gateway' },
|
{ id: 'connect', label: 'TCP CONNECT to Discord', desc: 'Прокси проксирует TCP к gateway' },
|
||||||
{ id: 'udp', label: 'UDP ASSOCIATE', desc: 'Прокси выдал UDP-релей' },
|
{ id: 'udp', label: 'UDP ASSOCIATE', desc: 'Прокси выдал UDP-релей' },
|
||||||
{ id: 'voice-quality', label: 'UDP voice quality', desc: 'Бёрст 30 STUN-пакетов через релей: потери, джиттер, латентность' },
|
{ id: 'voice-quality', label: 'UDP voice quality', desc: 'Бёрст 30 STUN-пакетов через релей: потери, джиттер, латентность' },
|
||||||
{ id: 'voice-srv', label: 'Discord voice servers', desc: 'Какие регионы Discord media доступны через прокси' },
|
|
||||||
{ id: 'api', label: 'Discord API reachable', desc: 'HTTPS до discord.com через прокси' },
|
{ id: 'api', label: 'Discord API reachable', desc: 'HTTPS до discord.com через прокси' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user