Skip to content

Commit 7f9ebc0

Browse files
committed
cmd/tailscale,net/netcheck: add debug feature to force preferred DERP
This provides an interface for a user to force a preferred DERP outcome for all future netchecks that will take precedence unless the forced region is unreachable. The option does not persist and will be lost when the daemon restarts. Updates tailscale/corp#18997 Updates tailscale/corp#24755 Signed-off-by: James Tucker <[email protected]>
1 parent 7406977 commit 7f9ebc0

File tree

7 files changed

+140
-1
lines changed

7 files changed

+140
-1
lines changed

client/tailscale/localclient.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,17 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
493493
return nil
494494
}
495495

496+
// DebugActionBody invokes a debug action with a body parameter, such as
497+
// "debug-force-prefer-derp".
498+
// These are development tools and subject to change or removal over time.
499+
func (lc *LocalClient) DebugActionBody(ctx context.Context, action string, rbody io.Reader) error {
500+
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, rbody)
501+
if err != nil {
502+
return fmt.Errorf("error %w: %s", err, body)
503+
}
504+
return nil
505+
}
506+
496507
// DebugResultJSON invokes a debug action and returns its result as something JSON-able.
497508
// These are development tools and subject to change or removal over time.
498509
func (lc *LocalClient) DebugResultJSON(ctx context.Context, action string) (any, error) {

cmd/tailscale/cli/debug.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ var debugCmd = &ffcli.Command{
175175
Exec: localAPIAction("pick-new-derp"),
176176
ShortHelp: "Switch to some other random DERP home region for a short time",
177177
},
178+
{
179+
Name: "force-prefer-derp",
180+
ShortUsage: "tailscale debug force-prefer-derp",
181+
Exec: forcePreferDERP,
182+
ShortHelp: "Prefer the given region ID if reachable (until restart, or 0 to clear)",
183+
},
178184
{
179185
Name: "force-netmap-update",
180186
ShortUsage: "tailscale debug force-netmap-update",
@@ -577,6 +583,25 @@ func runDERPMap(ctx context.Context, args []string) error {
577583
return nil
578584
}
579585

586+
func forcePreferDERP(ctx context.Context, args []string) error {
587+
var n int
588+
if len(args) != 1 {
589+
return errors.New("expected exactly one integer argument")
590+
}
591+
n, err := strconv.Atoi(args[0])
592+
if err != nil {
593+
return fmt.Errorf("expected exactly one integer argument: %w", err)
594+
}
595+
b, err := json.Marshal(n)
596+
if err != nil {
597+
return fmt.Errorf("failed to marshal DERP region: %w", err)
598+
}
599+
if err := localClient.DebugActionBody(ctx, "force-prefer-derp", bytes.NewReader(b)); err != nil {
600+
return fmt.Errorf("failed to force preferred DERP: %w", err)
601+
}
602+
return nil
603+
}
604+
580605
func localAPIAction(action string) func(context.Context, []string) error {
581606
return func(ctx context.Context, args []string) error {
582607
if len(args) > 0 {

ipn/ipnlocal/local.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2920,6 +2920,12 @@ func (b *LocalBackend) DebugPickNewDERP() error {
29202920
return b.sys.MagicSock.Get().DebugPickNewDERP()
29212921
}
29222922

2923+
// DebugForcePreferDERP forwards to netcheck.DebugForcePreferDERP.
2924+
// See its docs.
2925+
func (b *LocalBackend) DebugForcePreferDERP(n int) {
2926+
b.sys.MagicSock.Get().DebugForcePreferDERP(n)
2927+
}
2928+
29232929
// send delivers n to the connected frontend and any API watchers from
29242930
// LocalBackend.WatchNotifications (via the LocalAPI).
29252931
//

ipn/localapi/localapi.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,13 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
634634
}
635635
case "pick-new-derp":
636636
err = h.b.DebugPickNewDERP()
637+
case "force-prefer-derp":
638+
var n int
639+
err = json.NewDecoder(r.Body).Decode(&n)
640+
if err != nil {
641+
break
642+
}
643+
h.b.DebugForcePreferDERP(n)
637644
case "":
638645
err = fmt.Errorf("missing parameter 'action'")
639646
default:

net/netcheck/netcheck.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ type Client struct {
236236
// If false, the default net.Resolver will be used, with no caching.
237237
UseDNSCache bool
238238

239+
// if non-zero, force this DERP region to be preferred in all reports where
240+
// the DERP is found to be reachable.
241+
ForcePreferredDERP int
242+
239243
// For tests
240244
testEnoughRegions int
241245
testCaptivePortalDelay time.Duration
@@ -780,6 +784,12 @@ func (o *GetReportOpts) getLastDERPActivity(region int) time.Time {
780784
return o.GetLastDERPActivity(region)
781785
}
782786

787+
func (c *Client) SetForcePreferredDERP(region int) {
788+
c.mu.Lock()
789+
defer c.mu.Unlock()
790+
c.ForcePreferredDERP = region
791+
}
792+
783793
// GetReport gets a report. The 'opts' argument is optional and can be nil.
784794
// Callers are discouraged from passing a ctx with an arbitrary deadline as this
785795
// may cause GetReport to return prematurely before all reporting methods have
@@ -1277,6 +1287,9 @@ func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
12771287
if r.CaptivePortal != "" {
12781288
fmt.Fprintf(w, " captiveportal=%v", r.CaptivePortal)
12791289
}
1290+
if c.ForcePreferredDERP != 0 {
1291+
fmt.Fprintf(w, " force=%v", c.ForcePreferredDERP)
1292+
}
12801293
fmt.Fprintf(w, " derp=%v", r.PreferredDERP)
12811294
if r.PreferredDERP != 0 {
12821295
fmt.Fprintf(w, " derpdist=")
@@ -1435,6 +1448,21 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report,
14351448
// which undoes any region change we made above.
14361449
r.PreferredDERP = prevDERP
14371450
}
1451+
if c.ForcePreferredDERP != 0 {
1452+
// If the forced DERP region probed successfully, or has recent traffic,
1453+
// use it.
1454+
_, haveLatencySample := r.RegionLatency[c.ForcePreferredDERP]
1455+
var recentActivity bool
1456+
if lastHeard := rs.opts.getLastDERPActivity(c.ForcePreferredDERP); !lastHeard.IsZero() {
1457+
now := c.timeNow()
1458+
recentActivity = lastHeard.After(rs.start)
1459+
recentActivity = recentActivity || lastHeard.After(now.Add(-PreferredDERPFrameTime))
1460+
}
1461+
1462+
if haveLatencySample || recentActivity {
1463+
r.PreferredDERP = c.ForcePreferredDERP
1464+
}
1465+
}
14381466
}
14391467

14401468
func updateLatency(m map[int]time.Duration, regionID int, d time.Duration) {

net/netcheck/netcheck_test.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) {
201201
steps []step
202202
homeParams *tailcfg.DERPHomeParams
203203
opts *GetReportOpts
204+
forcedDERP int // if non-zero, force this DERP to be the preferred one
204205
wantDERP int // want PreferredDERP on final step
205206
wantPrevLen int // wanted len(c.prev)
206207
}{
@@ -366,12 +367,65 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) {
366367
wantPrevLen: 2,
367368
wantDERP: 1, // diff is 11ms, but d2 is greater than 2/3s of d1
368369
},
370+
{
371+
name: "forced_two",
372+
steps: []step{
373+
{time.Second, report("d1", 2, "d2", 3)},
374+
{2 * time.Second, report("d1", 4, "d2", 3)},
375+
},
376+
forcedDERP: 2,
377+
wantPrevLen: 2,
378+
wantDERP: 2,
379+
},
380+
{
381+
name: "forced_two_unavailable",
382+
steps: []step{
383+
{time.Second, report("d1", 2, "d2", 1)},
384+
{2 * time.Second, report("d1", 4)},
385+
},
386+
forcedDERP: 2,
387+
wantPrevLen: 2,
388+
wantDERP: 1,
389+
},
390+
{
391+
name: "forced_two_no_probe_recent_activity",
392+
steps: []step{
393+
{time.Second, report("d1", 2)},
394+
{2 * time.Second, report("d1", 4)},
395+
},
396+
opts: &GetReportOpts{
397+
GetLastDERPActivity: mkLDAFunc(map[int]time.Time{
398+
1: startTime,
399+
2: startTime.Add(time.Second),
400+
}),
401+
},
402+
forcedDERP: 2,
403+
wantPrevLen: 2,
404+
wantDERP: 2,
405+
},
406+
{
407+
name: "forced_two_no_probe_no_recent_activity",
408+
steps: []step{
409+
{time.Second, report("d1", 2)},
410+
{PreferredDERPFrameTime + time.Second, report("d1", 4)},
411+
},
412+
opts: &GetReportOpts{
413+
GetLastDERPActivity: mkLDAFunc(map[int]time.Time{
414+
1: startTime,
415+
2: startTime,
416+
}),
417+
},
418+
forcedDERP: 2,
419+
wantPrevLen: 2,
420+
wantDERP: 1,
421+
},
369422
}
370423
for _, tt := range tests {
371424
t.Run(tt.name, func(t *testing.T) {
372425
fakeTime := startTime
373426
c := &Client{
374-
TimeNow: func() time.Time { return fakeTime },
427+
TimeNow: func() time.Time { return fakeTime },
428+
ForcePreferredDERP: tt.forcedDERP,
375429
}
376430
dm := &tailcfg.DERPMap{HomeParams: tt.homeParams}
377431
rs := &reportState{

wgengine/magicsock/magicsock.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3013,6 +3013,14 @@ func (c *Conn) DebugPickNewDERP() error {
30133013
return errors.New("too few regions")
30143014
}
30153015

3016+
func (c *Conn) DebugForcePreferDERP(n int) {
3017+
c.mu.Lock()
3018+
defer c.mu.Unlock()
3019+
3020+
c.logf("magicsock: [debug] force preferred DERP set to: %d", n)
3021+
c.netChecker.SetForcePreferredDERP(n)
3022+
}
3023+
30163024
// portableTrySetSocketBuffer sets SO_SNDBUF and SO_RECVBUF on pconn to socketBufferSize,
30173025
// logging an error if it occurs.
30183026
func portableTrySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {

0 commit comments

Comments
 (0)