From 4d85cf586bd89713846d2787f04430e7b592ae38 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 27 May 2022 21:34:36 -0700 Subject: [PATCH] cmd/tailscale, ipn/ipnlocal: add "peerapi" ping type For debugging when stuff like #4750 isn't working. RELNOTE=tailscale ping -peerapi Change-Id: I9c52c90fb046e3ab7d2b121387073319fbf27b99 Signed-off-by: Brad Fitzpatrick --- cmd/tailscale/cli/ping.go | 9 +++++++ ipn/ipnlocal/local.go | 52 +++++++++++++++++++++++++++++++++++++++ ipn/ipnstate/ipnstate.go | 4 +++ tailcfg/tailcfg.go | 3 +++ types/netmap/netmap.go | 18 ++++++++++++++ 5 files changed, 86 insertions(+) diff --git a/cmd/tailscale/cli/ping.go b/cmd/tailscale/cli/ping.go index 36c1ed514..5eb968734 100644 --- a/cmd/tailscale/cli/ping.go +++ b/cmd/tailscale/cli/ping.go @@ -51,6 +51,7 @@ fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established") fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through WireGuard, but not either host OS stack)") fs.BoolVar(&pingArgs.icmp, "icmp", false, "do a ICMP-level ping (through WireGuard, but not the local host OS stack)") + fs.BoolVar(&pingArgs.peerAPI, "peerapi", false, "try hitting the peer's peerapi HTTP server") fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send") fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping") return fs @@ -63,6 +64,7 @@ verbose bool tsmp bool icmp bool + peerAPI bool timeout time.Duration } @@ -73,6 +75,9 @@ func pingType() tailcfg.PingType { if pingArgs.icmp { return tailcfg.PingICMP } + if pingArgs.peerAPI { + return tailcfg.PingPeerAPI + } return tailcfg.PingDisco } @@ -137,6 +142,10 @@ func runPing(ctx context.Context, args []string) error { // For now just say which protocol it used. via = string(pingType()) } + if pingArgs.peerAPI { + printf("hit peerapi of %s (%s) at %s in %s\n", pr.NodeIP, pr.NodeName, pr.PeerAPIURL, latency) + return nil + } anyPong = true extra := "" if pr.PeerAPIPort != 0 { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index d4f95122a..ce8e58342 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1706,6 +1706,27 @@ func (b *LocalBackend) StartLoginInteractive() { } func (b *LocalBackend) Ping(ctx context.Context, ip netaddr.IP, pingType tailcfg.PingType) (*ipnstate.PingResult, error) { + if pingType == tailcfg.PingPeerAPI { + t0 := time.Now() + node, base, err := b.pingPeerAPI(ctx, ip) + if err != nil && ctx.Err() != nil { + return nil, ctx.Err() + } + d := time.Since(t0) + pr := &ipnstate.PingResult{ + IP: ip.String(), + NodeIP: ip.String(), + LatencySeconds: d.Seconds(), + PeerAPIURL: base, + } + if err != nil { + pr.Err = err.Error() + } + if node != nil { + pr.NodeName = node.Name + } + return pr, nil + } ch := make(chan *ipnstate.PingResult, 1) b.e.Ping(ip, pingType, func(pr *ipnstate.PingResult) { select { @@ -1721,6 +1742,37 @@ func (b *LocalBackend) Ping(ctx context.Context, ip netaddr.IP, pingType tailcfg } } +func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netaddr.IP) (peer *tailcfg.Node, peerBase string, err error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + nm := b.NetMap() + if nm == nil { + return nil, "", errors.New("no netmap") + } + peer, ok := nm.PeerByTailscaleIP(ip) + if !ok { + return nil, "", fmt.Errorf("no peer found with Tailscale IP %v", ip) + } + base := peerAPIBase(nm, peer) + if base == "" { + return nil, "", fmt.Errorf("no peer API base found for peer %v (%v)", peer.ID, ip) + } + outReq, err := http.NewRequestWithContext(ctx, "HEAD", base, nil) + if err != nil { + return nil, "", err + } + tr := b.Dialer().PeerAPITransport() + res, err := tr.RoundTrip(outReq) + if err != nil { + return nil, "", err + } + defer res.Body.Close() // but unnecessary on HEAD responses + if res.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("HTTP status %v", res.Status) + } + return peer, base, nil +} + // parseWgStatusLocked returns an EngineStatus based on s. // // b.mu must be held; mostly because the caller is about to anyway, and doing so diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 7687da25c..f0322878d 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -508,6 +508,10 @@ type PingResult struct { // running the server on. PeerAPIPort uint16 `json:",omitempty"` + // PeerAPIURL is the URL that was hit for pings of type "peerapi" (tailcfg.PingPeerAPI). + // It's of the form "http://ip:port" (or [ip]:port for IPv6). + PeerAPIURL string `json:",omitempty"` + // IsLocalIP is whether the ping request error is due to it being // a ping to the local node. IsLocalIP bool `json:",omitempty"` diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index b7ddb9dec..d32f8edf9 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -1097,6 +1097,9 @@ type DNSRecord struct { // PingICMP performs a ping between two tailscale nodes using ICMP that is // received by the target systems IP stack. PingICMP PingType = "ICMP" + // PingPeerAPI performs a ping between two tailscale nodes using ICMP that is + // received by the target systems IP stack. + PingPeerAPI PingType = "peerapi" ) // PingRequest with no IP and Types is a request to send an HTTP request to prove the diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index ae8bec99f..9946cb823 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -72,6 +72,24 @@ type NetworkMap struct { UserProfiles map[tailcfg.UserID]tailcfg.UserProfile } +// PeerByTailscaleIP returns a peer's Node based on its Tailscale IP. +// +// If nm is nil or no peer is found, ok is false. +func (nm *NetworkMap) PeerByTailscaleIP(ip netaddr.IP) (peer *tailcfg.Node, ok bool) { + // TODO(bradfitz): + if nm == nil { + return nil, false + } + for _, n := range nm.Peers { + for _, a := range n.Addresses { + if a.IP() == ip { + return n, true + } + } + } + return nil, false +} + // MagicDNSSuffix returns the domain's MagicDNS suffix (even if // MagicDNS isn't necessarily in use). //