From d3574a350f5a73363cf8eaf373f2afea3b5de339 Mon Sep 17 00:00:00 2001 From: Andrew Dunham Date: Fri, 5 Jan 2024 11:28:09 -0500 Subject: [PATCH] cmd/tailscale, ipn/ipnlocal: add 'debug dial-types' command This command allows observing whether a given dialer ("SystemDial", "UserDial", etc.) will successfully obtain a connection to a provided host, from inside tailscaled itself. This is intended to help debug a variety of issues from subnet routers to split DNS setups. Updates #9619 Signed-off-by: Andrew Dunham Change-Id: Ie01ebb5469d3e287eac633ff656783960f697b84 --- cmd/tailscale/cli/debug.go | 68 ++++++++++++++++++++++++++++++++++++ ipn/localapi/localapi.go | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index fe6faef97..0f5264282 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -274,6 +274,16 @@ Exec: runPeerEndpointChanges, ShortHelp: "prints debug information about a peer's endpoint changes", }, + { + Name: "dial-types", + Exec: runDebugDialTypes, + ShortHelp: "prints debug information about connecting to a given host or IP", + FlagSet: (func() *flag.FlagSet { + fs := newFlagSet("dial-types") + fs.StringVar(&debugDialTypesArgs.network, "network", "tcp", `network type to dial ("tcp", "udp", etc.)`) + return fs + })(), + }, }, } @@ -1015,3 +1025,61 @@ func debugControlKnobs(ctx context.Context, args []string) error { e.Encode(v) return nil } + +var debugDialTypesArgs struct { + network string +} + +func runDebugDialTypes(ctx context.Context, args []string) error { + st, err := localClient.Status(ctx) + if err != nil { + return fixTailscaledConnectError(err) + } + description, ok := isRunningOrStarting(st) + if !ok { + printf("%s\n", description) + os.Exit(1) + } + + if len(args) != 2 || args[0] == "" || args[1] == "" { + return errors.New("usage: dial-types ") + } + + port, err := strconv.ParseUint(args[1], 10, 16) + if err != nil { + return fmt.Errorf("invalid port %q: %w", args[1], err) + } + + hostOrIP := args[0] + ip, _, err := tailscaleIPFromArg(ctx, hostOrIP) + if err != nil { + return err + } + if ip != hostOrIP { + log.Printf("lookup %q => %q", hostOrIP, ip) + } + + qparams := make(url.Values) + qparams.Set("ip", ip) + qparams.Set("port", strconv.FormatUint(port, 10)) + qparams.Set("network", debugDialTypesArgs.network) + + req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/debug-dial-types?"+qparams.Encode(), nil) + if err != nil { + return err + } + + resp, err := localClient.DoLocalRequest(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + fmt.Printf("%s", body) + return nil +} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 220d866b8..4559a3997 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -80,6 +80,7 @@ "component-debug-logging": (*Handler).serveComponentDebugLogging, "debug": (*Handler).serveDebug, "debug-derp-region": (*Handler).serveDebugDERPRegion, + "debug-dial-types": (*Handler).serveDebugDialTypes, "debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches, "debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules, "debug-portmap": (*Handler).serveDebugPortmap, @@ -840,6 +841,76 @@ func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Requ json.NewEncoder(w).Encode(res) } +func (h *Handler) serveDebugDialTypes(w http.ResponseWriter, r *http.Request) { + if !h.PermitWrite { + http.Error(w, "debug-dial-types access denied", http.StatusForbidden) + return + } + if r.Method != httpm.POST { + http.Error(w, "only POST allowed", http.StatusMethodNotAllowed) + return + } + + ip := r.FormValue("ip") + port := r.FormValue("port") + network := r.FormValue("network") + + addr := ip + ":" + port + if _, err := netip.ParseAddrPort(addr); err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "invalid address %q: %v", addr, err) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + + var bareDialer net.Dialer + + dialer := h.b.Dialer() + + var peerDialer net.Dialer + peerDialer.Control = dialer.PeerDialControlFunc() + + // Kick off a dial with each available dialer in parallel. + dialers := []struct { + name string + dial func(context.Context, string, string) (net.Conn, error) + }{ + {"SystemDial", dialer.SystemDial}, + {"UserDial", dialer.UserDial}, + {"PeerDial", peerDialer.DialContext}, + {"BareDial", bareDialer.DialContext}, + } + type result struct { + name string + conn net.Conn + err error + } + results := make(chan result, len(dialers)) + + var wg sync.WaitGroup + for _, dialer := range dialers { + dialer := dialer // loop capture + + wg.Add(1) + go func() { + defer wg.Done() + conn, err := dialer.dial(ctx, network, addr) + results <- result{dialer.name, conn, err} + }() + } + + wg.Wait() + for i := 0; i < len(dialers); i++ { + res := <-results + fmt.Fprintf(w, "[%s] connected=%v err=%v\n", res.name, res.conn != nil, res.err) + if res.conn != nil { + res.conn.Close() + } + } +} + // servePprofFunc is the implementation of Handler.servePprof, after auth, // for platforms where we want to link it in. var servePprofFunc func(http.ResponseWriter, *http.Request)