mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-08 09:07:44 +00:00
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:
parent
74069774be
commit
7f9ebc0a83
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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).
|
||||
//
|
||||
|
@ -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:
|
||||
|
@ -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) {
|
||||
|
@ -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{
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user