all: implement PingRequest for node connectivity checking

Implement tailcfg.PingRequest support so the control server can verify
whether a connected node is still reachable. This is the foundation for
faster offline detection (currently ~16min due to Go HTTP/2 TCP retransmit
behavior) and future C2N communication.

The server sends a PingRequest via MapResponse with a unique callback
URL. The Tailscale client responds with a HEAD request to that URL,
proving connectivity. Round-trip latency is measured.

Wire PingRequest through the Change → Batcher → MapResponse pipeline,
add a ping tracker on State for correlating requests with responses,
add ResolveNode for looking up nodes by ID/IP/hostname, and expose a
/debug/ping page (elem-go form UI) and /machine/ping-response endpoint.

Updates #2902
Updates #2129
This commit is contained in:
Kristoffer Dalby 2026-04-10 12:46:19 +00:00
parent 32e1d77663
commit b113655b71
9 changed files with 478 additions and 0 deletions

View file

@ -146,6 +146,9 @@ type State struct {
// only proceeds when the generation it carries matches the latest.
connectGen sync.Map // types.NodeID → *atomic.Uint64
// pings tracks pending ping requests and their response channels.
pings *pingTracker
// sshCheckAuth tracks when source nodes last completed SSH check auth.
//
// For rules without explicit checkPeriod (default 12h), auth covers any
@ -256,6 +259,7 @@ func NewState(cfg *types.Config) (*State, error) {
authCache: authCache,
primaryRoutes: routes.New(),
nodeStore: nodeStore,
pings: newPingTracker(),
sshCheckAuth: make(map[sshCheckPair]time.Time),
}, nil
@ -699,6 +703,37 @@ func (s *State) GetNodeByMachineKey(machineKey key.MachinePublic, userID types.U
return s.nodeStore.GetNodeByMachineKey(machineKey, userID)
}
// ResolveNode looks up a node by numeric ID, IPv4/IPv6 address, hostname, or given name.
// It tries ID first, then IP, then name matching.
func (s *State) ResolveNode(query string) (types.NodeView, bool) {
// Try numeric ID first.
id, idErr := types.ParseNodeID(query)
if idErr == nil {
return s.GetNodeByID(id)
}
// Try IP address.
addr, addrErr := netip.ParseAddr(query)
if addrErr == nil {
for _, n := range s.ListNodes().All() {
if slices.Contains(n.IPs(), addr) {
return n, true
}
}
return types.NodeView{}, false
}
// Try hostname / given name.
for _, n := range s.ListNodes().All() {
if n.Hostname() == query || n.GivenName() == query {
return n, true
}
}
return types.NodeView{}, false
}
// ListNodes retrieves specific nodes by ID, or all nodes if no IDs provided.
func (s *State) ListNodes(nodeIDs ...types.NodeID) views.Slice[types.NodeView] {
if len(nodeIDs) == 0 {