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

@ -39,6 +39,11 @@ type Change struct {
// must be computed at runtime per-node. Used for policy changes
// where each node may have different peer visibility.
RequiresRuntimePeerComputation bool
// PingRequest, if non-nil, is a ping request to send to the node.
// Used by the debug ping endpoint to verify node connectivity.
// PingRequest is always targeted to a specific node via TargetNode.
PingRequest *tailcfg.PingRequest
}
// boolFieldNames returns all boolean field names for exhaustive testing.
@ -93,6 +98,11 @@ func (r Change) Merge(other Change) Change {
merged.TargetNode = other.TargetNode
}
// Preserve PingRequest (first wins).
if merged.PingRequest == nil {
merged.PingRequest = other.PingRequest
}
if r.Reason != "" && other.Reason != "" && r.Reason != other.Reason {
merged.Reason = r.Reason + "; " + other.Reason
} else if other.Reason != "" {
@ -112,6 +122,10 @@ func (r Change) IsEmpty() bool {
return false
}
if r.PingRequest != nil {
return false
}
return len(r.PeersChanged) == 0 &&
len(r.PeersRemoved) == 0 &&
len(r.PeerPatches) == 0
@ -168,6 +182,10 @@ func (r Change) Type() string {
return "config"
}
if r.PingRequest != nil {
return "ping"
}
return "unknown"
}
@ -454,6 +472,16 @@ func UserRemoved() Change {
return c
}
// PingNode creates a Change that sends a PingRequest to a specific node.
// The node will respond to the PingRequest URL to prove connectivity.
func PingNode(nodeID types.NodeID, pr *tailcfg.PingRequest) Change {
return Change{
Reason: "ping node",
TargetNode: nodeID,
PingRequest: pr,
}
}
// ExtraRecords returns a Change for when DNS extra records change.
func ExtraRecords() Change {
c := DNSConfig()