diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 3dc821c10..3a429a82a 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -139,6 +139,11 @@ var debugCmd = &ffcli.Command{ Exec: localAPIAction("break-derp-conns"), ShortHelp: "break any open DERP connections from the daemon", }, + { + Name: "pick-new-derp", + Exec: localAPIAction("pick-new-derp"), + ShortHelp: "switch to some other random DERP home region for a short time", + }, { Name: "force-netmap-update", Exec: localAPIAction("force-netmap-update"), diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 6d9d21e5a..e948ba953 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2156,6 +2156,12 @@ func (b *LocalBackend) DebugForceNetmapUpdate() { b.setNetMapLocked(nm) } +// DebugPickNewDERP forwards to magicsock.Conn.DebugPickNewDERP. +// See its docs. +func (b *LocalBackend) DebugPickNewDERP() error { + return b.sys.MagicSock.Get().DebugPickNewDERP() +} + // send delivers n to the connected frontend and any API watchers from // LocalBackend.WatchNotifications (via the LocalAPI). // diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index e68e6adaf..a25c8e717 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -566,6 +566,8 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) { if err == nil { return } + case "pick-new-derp": + err = h.b.DebugPickNewDERP() case "": err = fmt.Errorf("missing parameter 'action'") default: diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 016061c21..4e03f014a 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -2715,6 +2715,33 @@ func (c *Conn) getPinger() *ping.Pinger { }) } +// DebugPickNewDERP picks a new DERP random home temporarily (even if just for +// seconds) and reports it to control. It exists to test DERP home changes and +// netmap deltas, etc. It serves no useful user purpose. +func (c *Conn) DebugPickNewDERP() error { + c.mu.Lock() + defer c.mu.Unlock() + dm := c.derpMap + if dm == nil { + return errors.New("no derpmap") + } + if c.netInfoLast == nil { + return errors.New("no netinfo") + } + for _, r := range dm.Regions { + if r.RegionID == c.myDerp { + continue + } + c.logf("magicsock: [debug] switching derp home to random %v (%v)", r.RegionID, r.RegionCode) + go c.setNearestDERP(r.RegionID) + ni2 := c.netInfoLast.Clone() + ni2.PreferredDERP = r.RegionID + c.callNetInfoCallbackLocked(ni2) + return nil + } + return errors.New("too few regions") +} + // 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) {