carlos / platform Public

status: GeoIP block on every box heartbeat; agent marks tunnel clients (geoip spec §5.5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ec776baa3d998e17e9f49499141ca6e4eb5e2875 parent 9ee9aac
4 files changed, +82 −2
  • internal/host/agent.go +27 −0
  • internal/host/agent_geoip_test.go +21 −0
  • internal/status/snapshot.go +17 −2
  • internal/status/snapshot_test.go +17 −0

CI passed — run details

diff --git a/internal/host/agent.go b/internal/host/agent.go
index 94dad4ed4..5b934c27e 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -573,6 +573,11 @@ func RunAgent(args []string) {
// socket. It presents this box's public address, the one the edges
// file publishes; with none, the edge keeps the door shut and says so.
edgeDoorOptions(os.Getenv, readPublicIP)...)
+ // A tunnel client takes the hub's GeoIP answer and mirrors nothing (geoip
+ // spec §5.1); the same rule tunnelLoop arms on.
+ if tunnelEnvComplete(os.Getenv) {
+ edgeOpts = append(edgeOpts, edge.WithGeoTunnelClient())
+ }
edge.RunWithActivatorReady(args, act, cache, func(e *edge.Edge) {
go tunnelLoop(e)
// The edge owns the health probers, so its state table is only
@@ -581,6 +586,8 @@ func RunAgent(args []string) {
// the loop always sees the CURRENT probe set: Reload replaces probers
// behind this handle without the heartbeat needing to be re-wired.
setUpstreamSource(e.UpstreamStates)
+ // The GeoIP block for the heartbeat (geoip spec §5.5), the same way.
+ geoIPSource.Store(e.GeoIPState)
// The layout pass reloads routing through this as soon as a move
// commits, instead of waiting for the 2s registry poll.
reload := func() { _ = e.Reload() }
@@ -2780,6 +2787,25 @@ func shouldSkipPublisherElection(getenv func(string) string) bool {
// goroutine a test would have no way to stop.
var upstreamSource atomic.Value // func() map[string][]status.UpstreamState
+// geoIPSource is the edge's GeoIP state for the heartbeat, published from
+// the edge-ready callback like upstreamSource (geoip spec §5.5).
+var geoIPSource atomic.Value // func() edge.GeoIPState
+
+// geoIPState reads it, or nil before the edge is ready.
+func geoIPState() *status.GeoIPState {
+ fn, _ := geoIPSource.Load().(func() edge.GeoIPState)
+ if fn == nil {
+ return nil
+ }
+ s := fn()
+ return &status.GeoIPState{Mode: s.Mode, SHA256: s.SHA256, Type: s.Type, Error: s.Error}
+}
+
+// tunnelEnvComplete is tunnelLoop's own arming rule: all three set.
+func tunnelEnvComplete(getenv func(string) string) bool {
+ return getenv("CARLOS_TUNNEL_URL") != "" && getenv("CARLOS_TUNNEL_TOKEN") != "" && getenv("CARLOS_TUNNEL_NAME") != ""
+}
+
// setUpstreamSource publishes (or, with nil, withdraws) the probe-state
// source. Called once, from the edge-ready callback.
func setUpstreamSource(fn func() map[string][]status.UpstreamState) {
@@ -2984,6 +3010,7 @@ func statusLoop(reg *registry.Registry) {
SystemChannel: boxSystemChannel(os.Getenv),
System: systemStateProbe(release.OptDir()),
Tunnel: tunnelState(),
+ GeoIP: geoIPState(),
// The edge's probe table, read WHOLE every tick (never merged
// with the last one), so a route this box has stopped probing
// leaves no ghost behind in the next snapshot. nil until the
diff --git a/internal/host/agent_geoip_test.go b/internal/host/agent_geoip_test.go
new file mode 100644
index 000000000..c3ed3cc90
--- /dev/null
+++ b/internal/host/agent_geoip_test.go
@@ -0,0 +1,21 @@
+package host
+
+import "testing"
+
+func TestTunnelEnvComplete(t *testing.T) {
+ env := map[string]string{"CARLOS_TUNNEL_URL": "https://t", "CARLOS_TUNNEL_TOKEN": "x", "CARLOS_TUNNEL_NAME": "n"}
+ get := func(k string) string { return env[k] }
+ if !tunnelEnvComplete(get) {
+ t.Fatal("complete env")
+ }
+ delete(env, "CARLOS_TUNNEL_NAME")
+ if tunnelEnvComplete(get) {
+ t.Fatal("incomplete env must not make a tunnel client (tunnelLoop would not run)")
+ }
+}
+
+func TestGeoIPStateNilBeforeTheEdgeIsReady(t *testing.T) {
+ if geoIPState() != nil {
+ t.Fatal("no edge published yet must mean no GeoIP block")
+ }
+}
diff --git a/internal/status/snapshot.go b/internal/status/snapshot.go
index 8f8577ff9..07e479760 100644
--- a/internal/status/snapshot.go
+++ b/internal/status/snapshot.go
@@ -100,6 +100,9 @@ type Probes struct {
// unknown/not-applicable for both, never a guess.
Release string
Tunnel string
+ // GeoIP is the edge's GeoIP mirror state (geoip spec §5.5); nil when
+ // the edge has not published one (no edge yet, or an older binary).
+ GeoIP *GeoIPState
// Platform is runtime.GOOS-runtime.GOARCH, so the console knows which
// artifacts a platform promote must mirror. SystemChannel is the box's
// CARLOS_SYSTEM_CHANNEL, so the console knows which boxes a promote
@@ -280,6 +283,10 @@ type BoxMetrics struct {
// naming convention, because it is not using one.
InstanceID string `json:"instance_id,omitempty"`
Tunnel string `json:"tunnel,omitempty"` // ""|"connected"|"reconnecting"
+ // GeoIP is present on every box running a GeoIP-aware binary (geoip
+ // spec §5.5); its absence is how the rollout verdict tells an older
+ // binary, which does not strip forged geo headers.
+ GeoIP *GeoIPState `json:"geoip,omitempty"`
// RouteVersions is host -> registry.Route.Version for every non-platform
// route this box carries with a Version the adopt pass has stamped
// (issue #78's staged_version chip) — a hostname-keyed map, which is
@@ -741,11 +748,11 @@ func CollectBox(routes []registry.Route, p Probes, now time.Time) Box {
p.InstanceID != "" || p.Platform != "" || p.SystemChannel != "" || p.System != nil ||
len(routeVersions) > 0 || len(aliasReadings) > 0 || len(p.Upstreams) > 0 ||
len(routeDBBytes) > 0 || p.Layout != nil || len(p.ConfigRefusals) > 0 || p.ConfigGuard != "" ||
- p.Alerts != nil {
+ p.Alerts != nil || p.GeoIP != nil {
box.Metrics = &BoxMetrics{
MemTotalBytes: p.MemTotalBytes, MemAvailBytes: p.MemAvailBytes,
Load1: p.Load1, DiskFreeBytes: p.DiskFreeBytes,
- Release: p.Release, Tunnel: p.Tunnel,
+ Release: p.Release, Tunnel: p.Tunnel, GeoIP: p.GeoIP,
Platform: p.Platform, SystemChannel: p.SystemChannel, System: p.System,
InstanceID: p.InstanceID,
Alerts: p.Alerts,
@@ -822,3 +829,11 @@ func currentEtag(ctx context.Context, s release.Store, key string) (string, erro
}
return etag, nil
}
+
+// GeoIPState is one box's GeoIP mirror (geoip spec §5.5).
+type GeoIPState struct {
+ Mode string `json:"mode"` // mirror | tunnel | none
+ SHA256 string `json:"sha256,omitempty"`
+ Type string `json:"type,omitempty"`
+ Error string `json:"error,omitempty"`
+}
diff --git a/internal/status/snapshot_test.go b/internal/status/snapshot_test.go
index e72097397..85fdcb47c 100644
--- a/internal/status/snapshot_test.go
+++ b/internal/status/snapshot_test.go
@@ -140,3 +140,20 @@ func TestBoxMetricsCarryAlertDelivery(t *testing.T) {
t.Fatalf("fleet.json leaked the alert-sink state: %s", fb)
}
}
+
+func TestCollectBoxCarriesGeoIP(t *testing.T) {
+ box := CollectBox(nil, Probes{GeoIP: &GeoIPState{Mode: "mirror", SHA256: "abc", Type: "DBIP-City-Lite"}}, time.Now())
+ if box.Metrics == nil || box.Metrics.GeoIP == nil || box.Metrics.GeoIP.SHA256 != "abc" {
+ t.Fatalf("metrics = %+v", box.Metrics)
+ }
+ raw, err := json.Marshal(box)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(raw), `"geoip":{"mode":"mirror"`) {
+ t.Fatalf("json = %s", raw)
+ }
+ if b := CollectBox(nil, Probes{}, time.Now()); b.Metrics != nil {
+ t.Fatal("no GeoIP and nothing else known must still mean no metrics block")
+ }
+}