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 <andrew@du.nham.ca>
Change-Id: Ie01ebb5469d3e287eac633ff656783960f697b84
pull/10760/head
Andrew Dunham 11 months ago
parent aed2cfec4e
commit d3574a350f

@ -274,6 +274,16 @@ var debugCmd = &ffcli.Command{
Exec: runPeerEndpointChanges, Exec: runPeerEndpointChanges,
ShortHelp: "prints debug information about a peer's endpoint changes", 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) e.Encode(v)
return nil 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 <hostname-or-IP> <port>")
}
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
}

@ -80,6 +80,7 @@ var handler = map[string]localAPIHandler{
"component-debug-logging": (*Handler).serveComponentDebugLogging, "component-debug-logging": (*Handler).serveComponentDebugLogging,
"debug": (*Handler).serveDebug, "debug": (*Handler).serveDebug,
"debug-derp-region": (*Handler).serveDebugDERPRegion, "debug-derp-region": (*Handler).serveDebugDERPRegion,
"debug-dial-types": (*Handler).serveDebugDialTypes,
"debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches, "debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches,
"debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules, "debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules,
"debug-portmap": (*Handler).serveDebugPortmap, "debug-portmap": (*Handler).serveDebugPortmap,
@ -840,6 +841,76 @@ func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Requ
json.NewEncoder(w).Encode(res) 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, // servePprofFunc is the implementation of Handler.servePprof, after auth,
// for platforms where we want to link it in. // for platforms where we want to link it in.
var servePprofFunc func(http.ResponseWriter, *http.Request) var servePprofFunc func(http.ResponseWriter, *http.Request)

Loading…
Cancel
Save