From b9a4630679ea6dfe67306c6d6a4d11466dd5ca44 Mon Sep 17 00:00:00 2001 From: Don O'Neill Date: Sat, 3 Oct 2026 14:37:44 -0700 Subject: [PATCH 1/2] cmd/tailcat: test perf over a path that stays relayed With TS_DEBUG_ALWAYS_USE_DERP on the client and a whole-second --timeout, perf --via-derp should run over the relay, and perf without it should refuse with "no direct path". Both currently fail with "no reply to pings after 2s". Signed-off-by: Don O'Neill --- cmd/tailcat/perf_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cmd/tailcat/perf_test.go b/cmd/tailcat/perf_test.go index 243a07421..9e365dcf8 100644 --- a/cmd/tailcat/perf_test.go +++ b/cmd/tailcat/perf_test.go @@ -6,6 +6,7 @@ package main import ( "encoding/json" "regexp" + "slices" "strings" "testing" "time" @@ -97,6 +98,33 @@ func TestPerf(t *testing.T) { t.Errorf("no RTT samples in %+v", got.Result) } }) + + // A path that stays relayed must still come back from the wait for + // a direct path, whatever --timeout is. Whole seconds used to fail: + // the last once-a-second ping saw about 1s left, slept past the + // deadline, and reported "no reply to pings". + t.Run("relayed", func(t *testing.T) { + relayedPerf := func(args ...string) ([]byte, error) { + all := append([]string{"--key=new", "--derpmap-url=" + e.derpMapURL, "perf", "--timeout=2s", "--time=1s", "--interval=0"}, args...) + all = append(all, addr) + cmd := e.cmd(all...) + cmd.Env = append(slices.Clone(cmd.Env), "TS_DEBUG_ALWAYS_USE_DERP=1") + return cmd.CombinedOutput() + } + + out, err := relayedPerf("--via-derp") + if err != nil { + t.Fatalf("perf --via-derp: %v\n%s", err, out) + } + if !regexp.MustCompile(`(?m)^# path: relayed via DERP\(`).Match(out) { + t.Errorf("output missing relayed path:\n%s", out) + } + + out, err = relayedPerf() + if err == nil || !strings.Contains(string(out), "no direct path to the server") { + t.Errorf("perf without --via-derp: err=%v, want a no-direct-path refusal:\n%s", err, out) + } + }) } // TestPerfServeRejectsPortClash checks that a port list can't proxy From e445fe4a15200308aaf2500c261db36c06f0851d Mon Sep 17 00:00:00 2001 From: Don O'Neill Date: Sat, 3 Oct 2026 15:01:14 -0700 Subject: [PATCH 2/2] cmd/tailcat: return the relayed path when perf's path wait times out waitForDirectPath returned a non-direct path only when under 0.5s of its timeout remained. Pongs arrive in milliseconds, so with a whole-second --timeout (the 10s default included) the last ping saw about 1s left, slept past the deadline, and the next one failed as "no reply to pings". A path that stayed relayed never came back: --via-derp never ran a test, and without it the "no direct path" refusal was never shown. Keep the last path measured and return it at the deadline, as the function's doc comment already says. "No reply to pings" is now only reported when nothing replied. Fixes #144 Signed-off-by: Don O'Neill --- cmd/tailcat/perf.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/tailcat/perf.go b/cmd/tailcat/perf.go index dc50e8970..565740794 100644 --- a/cmd/tailcat/perf.go +++ b/cmd/tailcat/perf.go @@ -191,6 +191,7 @@ func probePath(ctx context.Context, cl *tailcat.Client) (pathInfo, error) { // discovery along. func waitForDirectPath(ctx context.Context, cl *tailcat.Client, timeout time.Duration) (pathInfo, error) { deadline := time.Now().Add(timeout) + var last *pathInfo for { t0 := time.Now() pingCtx, cancel := context.WithDeadline(ctx, deadline) @@ -198,6 +199,9 @@ func waitForDirectPath(ctx context.Context, cl *tailcat.Client, timeout time.Dur cancel() if err != nil { if ctx.Err() == nil && errors.Is(err, context.DeadlineExceeded) { + if last != nil { + return *last, nil + } return pathInfo{}, fmt.Errorf("no reply to pings after %v", timeout) } return pathInfo{}, err @@ -205,6 +209,7 @@ func waitForDirectPath(ctx context.Context, cl *tailcat.Client, timeout time.Dur if p.Direct || time.Until(deadline) < time.Second/2 { return p, nil } + last = &p select { case <-ctx.Done(): return pathInfo{}, ctx.Err()