Browse Source

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 <[email protected]>
Brad Fitzpatrick 3 years ago
parent
commit
4d85cf586b
5 changed files with 86 additions and 0 deletions
  1. 9 0
      cmd/tailscale/cli/ping.go
  2. 52 0
      ipn/ipnlocal/local.go
  3. 4 0
      ipn/ipnstate/ipnstate.go
  4. 3 0
      tailcfg/tailcfg.go
  5. 18 0
      types/netmap/netmap.go

+ 9 - 0
cmd/tailscale/cli/ping.go

@@ -51,6 +51,7 @@ relay node.
 		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 @@ var pingArgs struct {
 	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 {

+ 52 - 0
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

+ 4 - 0
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"`

+ 3 - 0
tailcfg/tailcfg.go

@@ -1097,6 +1097,9 @@ const (
 	// 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

+ 18 - 0
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).
 //