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 <james@tailscale.com>
This commit is contained in:
James Tucker 2024-12-04 12:02:59 -08:00 committed by James Tucker
parent 74069774be
commit 7f9ebc0a83
7 changed files with 140 additions and 1 deletions

View File

@ -493,6 +493,17 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
return nil return nil
} }
// DebugActionBody invokes a debug action with a body parameter, such as
// "debug-force-prefer-derp".
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugActionBody(ctx context.Context, action string, rbody io.Reader) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, rbody)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
return nil
}
// DebugResultJSON invokes a debug action and returns its result as something JSON-able. // DebugResultJSON invokes a debug action and returns its result as something JSON-able.
// These are development tools and subject to change or removal over time. // These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugResultJSON(ctx context.Context, action string) (any, error) { func (lc *LocalClient) DebugResultJSON(ctx context.Context, action string) (any, error) {

View File

@ -175,6 +175,12 @@ var debugCmd = &ffcli.Command{
Exec: localAPIAction("pick-new-derp"), Exec: localAPIAction("pick-new-derp"),
ShortHelp: "Switch to some other random DERP home region for a short time", ShortHelp: "Switch to some other random DERP home region for a short time",
}, },
{
Name: "force-prefer-derp",
ShortUsage: "tailscale debug force-prefer-derp",
Exec: forcePreferDERP,
ShortHelp: "Prefer the given region ID if reachable (until restart, or 0 to clear)",
},
{ {
Name: "force-netmap-update", Name: "force-netmap-update",
ShortUsage: "tailscale debug force-netmap-update", ShortUsage: "tailscale debug force-netmap-update",
@ -577,6 +583,25 @@ func runDERPMap(ctx context.Context, args []string) error {
return nil return nil
} }
func forcePreferDERP(ctx context.Context, args []string) error {
var n int
if len(args) != 1 {
return errors.New("expected exactly one integer argument")
}
n, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("expected exactly one integer argument: %w", err)
}
b, err := json.Marshal(n)
if err != nil {
return fmt.Errorf("failed to marshal DERP region: %w", err)
}
if err := localClient.DebugActionBody(ctx, "force-prefer-derp", bytes.NewReader(b)); err != nil {
return fmt.Errorf("failed to force preferred DERP: %w", err)
}
return nil
}
func localAPIAction(action string) func(context.Context, []string) error { func localAPIAction(action string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error { return func(ctx context.Context, args []string) error {
if len(args) > 0 { if len(args) > 0 {

View File

@ -2920,6 +2920,12 @@ func (b *LocalBackend) DebugPickNewDERP() error {
return b.sys.MagicSock.Get().DebugPickNewDERP() return b.sys.MagicSock.Get().DebugPickNewDERP()
} }
// DebugForcePreferDERP forwards to netcheck.DebugForcePreferDERP.
// See its docs.
func (b *LocalBackend) DebugForcePreferDERP(n int) {
b.sys.MagicSock.Get().DebugForcePreferDERP(n)
}
// send delivers n to the connected frontend and any API watchers from // send delivers n to the connected frontend and any API watchers from
// LocalBackend.WatchNotifications (via the LocalAPI). // LocalBackend.WatchNotifications (via the LocalAPI).
// //

View File

@ -634,6 +634,13 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
} }
case "pick-new-derp": case "pick-new-derp":
err = h.b.DebugPickNewDERP() err = h.b.DebugPickNewDERP()
case "force-prefer-derp":
var n int
err = json.NewDecoder(r.Body).Decode(&n)
if err != nil {
break
}
h.b.DebugForcePreferDERP(n)
case "": case "":
err = fmt.Errorf("missing parameter 'action'") err = fmt.Errorf("missing parameter 'action'")
default: default:

View File

@ -236,6 +236,10 @@ type Client struct {
// If false, the default net.Resolver will be used, with no caching. // If false, the default net.Resolver will be used, with no caching.
UseDNSCache bool UseDNSCache bool
// if non-zero, force this DERP region to be preferred in all reports where
// the DERP is found to be reachable.
ForcePreferredDERP int
// For tests // For tests
testEnoughRegions int testEnoughRegions int
testCaptivePortalDelay time.Duration testCaptivePortalDelay time.Duration
@ -780,6 +784,12 @@ func (o *GetReportOpts) getLastDERPActivity(region int) time.Time {
return o.GetLastDERPActivity(region) return o.GetLastDERPActivity(region)
} }
func (c *Client) SetForcePreferredDERP(region int) {
c.mu.Lock()
defer c.mu.Unlock()
c.ForcePreferredDERP = region
}
// GetReport gets a report. The 'opts' argument is optional and can be nil. // GetReport gets a report. The 'opts' argument is optional and can be nil.
// Callers are discouraged from passing a ctx with an arbitrary deadline as this // Callers are discouraged from passing a ctx with an arbitrary deadline as this
// may cause GetReport to return prematurely before all reporting methods have // may cause GetReport to return prematurely before all reporting methods have
@ -1277,6 +1287,9 @@ func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
if r.CaptivePortal != "" { if r.CaptivePortal != "" {
fmt.Fprintf(w, " captiveportal=%v", r.CaptivePortal) fmt.Fprintf(w, " captiveportal=%v", r.CaptivePortal)
} }
if c.ForcePreferredDERP != 0 {
fmt.Fprintf(w, " force=%v", c.ForcePreferredDERP)
}
fmt.Fprintf(w, " derp=%v", r.PreferredDERP) fmt.Fprintf(w, " derp=%v", r.PreferredDERP)
if r.PreferredDERP != 0 { if r.PreferredDERP != 0 {
fmt.Fprintf(w, " derpdist=") fmt.Fprintf(w, " derpdist=")
@ -1435,6 +1448,21 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report,
// which undoes any region change we made above. // which undoes any region change we made above.
r.PreferredDERP = prevDERP r.PreferredDERP = prevDERP
} }
if c.ForcePreferredDERP != 0 {
// If the forced DERP region probed successfully, or has recent traffic,
// use it.
_, haveLatencySample := r.RegionLatency[c.ForcePreferredDERP]
var recentActivity bool
if lastHeard := rs.opts.getLastDERPActivity(c.ForcePreferredDERP); !lastHeard.IsZero() {
now := c.timeNow()
recentActivity = lastHeard.After(rs.start)
recentActivity = recentActivity || lastHeard.After(now.Add(-PreferredDERPFrameTime))
}
if haveLatencySample || recentActivity {
r.PreferredDERP = c.ForcePreferredDERP
}
}
} }
func updateLatency(m map[int]time.Duration, regionID int, d time.Duration) { func updateLatency(m map[int]time.Duration, regionID int, d time.Duration) {

View File

@ -201,6 +201,7 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) {
steps []step steps []step
homeParams *tailcfg.DERPHomeParams homeParams *tailcfg.DERPHomeParams
opts *GetReportOpts opts *GetReportOpts
forcedDERP int // if non-zero, force this DERP to be the preferred one
wantDERP int // want PreferredDERP on final step wantDERP int // want PreferredDERP on final step
wantPrevLen int // wanted len(c.prev) wantPrevLen int // wanted len(c.prev)
}{ }{
@ -366,12 +367,65 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) {
wantPrevLen: 2, wantPrevLen: 2,
wantDERP: 1, // diff is 11ms, but d2 is greater than 2/3s of d1 wantDERP: 1, // diff is 11ms, but d2 is greater than 2/3s of d1
}, },
{
name: "forced_two",
steps: []step{
{time.Second, report("d1", 2, "d2", 3)},
{2 * time.Second, report("d1", 4, "d2", 3)},
},
forcedDERP: 2,
wantPrevLen: 2,
wantDERP: 2,
},
{
name: "forced_two_unavailable",
steps: []step{
{time.Second, report("d1", 2, "d2", 1)},
{2 * time.Second, report("d1", 4)},
},
forcedDERP: 2,
wantPrevLen: 2,
wantDERP: 1,
},
{
name: "forced_two_no_probe_recent_activity",
steps: []step{
{time.Second, report("d1", 2)},
{2 * time.Second, report("d1", 4)},
},
opts: &GetReportOpts{
GetLastDERPActivity: mkLDAFunc(map[int]time.Time{
1: startTime,
2: startTime.Add(time.Second),
}),
},
forcedDERP: 2,
wantPrevLen: 2,
wantDERP: 2,
},
{
name: "forced_two_no_probe_no_recent_activity",
steps: []step{
{time.Second, report("d1", 2)},
{PreferredDERPFrameTime + time.Second, report("d1", 4)},
},
opts: &GetReportOpts{
GetLastDERPActivity: mkLDAFunc(map[int]time.Time{
1: startTime,
2: startTime,
}),
},
forcedDERP: 2,
wantPrevLen: 2,
wantDERP: 1,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
fakeTime := startTime fakeTime := startTime
c := &Client{ c := &Client{
TimeNow: func() time.Time { return fakeTime }, TimeNow: func() time.Time { return fakeTime },
ForcePreferredDERP: tt.forcedDERP,
} }
dm := &tailcfg.DERPMap{HomeParams: tt.homeParams} dm := &tailcfg.DERPMap{HomeParams: tt.homeParams}
rs := &reportState{ rs := &reportState{

View File

@ -3013,6 +3013,14 @@ func (c *Conn) DebugPickNewDERP() error {
return errors.New("too few regions") return errors.New("too few regions")
} }
func (c *Conn) DebugForcePreferDERP(n int) {
c.mu.Lock()
defer c.mu.Unlock()
c.logf("magicsock: [debug] force preferred DERP set to: %d", n)
c.netChecker.SetForcePreferredDERP(n)
}
// portableTrySetSocketBuffer sets SO_SNDBUF and SO_RECVBUF on pconn to socketBufferSize, // portableTrySetSocketBuffer sets SO_SNDBUF and SO_RECVBUF on pconn to socketBufferSize,
// logging an error if it occurs. // logging an error if it occurs.
func portableTrySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) { func portableTrySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {