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
}
// 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.
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugResultJSON(ctx context.Context, action string) (any, error) {

View File

@ -175,6 +175,12 @@
Exec: localAPIAction("pick-new-derp"),
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",
ShortUsage: "tailscale debug force-netmap-update",
@ -577,6 +583,25 @@ func runDERPMap(ctx context.Context, args []string) error {
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 {
return func(ctx context.Context, args []string) error {
if len(args) > 0 {

View File

@ -2920,6 +2920,12 @@ func (b *LocalBackend) DebugPickNewDERP() error {
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
// 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":
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 "":
err = fmt.Errorf("missing parameter 'action'")
default:

View File

@ -236,6 +236,10 @@ type Client struct {
// If false, the default net.Resolver will be used, with no caching.
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
testEnoughRegions int
testCaptivePortalDelay time.Duration
@ -780,6 +784,12 @@ func (o *GetReportOpts) getLastDERPActivity(region int) time.Time {
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.
// Callers are discouraged from passing a ctx with an arbitrary deadline as this
// 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 != "" {
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)
if r.PreferredDERP != 0 {
fmt.Fprintf(w, " derpdist=")
@ -1435,6 +1448,21 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report,
// which undoes any region change we made above.
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) {

View File

@ -201,6 +201,7 @@ type step struct {
steps []step
homeParams *tailcfg.DERPHomeParams
opts *GetReportOpts
forcedDERP int // if non-zero, force this DERP to be the preferred one
wantDERP int // want PreferredDERP on final step
wantPrevLen int // wanted len(c.prev)
}{
@ -366,12 +367,65 @@ type step struct {
wantPrevLen: 2,
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 {
t.Run(tt.name, func(t *testing.T) {
fakeTime := startTime
c := &Client{
TimeNow: func() time.Time { return fakeTime },
TimeNow: func() time.Time { return fakeTime },
ForcePreferredDERP: tt.forcedDERP,
}
dm := &tailcfg.DERPMap{HomeParams: tt.homeParams}
rs := &reportState{

View File

@ -3013,6 +3013,14 @@ func (c *Conn) DebugPickNewDERP() error {
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,
// logging an error if it occurs.
func portableTrySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {