// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package ipnlocal import ( "context" "errors" "fmt" "net/http" "net/netip" "time" "tailscale.com/feature" "tailscale.com/feature/buildfeatures" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" ) // Pinger is the [LocalBackend.Ping] method. type Pinger interface { // Ping is a request to do a ping with the peer handling the given IP. Ping(ctx context.Context, ip netip.Addr, pingType tailcfg.PingType, size int) (*ipnstate.PingResult, error) } func (b *LocalBackend) Ping(ctx context.Context, ip netip.Addr, pingType tailcfg.PingType, size int) (*ipnstate.PingResult, error) { if pingType == tailcfg.PingPeerAPI { t0 := b.clock.Now() node, base, err := b.pingPeerAPI(ctx, ip) if err != nil && ctx.Err() != nil { return nil, ctx.Err() } d := b.clock.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.Valid() { pr.NodeName = node.Name() } return pr, nil } ch := make(chan *ipnstate.PingResult, 1) b.e.Ping(ip, pingType, size, func(pr *ipnstate.PingResult) { select { case ch <- pr: default: } }) select { case pr := <-ch: return pr, nil case <-ctx.Done(): return nil, ctx.Err() } } func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netip.Addr) (peer tailcfg.NodeView, peerBase string, err error) { if !buildfeatures.HasPeerAPIClient { return peer, peerBase, feature.ErrUnavailable } var zero tailcfg.NodeView ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() nm := b.NetMap() if nm == nil { return zero, "", errors.New("no netmap") } peer, ok := nm.PeerByTailscaleIP(ip) if !ok { return zero, "", fmt.Errorf("no peer found with Tailscale IP %v", ip) } if peer.Expired() { return zero, "", errors.New("peer's node key has expired") } base := peerAPIBase(nm, peer) if base == "" { return zero, "", fmt.Errorf("no PeerAPI base found for peer %v (%v)", peer.ID(), ip) } outReq, err := http.NewRequestWithContext(ctx, "HEAD", base, nil) if err != nil { return zero, "", err } tr := b.Dialer().PeerAPITransport() res, err := tr.RoundTrip(outReq) if err != nil { return zero, "", err } defer res.Body.Close() // but unnecessary on HEAD responses if res.StatusCode != http.StatusOK { return zero, "", fmt.Errorf("HTTP status %v", res.Status) } return peer, base, nil }