From 109aa3b2fb5020bdc60e9c0991b9d6f0c9ad846e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 26 Nov 2022 14:23:00 -0800 Subject: [PATCH] cmd/tailscale: add start of "debug derp" subcommand Updates #6526 Change-Id: I84e440a8bd837c383000ce0cec4ff36b24249e8b Signed-off-by: Brad Fitzpatrick --- client/tailscale/localclient.go | 9 ++++ cmd/tailscale/cli/debug.go | 18 +++++++ cmd/tailscale/depaware.txt | 1 + ipn/ipnstate/ipnstate.go | 8 +++ ipn/localapi/debugderp.go | 92 +++++++++++++++++++++++++++++++++ ipn/localapi/localapi.go | 1 + 6 files changed, 129 insertions(+) create mode 100644 ipn/localapi/debugderp.go diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 8de7a7164..f058277ca 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -981,6 +981,15 @@ func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID) return err } +func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) { + v := url.Values{"region": {regionIDOrCode}} + body, err := lc.send(ctx, "POST", "/localapi/v0/debug-derp-region?"+v.Encode(), 200, nil) + if err != nil { + return nil, fmt.Errorf("error %w: %s", err, body) + } + return decodeJSON[*ipnstate.DebugDERPRegionReport](body) +} + // WatchIPNMask are filtering options for LocalClient.WatchIPNBus. // // The zero value is a valid WatchOpt that means to watch everything. diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 3e679958a..9fea75f37 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -37,6 +37,7 @@ "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/util/must" "tailscale.com/util/strs" ) @@ -160,6 +161,11 @@ return fs })(), }, + { + Name: "derp", + Exec: runDebugDERP, + ShortHelp: "test a DERP configuration", + }, }, } @@ -610,3 +616,15 @@ func runDevStoreSet(ctx context.Context, args []string) error { } return localClient.SetDevStoreKeyValue(ctx, key, val) } + +func runDebugDERP(ctx context.Context, args []string) error { + if len(args) != 1 { + return errors.New("usage: debug derp ") + } + st, err := localClient.DebugDERPRegion(ctx, args[0]) + if err != nil { + return err + } + fmt.Printf("%s\n", must.Get(json.MarshalIndent(st, "", " "))) + return nil +} diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index d2ec5da56..ff565ccc9 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -108,6 +108,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/util/lineread from tailscale.com/net/interfaces+ tailscale.com/util/mak from tailscale.com/net/netcheck+ tailscale.com/util/multierr from tailscale.com/control/controlhttp + tailscale.com/util/must from tailscale.com/cmd/tailscale/cli tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli tailscale.com/util/singleflight from tailscale.com/net/dnscache tailscale.com/util/strs from tailscale.com/hostinfo+ diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 7b691628b..45399935c 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -618,3 +618,11 @@ func sortKey(ps *PeerStatus) string { raw := ps.PublicKey.Raw32() return string(raw[:]) } + +// DebugDERPRegionReport is the result of a "tailscale debug derp" command, +// to let people debug a custom DERP setup. +type DebugDERPRegionReport struct { + Info []string + Warnings []string + Errors []string +} diff --git a/ipn/localapi/debugderp.go b/ipn/localapi/debugderp.go new file mode 100644 index 000000000..4a598f261 --- /dev/null +++ b/ipn/localapi/debugderp.go @@ -0,0 +1,92 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package localapi + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" +) + +func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) { + if !h.PermitWrite { + http.Error(w, "debug access denied", http.StatusForbidden) + return + } + if r.Method != "POST" { + http.Error(w, "POST required", http.StatusMethodNotAllowed) + return + } + var st ipnstate.DebugDERPRegionReport + defer func() { + j, _ := json.Marshal(st) + w.Header().Set("Content-Type", "application/json") + w.Write(j) + }() + + dm := h.b.DERPMap() + if dm == nil { + st.Errors = append(st.Errors, "no DERP map (not connected?)") + return + } + regStr := r.FormValue("region") + var reg *tailcfg.DERPRegion + if id, err := strconv.Atoi(regStr); err == nil { + reg = dm.Regions[id] + } else { + for _, r := range dm.Regions { + if r.RegionCode == regStr { + reg = r + break + } + } + } + if reg == nil { + st.Errors = append(st.Errors, fmt.Sprintf("no such region %q in DERP map", regStr)) + return + } + st.Info = append(st.Info, fmt.Sprintf("Region %v == %q", reg.RegionID, reg.RegionCode)) + + if reg.Avoid { + st.Warnings = append(st.Warnings, "Region is marked with Avoid bit") + } + if len(reg.Nodes) == 0 { + st.Errors = append(st.Errors, "Region has no nodes defined") + return + } + + // TODO(bradfitz): finish: + // * first try TCP connection + // * reconnect 4 or 5 times; see if we ever get a different server key. + // if so, they're load balancing the wrong way. error. + // * try to DERP auth with new public key. + // * if rejected, add Info that it's likely the DERP server authz is on, + // try with LocalBackend's node key instead. + // * if they have more then one node, try to relay a packet between them + // and see if it works (like cmd/derpprobe). But if server authz is on, + // we won't be able to, so just warn. Say to turn that off, try again, + // then turn it back on. TODO(bradfitz): maybe add a debug frame to DERP + // protocol to say how many peers it's meshed with. Should match count + // in DERPRegion. Or maybe even list all their server pub keys that it's peered + // with. + // * try STUN queries + // * warn about IPv6 only + // * If their certificate is bad, either expired or just wrongly + // issued in the first place, tell them specifically that the + // cert is bad not just that the connection failed. + // * If /generate_204 on port 80 cannot be reached, warn + // that they won't get captive portal detection and + // should allow port 80. + // * If they have exactly one DERP region because they + // removed all of Tailscale's DERPs, warn that they have + // a SPOF that will hamper even direct connections from + // working. (warning, not error, as that's probably a likely + // config for headscale users) + st.Info = append(st.Info, "TODO: 🦉") +} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 0e401f770..1b528595d 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -65,6 +65,7 @@ "check-prefs": (*Handler).serveCheckPrefs, "component-debug-logging": (*Handler).serveComponentDebugLogging, "debug": (*Handler).serveDebug, + "debug-derp-region": (*Handler).serveDebugDERPRegion, "derpmap": (*Handler).serveDERPMap, "dev-set-state-store": (*Handler).serveDevSetStateStore, "dial": (*Handler).serveDial,