summaryrefslogtreecommitdiffhomepage
path: root/ipn
diff options
context:
space:
mode:
authorAndrew Dunham <andrew@du.nham.ca>2022-10-18 17:29:54 -0400
committerAndrew Dunham <andrew@du.nham.ca>2022-10-21 11:08:39 -0400
commitd8b9698eaaaf40ea857ed888ffe5b7ac99575c8f (patch)
tree6831136356d8ef3006b6736a37ec48e8697fa576 /ipn
parent95f630ced019a6d90b28ad2e682740fe3cb1830c (diff)
downloadtailscale-andrew/debug-subnet-router.tar.xz
tailscale-andrew/debug-subnet-router.zip
Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Change-Id: Id806c5c62b5097d9a5a7600324349ce7692d4d55
Diffstat (limited to 'ipn')
-rw-r--r--ipn/ipnlocal/local.go188
-rw-r--r--ipn/localapi/localapi.go21
2 files changed, 209 insertions, 0 deletions
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index e6ac4701f..ea55bb31e 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -25,6 +25,7 @@ import (
"go4.org/netipx"
"golang.org/x/exp/slices"
+ "golang.org/x/sync/errgroup"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/doctor"
@@ -3724,6 +3725,193 @@ func (b *LocalBackend) DebugReSTUN() error {
return nil
}
+func (b *LocalBackend) DebugSubnetRoute(ctx context.Context, addr string) (*apitype.SubnetRouteDebugResponse, error) {
+ b.mu.Lock()
+ nm := b.netMap
+ b.mu.Unlock()
+
+ if nm == nil {
+ return nil, errors.New("no netmap")
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ out := &apitype.SubnetRouteDebugResponse{
+ InputAddr: addr,
+ }
+
+ returnWithError := func(format string, args ...any) (*apitype.SubnetRouteDebugResponse, error) {
+ if len(format) > 0 && format[len(format)-1] != '\n' {
+ format += "\n"
+ }
+ out.Errors = append(out.Errors, fmt.Sprintf(format, args...))
+ return out, nil
+ }
+
+ ip, err := netip.ParseAddr(addr)
+ if err != nil {
+ // Try resolving the address using both the Go and platform-specific resolver
+ var addrs []apitype.SubnetRouteDebugAddress
+ for _, preferGo := range []bool{true, false} {
+ resolver := net.Resolver{PreferGo: preferGo}
+ ips, err := resolver.LookupNetIP(ctx, "ip", addr)
+ if err != nil {
+ return returnWithError("error resolving address %q: %v", addr, err)
+ }
+ for _, ip := range ips {
+ addrs = append(addrs, apitype.SubnetRouteDebugAddress{
+ Addr: ip.Unmap(),
+ Source: fmt.Sprintf("net.Resolver{PreferGo: %v}", preferGo),
+ })
+ }
+ }
+
+ // Pick the first IP, since we always expect it.
+ // TODO: try all IPs?
+ out.Addresses = addrs
+ ip = addrs[0].Addr
+ } else {
+ out.Addresses = []apitype.SubnetRouteDebugAddress{{
+ Addr: ip,
+ Source: "user",
+ }}
+ }
+ ip = ip.Unmap()
+
+ // Try to determine which subnet router is routing this address.
+ type nodeWithMatching struct {
+ *tailcfg.Node
+ MatchingAllowedIPs []netip.Prefix
+ }
+ var ns []nodeWithMatching
+ for _, peer := range nm.Peers {
+ curr := nodeWithMatching{Node: peer}
+ for _, allowedip := range peer.AllowedIPs {
+ if !allowedip.Contains(ip) {
+ continue
+ }
+ curr.MatchingAllowedIPs = append(curr.MatchingAllowedIPs, allowedip)
+ }
+
+ if len(curr.MatchingAllowedIPs) > 0 {
+ ns = append(ns, curr)
+ }
+ }
+ if len(ns) == 0 {
+ return returnWithError("this node has no peers advertising a route for %s", ip)
+ }
+
+ // For each possible subnet router, check the status.
+ type nodeRes struct {
+ dn apitype.SubnetRouteDebugNode
+ err error
+ }
+ nodeResults := make(chan nodeRes, len(ns))
+ grp, grpCtx := errgroup.WithContext(ctx)
+ grp.SetLimit(5)
+ for _, node := range ns {
+ node := node // capture loop variable
+ grp.Go(func() error {
+ var retErr error
+ dn := apitype.SubnetRouteDebugNode{
+ StableID: node.StableID,
+ Name: node.Name,
+ AllowedIPs: node.MatchingAllowedIPs,
+ }
+ defer func() {
+ nodeResults <- nodeRes{dn, retErr}
+ }()
+
+ // Check PrimaryRoutes
+ for _, pref := range node.PrimaryRoutes {
+ if pref.Contains(ip) {
+ dn.Primary = append(dn.Primary, pref)
+ }
+ }
+
+ // Check for exit node
+ if tsaddr.ContainsExitRoutes(node.AllowedIPs) {
+ dn.IsExitNode = true
+ }
+
+ // Do online checks after gathering all data that doesn't
+ // require an interaction, so we can 'continue' if the node
+ // isn't online.
+ if node.Online == nil {
+ dn.Online = "unknown"
+ return nil
+ } else if !*node.Online {
+ dn.Online = "false"
+ return nil
+ } else {
+ dn.Online = "true"
+ }
+
+ // Try pinging the node itself
+ // TODO: try all IPs?
+ // TODO: check if we have the right v4/v6 address support
+ candidates := []struct {
+ ty tailcfg.PingType
+ res **apitype.SubnetRouteDebugPingResponse
+ }{
+ {tailcfg.PingDisco, &dn.DiscoPing},
+ {tailcfg.PingICMP, &dn.ICMPPing},
+ }
+ for _, cand := range candidates {
+ ip := node.Addresses[0].Addr()
+
+ res := &apitype.SubnetRouteDebugPingResponse{
+ IP: ip,
+ }
+ *cand.res = res
+
+ pingRes := make(chan *ipnstate.PingResult, 1)
+ b.e.Ping(ip, cand.ty, func(pr *ipnstate.PingResult) {
+ select {
+ case pingRes <- pr:
+ default:
+ }
+ })
+ select {
+ case pr := <-pingRes:
+ if pr.Err != "" {
+ res.Err = pr.Err
+ } else {
+ res.LatencySeconds = pr.LatencySeconds
+ }
+
+ case <-grpCtx.Done():
+ res.Err = grpCtx.Err().Error()
+ retErr = fmt.Errorf("context canceled while waiting for %s response from %v: %v", cand.ty, ip, grpCtx.Err())
+ return nil
+ }
+ }
+ return nil
+ })
+ }
+ grp.Wait()
+
+resultsLoop:
+ for i := 0; i < len(ns); i++ {
+ select {
+ case res := <-nodeResults:
+ if res.err != nil {
+ out.Errors = append(out.Errors, res.err.Error())
+ }
+ out.Nodes = append(out.Nodes, res.dn)
+
+ // We could have finished before starting all goroutines, so we
+ // need to handle the case where our channel doesn't have all
+ // the responses.
+ default:
+ break resultsLoop
+ }
+ }
+
+ return out, nil
+}
+
func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
ig, ok := b.e.(wgengine.InternalsGetter)
if !ok {
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index bfdd6a15a..fe34fdc44 100644
--- a/ipn/localapi/localapi.go
+++ b/ipn/localapi/localapi.go
@@ -59,6 +59,7 @@ var handler = map[string]localAPIHandler{
"check-prefs": (*Handler).serveCheckPrefs,
"component-debug-logging": (*Handler).serveComponentDebugLogging,
"debug": (*Handler).serveDebug,
+ "debug-subnet-route": (*Handler).serveDebugSubnetRoute,
"derpmap": (*Handler).serveDERPMap,
"dial": (*Handler).serveDial,
"file-targets": (*Handler).serveFileTargets,
@@ -417,6 +418,26 @@ func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Requ
json.NewEncoder(w).Encode(res)
}
+func (h *Handler) serveDebugSubnetRoute(w http.ResponseWriter, r *http.Request) {
+ if !h.PermitWrite {
+ http.Error(w, "debug access denied", http.StatusForbidden)
+ return
+ }
+ addr := r.FormValue("addr")
+ res, err := h.b.DebugSubnetRoute(r.Context(), addr)
+ w.Header().Set("Content-Type", "application/json")
+ if err != nil {
+ json.NewEncoder(w).Encode(struct {
+ Errors []string
+ }{
+ Errors: []string{err.Error()},
+ })
+ return
+ }
+
+ json.NewEncoder(w).Encode(res)
+}
+
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
// for platforms where we want to link it in.
var serveProfileFunc func(http.ResponseWriter, *http.Request)