fix(nodes): resolve agent public address from master-observed source IP

NAT'd agents (cloud 1:1 EIP) only ever report their private
interface address in heartbeat telemetry, so the node list showed
an unusable RFC1918 address. The master now records the source
address of every authenticated agent request (CF-Connecting-IP,
else first X-Forwarded-For hop, else RemoteAddr; change-only
writes) on the agent row as seen_ipv4/seen_ipv6, and the node
list resolves the displayed pair via service.ResolveAgentAddresses:
public telemetry wins, else a public observed source, else the
reported address as-is. CGNAT 100.64/10 observed addresses are
never promoted. Telemetry values are kept verbatim; resolution
is read-time only, and agents need no changes.
This commit is contained in:
Muse
2026-10-22 15:25:08 +00:00
parent e310abcba0
commit c490bdcd76
6 changed files with 527 additions and 10 deletions
+14
View File
@@ -48,6 +48,20 @@ type AgentNode struct {
// a telemetry object.
TelemetryAt int64 `json:"telemetryAt"`
// Seen source addresses (public-address resolution): the source IP of
// the agent's own authenticated requests, as observed by the master —
// the CF-Connecting-IP header when the agent comes through Cloudflare,
// else the direct peer address. An agent behind 1:1 NAT reports only
// its private interface address in telemetry (its public address is
// not assigned to any local interface, so enumeration can never see
// it); the observed source address is the master's view of the
// agent's public side. Writes are change-only (see
// AgentController.captureSeenIP); SeenIPAt is the unix-milli time of
// the last change, not of the last request.
SeenIPv4 string `json:"seenIpv4"`
SeenIPv6 string `json:"seenIpv6"`
SeenIPAt int64 `json:"seenIpAt"`
// One-click update (item 20): the master records an update intent here;
// the agent picks it up from its config poll, downloads + verifies +
// swaps its binary, restarts itself and reports the outcome back on