Skip to content

Commit bf49462

Browse files
committed
Prevent stale labels, flaky websocket tests, and speed misreports
Bound Leo IP sends and stale response delivery by timeout, bootstrap speed-mode metadata lookups without JSON banner noise, respect --speed=false pre-dispatch, validate speed source binding, cap probe response reads, avoid caching empty nali labels, treat server-only speed metadata as healthy when no client IP is available, make websocket async tests event-driven, restart Leo receive parsing reliably, close websocket receive channels exactly once, send Leo requests on the waited websocket, back off failed Geo/RDNS metadata lookups, refresh MTR API info cache timestamps after lookup, report upload response read errors, avoid launching latency probes after cancellation, and keep speed JSON tests tolerant of degraded latency samples.
1 parent 510c374 commit bf49462

32 files changed

Lines changed: 1239 additions & 186 deletions

cmd/cmd.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -751,9 +751,13 @@ func applyDN42Mode(enabled bool, dataOrigin *string, disableMaptrace *bool) {
751751
if !enabled {
752752
return
753753
}
754+
applyDN42DataOrigin(dataOrigin)
755+
*disableMaptrace = true
756+
}
757+
758+
func applyDN42DataOrigin(dataOrigin *string) {
754759
config.InitConfig()
755760
*dataOrigin = "DN42"
756-
*disableMaptrace = true
757761
}
758762

759763
func prepareRuntimeEnvironment(ctx context.Context, dn42 bool, dataOrigin *string, disableMaptrace *bool, powProvider *string, asyncLeo bool) *wshandle.WsConn {
@@ -1254,8 +1258,8 @@ func Execute() {
12541258
return
12551259
}
12561260
if *speedMode {
1257-
// `--speed` should have been handled before the main parser runs. If we reached
1258-
// here, return a defensive error to avoid silently falling through.
1261+
// maybeRunSpeedMode should handle --speed before parser.Parse runs. This
1262+
// branch is a safety net for parser edge cases such as a "--" terminator.
12591263
fmt.Fprintln(os.Stderr, "internal error: speed mode dispatch failed")
12601264
os.Exit(1)
12611265
}

cmd/nali_mode.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,9 @@ func runNaliMode(ctx context.Context, opts naliRunOptions) error {
218218
restoreFastIPOutput := setFastIPOutputSuppression(true)
219219
defer restoreFastIPOutput()
220220

221-
disableMaptrace := false
222-
applyDN42Mode(opts.dn42, &opts.data, &disableMaptrace)
221+
if opts.dn42 {
222+
applyDN42DataOrigin(&opts.data)
223+
}
223224
leoWs := initLeoWebsocket(ctx, &opts.data, &opts.pow, false)
224225
defer closeLeoWebsocket(leoWs)
225226

cmd/nali_mode_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ func TestValidateNaliModeOptions(t *testing.T) {
9292
}
9393
}
9494

95+
func TestValidateNaliModeOptionsRejectsStartupConflictsFromInputs(t *testing.T) {
96+
tests := []struct {
97+
name string
98+
input naliModeOptionInputs
99+
want string
100+
}{
101+
{name: "deploy", input: naliModeOptionInputs{deploy: true}, want: "--deploy"},
102+
{name: "from", input: naliModeOptionInputs{from: "Germany"}, want: "--from"},
103+
{name: "init", input: naliModeOptionInputs{init: true}, want: "--init"},
104+
{name: "listen", input: naliModeOptionInputs{listen: "127.0.0.1:8080"}, want: "--listen"},
105+
}
106+
107+
for _, tt := range tests {
108+
t.Run(tt.name, func(t *testing.T) {
109+
tt.input.parser = argparse.NewParser("nexttrace", "")
110+
err := validateNaliModeOptions(buildNaliModeOptions(tt.input))
111+
if err == nil || !strings.Contains(err.Error(), tt.want) {
112+
t.Fatalf("validateNaliModeOptions() error = %v, want conflict %q", err, tt.want)
113+
}
114+
})
115+
}
116+
}
117+
95118
func TestBuildNaliModeOptionsDetectsExplicitDefaults(t *testing.T) {
96119
parser := argparse.NewParser("nexttrace", "")
97120
parser.Int("q", "queries", &argparse.Options{Default: 3})

cmd/speed_mode.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,17 @@ import (
1616
speedconfig "github.com/nxtrace/NTrace-core/internal/speedtest/config"
1717
speedrender "github.com/nxtrace/NTrace-core/internal/speedtest/render"
1818
speedrunner "github.com/nxtrace/NTrace-core/internal/speedtest/runner"
19+
"github.com/nxtrace/NTrace-core/wshandle"
1920
)
2021

22+
type speedMetadataBackend interface {
23+
Close()
24+
}
25+
26+
var newSpeedMetadataBackend = func(ctx context.Context) speedMetadataBackend {
27+
return wshandle.NewWithContextAsync(ctx)
28+
}
29+
2130
func registerSpeedFlag(parser *argparse.Parser) *bool {
2231
return registerSpeedFlagWithAvailability(parser, enableSpeed)
2332
}
@@ -49,9 +58,12 @@ func containsSpeedFlag(args []string) bool {
4958
if arg == "--" {
5059
return false
5160
}
52-
if arg == "--speed" || strings.HasPrefix(arg, "--speed=") {
61+
if arg == "--speed" {
5362
return true
5463
}
64+
if v, ok := strings.CutPrefix(arg, "--speed="); ok {
65+
return v == "" || strings.EqualFold(v, "true") || v == "1"
66+
}
5567
}
5668
return false
5769
}
@@ -69,6 +81,10 @@ func runSpeedMode(rawArgs []string, stdout, stderr io.Writer) int {
6981

7082
rootCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
7183
defer stop()
84+
restoreFastIPOutput := suppressSpeedMetadataOutput(cfg)
85+
defer restoreFastIPOutput()
86+
metadataBackend := startSpeedMetadataBackend(rootCtx, cfg)
87+
defer closeSpeedMetadataBackend(metadataBackend)
7288

7389
var bus *speedrender.Bus
7490
isTTY := speedrender.IsTTY()
@@ -80,9 +96,9 @@ func runSpeedMode(rawArgs []string, stdout, stderr io.Writer) int {
8096
} else {
8197
bus = speedrender.NewBus(speedrender.NewPlainRenderer(stderr))
8298
}
99+
defer bus.Close()
83100

84101
res := speedrunner.Run(rootCtx, cfg, bus, isTTY)
85-
bus.Close()
86102
if cfg.OutputJSON {
87103
enc := json.NewEncoder(stdout)
88104
enc.SetEscapeHTML(false)
@@ -93,3 +109,23 @@ func runSpeedMode(rawArgs []string, stdout, stderr io.Writer) int {
93109
}
94110
return res.ExitCode
95111
}
112+
113+
func suppressSpeedMetadataOutput(cfg *speedconfig.Config) func() {
114+
if cfg == nil || cfg.NoMetadata || !cfg.OutputJSON {
115+
return func() {}
116+
}
117+
return setFastIPOutputSuppression(true)
118+
}
119+
120+
func startSpeedMetadataBackend(ctx context.Context, cfg *speedconfig.Config) speedMetadataBackend {
121+
if cfg == nil || cfg.NoMetadata {
122+
return nil
123+
}
124+
return newSpeedMetadataBackend(ctx)
125+
}
126+
127+
func closeSpeedMetadataBackend(backend speedMetadataBackend) {
128+
if backend != nil {
129+
backend.Close()
130+
}
131+
}

cmd/speed_mode_test.go

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cmd
22

33
import (
44
"bytes"
5+
"context"
6+
"crypto/x509"
57
"encoding/json"
68
"io"
79
"net/http"
@@ -13,10 +15,12 @@ import (
1315

1416
"github.com/akamensky/argparse"
1517

18+
speedconfig "github.com/nxtrace/NTrace-core/internal/speedtest/config"
1619
"github.com/nxtrace/NTrace-core/internal/speedtest/netx"
1720
"github.com/nxtrace/NTrace-core/internal/speedtest/provider/apple"
1821
"github.com/nxtrace/NTrace-core/internal/speedtest/provider/cloudflare"
1922
"github.com/nxtrace/NTrace-core/internal/speedtest/result"
23+
"github.com/nxtrace/NTrace-core/util"
2024
)
2125

2226
func TestRegisterSpeedFlagWithAvailabilityEnabledAddsSingleHelpEntry(t *testing.T) {
@@ -79,6 +83,70 @@ func TestRunSpeedModeRejectsUnexpectedTarget(t *testing.T) {
7983
}
8084
}
8185

86+
type fakeSpeedMetadataBackend struct {
87+
closed bool
88+
}
89+
90+
func (f *fakeSpeedMetadataBackend) Close() {
91+
f.closed = true
92+
}
93+
94+
func TestStartSpeedMetadataBackendHonorsNoMetadata(t *testing.T) {
95+
calls := 0
96+
prev := newSpeedMetadataBackend
97+
newSpeedMetadataBackend = func(context.Context) speedMetadataBackend {
98+
calls++
99+
return &fakeSpeedMetadataBackend{}
100+
}
101+
defer func() { newSpeedMetadataBackend = prev }()
102+
103+
if backend := startSpeedMetadataBackend(context.Background(), &speedconfig.Config{NoMetadata: true}); backend != nil {
104+
t.Fatalf("backend = %#v, want nil when metadata is disabled", backend)
105+
}
106+
if calls != 0 {
107+
t.Fatalf("metadata backend calls = %d, want 0", calls)
108+
}
109+
}
110+
111+
func TestStartSpeedMetadataBackendClosesWhenEnabled(t *testing.T) {
112+
fake := &fakeSpeedMetadataBackend{}
113+
prev := newSpeedMetadataBackend
114+
newSpeedMetadataBackend = func(context.Context) speedMetadataBackend {
115+
return fake
116+
}
117+
defer func() { newSpeedMetadataBackend = prev }()
118+
119+
backend := startSpeedMetadataBackend(context.Background(), &speedconfig.Config{})
120+
if backend != fake {
121+
t.Fatalf("backend = %#v, want fake backend", backend)
122+
}
123+
closeSpeedMetadataBackend(backend)
124+
if !fake.closed {
125+
t.Fatal("metadata backend was not closed")
126+
}
127+
}
128+
129+
func TestSuppressSpeedMetadataOutputOnlyForJSONMetadata(t *testing.T) {
130+
orig := util.SuppressFastIPOutput
131+
defer func() { util.SuppressFastIPOutput = orig }()
132+
133+
util.SuppressFastIPOutput = false
134+
restore := suppressSpeedMetadataOutput(&speedconfig.Config{OutputJSON: true})
135+
if !util.SuppressFastIPOutput {
136+
t.Fatal("SuppressFastIPOutput = false, want true for JSON metadata")
137+
}
138+
restore()
139+
if util.SuppressFastIPOutput {
140+
t.Fatal("SuppressFastIPOutput = true after restore, want false")
141+
}
142+
143+
restore = suppressSpeedMetadataOutput(&speedconfig.Config{OutputJSON: true, NoMetadata: true})
144+
if util.SuppressFastIPOutput {
145+
t.Fatal("SuppressFastIPOutput = true, want unchanged when metadata is disabled")
146+
}
147+
restore()
148+
}
149+
82150
func TestRunSpeedModeAppleJSON(t *testing.T) {
83151
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84152
switch r.URL.Path {
@@ -100,16 +168,22 @@ func TestRunSpeedModeAppleJSON(t *testing.T) {
100168

101169
restoreApple := apple.SetBaseForTest(srv.URL)
102170
defer restoreApple()
103-
restoreRoots := netx.SetExtraRootCAsForTest(srv.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs)
171+
restoreRoots := netx.SetExtraRootCAsForTest(testServerRootCAs(srv))
104172
defer restoreRoots()
105173

106174
u, _ := url.Parse(srv.URL)
107175
var stdout, stderr bytes.Buffer
108176
rc := runSpeedMode([]string{"--speed", "--json", "--no-metadata", "--max", "64KiB", "--timeout", "1500", "--threads", "2", "--latency-count", "2", "--endpoint", u.Hostname()}, &stdout, &stderr)
109177
if rc != 0 && rc != 2 {
110-
t.Fatalf("runSpeedMode(apple) rc = %d, stderr=%s", rc, stderr.String())
178+
t.Fatalf("runSpeedMode(apple) rc = %d, want 0 or degraded 2, stderr=%s", rc, stderr.String())
179+
}
180+
res := assertPureJSONSpeedResult(t, stdout.Bytes(), "apple")
181+
if res.ExitCode != rc {
182+
t.Fatalf("JSON exit_code = %d, want rc %d", res.ExitCode, rc)
183+
}
184+
if rc == 2 && !res.Degraded {
185+
t.Fatal("Degraded = false, want true when rc=2")
111186
}
112-
assertPureJSONSpeedResult(t, stdout.Bytes(), "apple")
113187
}
114188

115189
func TestRunSpeedModeCloudflareJSON(t *testing.T) {
@@ -136,18 +210,24 @@ func TestRunSpeedModeCloudflareJSON(t *testing.T) {
136210

137211
restoreCF := cloudflare.SetBaseForTest(srv.URL)
138212
defer restoreCF()
139-
restoreRoots := netx.SetExtraRootCAsForTest(srv.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs)
213+
restoreRoots := netx.SetExtraRootCAsForTest(testServerRootCAs(srv))
140214
defer restoreRoots()
141215

142216
var stdout, stderr bytes.Buffer
143217
rc := runSpeedMode([]string{"--speed", "--speed-provider", "cloudflare", "--json", "--no-metadata", "--max", "64KiB", "--timeout", "1500", "--threads", "2", "--latency-count", "2", "--non-interactive"}, &stdout, &stderr)
144218
if rc != 0 && rc != 2 {
145-
t.Fatalf("runSpeedMode(cloudflare) rc = %d, stderr=%s", rc, stderr.String())
219+
t.Fatalf("runSpeedMode(cloudflare) rc = %d, want 0 or degraded 2, stderr=%s", rc, stderr.String())
220+
}
221+
res := assertPureJSONSpeedResult(t, stdout.Bytes(), "cloudflare")
222+
if res.ExitCode != rc {
223+
t.Fatalf("JSON exit_code = %d, want rc %d", res.ExitCode, rc)
224+
}
225+
if rc == 2 && !res.Degraded {
226+
t.Fatal("Degraded = false, want true when rc=2")
146227
}
147-
assertPureJSONSpeedResult(t, stdout.Bytes(), "cloudflare")
148228
}
149229

150-
func assertPureJSONSpeedResult(t *testing.T, data []byte, providerName string) {
230+
func assertPureJSONSpeedResult(t *testing.T, data []byte, providerName string) result.RunResult {
151231
t.Helper()
152232
if bytes.Contains(data, []byte("preferred API IP")) {
153233
t.Fatalf("speed JSON output should not contain text noise:\n%s", data)
@@ -162,11 +242,28 @@ func assertPureJSONSpeedResult(t *testing.T, data []byte, providerName string) {
162242
if len(res.Rounds) != 4 {
163243
t.Fatalf("len(Rounds) = %d, want 4", len(res.Rounds))
164244
}
245+
return res
246+
}
247+
248+
func testServerRootCAs(srv *httptest.Server) *x509.CertPool {
249+
if transport, ok := srv.Client().Transport.(*http.Transport); ok && transport.TLSClientConfig != nil && transport.TLSClientConfig.RootCAs != nil {
250+
return transport.TLSClientConfig.RootCAs
251+
}
252+
pool := x509.NewCertPool()
253+
pool.AddCert(srv.Certificate())
254+
return pool
165255
}
166256

167257
func TestContainsSpeedFlagSupportsAssignedFormAndRespectsTerminator(t *testing.T) {
168-
if !containsSpeedFlag([]string{"--speed=true"}) {
169-
t.Fatal("containsSpeedFlag(--speed=true) = false, want true")
258+
for _, arg := range []string{"--speed=true", "--speed=True", "--speed=1", "--speed="} {
259+
if !containsSpeedFlag([]string{arg}) {
260+
t.Fatalf("containsSpeedFlag(%s) = false, want true", arg)
261+
}
262+
}
263+
for _, arg := range []string{"--speed=false", "--speed=0", "--speed=no"} {
264+
if containsSpeedFlag([]string{arg}) {
265+
t.Fatalf("containsSpeedFlag(%s) = true, want false", arg)
266+
}
170267
}
171268
if containsSpeedFlag([]string{"--", "--speed"}) {
172269
t.Fatal("containsSpeedFlag should ignore --speed after terminator")

internal/nali/nali.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func (a *Annotator) lookupLabel(ctx context.Context, ip string) string {
216216
shouldCache = true
217217
}
218218
}
219-
if shouldCache {
219+
if shouldCache && label != "" {
220220
a.storeLabel(ip, label)
221221
}
222222
return label

internal/nali/nali_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ func TestAnnotateLineDoesNotCacheLookupErrors(t *testing.T) {
167167
}
168168
}
169169

170+
func TestAnnotateLineDoesNotCacheEmptyGeo(t *testing.T) {
171+
calls := 0
172+
a := New(Config{
173+
Lang: "en",
174+
Source: func(ip string, timeout time.Duration, lang string, maptrace bool) (*ipgeo.IPGeoData, error) {
175+
calls++
176+
if calls == 1 {
177+
return &ipgeo.IPGeoData{}, nil
178+
}
179+
return &ipgeo.IPGeoData{CountryEn: "ok"}, nil
180+
},
181+
})
182+
183+
if got := a.AnnotateLine(context.Background(), "A 8.8.8.8"); got != "A 8.8.8.8" {
184+
t.Fatalf("first AnnotateLine() = %q, want original", got)
185+
}
186+
if got := a.AnnotateLine(context.Background(), "A 8.8.8.8"); got != "A 8.8.8.8 [ok]" {
187+
t.Fatalf("second AnnotateLine() = %q, want annotated", got)
188+
}
189+
if calls != 2 {
190+
t.Fatalf("lookup calls = %d, want 2", calls)
191+
}
192+
}
193+
170194
func TestFindIPSpans(t *testing.T) {
171195
line := "IP:1.1.1.1 [2001:db8::1]:443"
172196
spans := FindIPSpans(line)

0 commit comments

Comments
 (0)