From 5555bbcd222f6f0e757d458f4251d87450d67b41 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 3 Oct 2025 15:12:03 -0700 Subject: [PATCH] ipn/ipnlocal: extract ping implementation into its own file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There’s enough code to pull this out into its own ping.go file, since local.go contains a lot of stuff. The next commit will add a Pinger interface to this file, so I am breaking out this refactor to make identifying the changes and reviewing the actual patch easier. Signed-off-by: Simon Law --- ipn/ipnlocal/local.go | 75 ---------------------------------- ipn/ipnlocal/ping.go | 93 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 75 deletions(-) create mode 100644 ipn/ipnlocal/ping.go diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index fea887f0b..9490537df 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3700,81 +3700,6 @@ func (b *LocalBackend) StartLoginInteractiveAs(ctx context.Context, user ipnauth return nil } -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 -} - // 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/ipnlocal/ping.go b/ipn/ipnlocal/ping.go new file mode 100644 index 000000000..4f6808a1e --- /dev/null +++ b/ipn/ipnlocal/ping.go @@ -0,0 +1,93 @@ +// 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" +) + +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 +}