Files
tslink/netdiag/egress_test.go
2026-07-26 20:44:49 +08:00

155 lines
4.4 KiB
Go

package netdiag
import (
"net/netip"
"testing"
)
func obs(m EgressMethod, ip string) EgressObservation {
o := EgressObservation{Method: m}
if ip != "" {
o.IP = netip.MustParseAddr(ip)
}
return o
}
// TestEgressDivergenceSeverity pins the distinction the verdict depends on:
// STUN disagreeing with itself is a hard failure for hole punching, whereas
// HTTP-only disagreement is a proxy artefact and must stay a warning.
func TestEgressDivergenceSeverity(t *testing.T) {
cases := []struct {
name string
obs []EgressObservation
wantDivergent bool
wantSTUN bool
wantStatus Status
}{
{
name: "single egress",
obs: []EgressObservation{obs(MethodSTUN, "1.2.3.4"), obs(MethodHTTPv4, "1.2.3.4")},
wantStatus: StatusOK,
},
{
name: "dual stack is not divergence",
obs: []EgressObservation{
obs(MethodSTUN, "1.2.3.4"), obs(MethodHTTPv6, "2001:db8::1"),
},
wantStatus: StatusOK,
},
{
name: "http-only split warns",
obs: []EgressObservation{
obs(MethodSTUN, "1.2.3.4"),
obs(MethodHTTPv4, "5.6.7.8"),
},
wantDivergent: true,
wantSTUN: false,
wantStatus: StatusWarn,
},
{
name: "stun split fails",
obs: []EgressObservation{
obs(MethodSTUN, "1.2.3.4"),
obs(MethodSTUN, "5.6.7.8"),
},
wantDivergent: true,
wantSTUN: true,
wantStatus: StatusFail,
},
{
name: "proxy split alone stays a warning",
obs: []EgressObservation{
obs(MethodSTUN, "1.2.3.4"),
obs(MethodHTTPProxy, "9.9.9.9"),
},
wantDivergent: true,
wantSTUN: false,
wantStatus: StatusWarn,
},
{
name: "no observations fails",
obs: []EgressObservation{obs(MethodSTUN, "")},
wantStatus: StatusFail,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rep := EgressReport{Observations: tc.obs}
rep.UniqueIPs = egUniqueIPs(rep.Observations)
rep.Divergent = egDivergent(rep.UniqueIPs)
rep.DivergentSTUN = egDivergentSTUN(rep.Observations)
egFinish(&rep)
if rep.Divergent != tc.wantDivergent {
t.Errorf("Divergent = %v, want %v", rep.Divergent, tc.wantDivergent)
}
if rep.DivergentSTUN != tc.wantSTUN {
t.Errorf("DivergentSTUN = %v, want %v", rep.DivergentSTUN, tc.wantSTUN)
}
if rep.Status != tc.wantStatus {
t.Errorf("Status = %v, want %v (summary: %s)", rep.Status, tc.wantStatus, rep.Summary)
}
})
}
}
// A STUN observation that errored carries no address and must not be mistaken
// for a second egress.
func TestEgressDivergentSTUNIgnoresErrors(t *testing.T) {
o := []EgressObservation{
obs(MethodSTUN, "1.2.3.4"),
{Method: MethodSTUN, Err: "timeout"},
}
if egDivergentSTUN(o) {
t.Error("a failed STUN probe must not count as a second egress IP")
}
}
// egFinish runs again after geolocation, so it must not drift.
func TestEgFinishIdempotent(t *testing.T) {
rep := EgressReport{Observations: []EgressObservation{
obs(MethodSTUN, "1.2.3.4"), obs(MethodSTUN, "5.6.7.8"),
}}
rep.UniqueIPs = egUniqueIPs(rep.Observations)
rep.Divergent = egDivergent(rep.UniqueIPs)
rep.DivergentSTUN = egDivergentSTUN(rep.Observations)
egFinish(&rep)
first, status := rep.Summary, rep.Status
egFinish(&rep)
if rep.Summary != first || rep.Status != status {
t.Errorf("egFinish is not idempotent:\n first: %s (%v)\nsecond: %s (%v)",
first, status, rep.Summary, rep.Status)
}
}
// TestHeadlineDivergence checks the two verdict strings the user sees.
//
// The report is otherwise healthy: earlier branches (blocked UDP, symmetric
// NAT, unreachable overseas) all outrank egress and would mask it.
func healthyReport(eg EgressReport) *Report {
return &Report{
UDP: UDPReport{V4OK: true},
NAT: NATReport{Type: NATFullCone},
Overseas: OverseasReport{Status: StatusOK},
Egress: eg,
}
}
func TestHeadlineDivergence(t *testing.T) {
strong := healthyReport(EgressReport{Divergent: true, DivergentSTUN: true})
if got, lvl := headline(strong); got != "STUN 检测到多个出口 IP,代理或分流工具正在影响连接" {
t.Errorf("strong headline = %q", got)
} else if lvl != StatusFail {
t.Errorf("strong headline severity = %v, want fail", lvl)
}
weak := healthyReport(EgressReport{Divergent: true})
if got, lvl := headline(weak); got != "仅 HTTP 探测到多个出口 IP,代理或分流工具可能影响连接" {
t.Errorf("weak headline = %q", got)
} else if lvl != StatusWarn {
t.Errorf("weak headline severity = %v, want warn", lvl)
}
}