#162: foldHomes ignores role replica as a decision, not an accident
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 files changed,
+119
−0
internal/edge/alias_test.go+77 −0internal/edge/homemap.go+14 −0internal/edge/homemap_test.go+28 −0
CI passed — run details
diff --git a/internal/edge/alias_test.go b/internal/edge/alias_test.go| index 2627c867b..d58abed21 100644 |
| --- a/internal/edge/alias_test.go |
| +++ b/internal/edge/alias_test.go |
| @@ -1,6 +1,7 @@ |
| package edge |
| import ( |
| + "context" |
| "maps" |
| "net/http" |
| "net/http/httptest" |
| @@ -590,3 +591,79 @@ func TestHTTPFallbackRedirectsTunnelHost(t *testing.T) { |
| t.Fatalf("tunnel.test: Location = %q, want https://tunnel.test/register", loc) |
| } |
| } |
| + |
| +// TestReplicaAliasServesLocallyWithoutBackhaul is the assertion the whole of |
| +// #162 rests on (spec §3, §4): on a replica's box, a request for the |
| +// ACCOUNT's public host is served by the local replica upstream, and is not |
| +// backhauled to the home. |
| +// |
| +// The registry holds exactly what Tasks 3 and 4 leave on a replica box — the |
| +// replica's own route carrying replica_of, and a route_aliases row mapping the |
| +// account host to it — and the home map is seeded, from a real store, with an |
| +// edges file in which ANOTHER box publishes that same account host as role |
| +// "home". Both halves are live at once, which is the point: with the alias row |
| +// gone this request has a dial target waiting for it (a mesh key is minted, so |
| +// the hop can be proven) and the local upstream would never be reached. The |
| +// home EIP is a refusing address so that failure is a fast 502 rather than a |
| +// dial timeout. |
| +func TestReplicaAliasServesLocallyWithoutBackhaul(t *testing.T) { |
| + backend, seenHost := hostEchoBackend(t, "served by the local replica") |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "r7x2k9m4qa.go.tito.io", Kind: "service", |
| + Addr: backend.Listener.Addr().String(), Status: "ready", |
| + Account: "bab", App: "tito", ReplicaOf: "acme.go.tito.io", |
| + }); err != nil { |
| + t.Fatalf("upsert replica route: %v", err) |
| + } |
| + upsertAlias(t, reg, "acme.go.tito.io", "r7x2k9m4qa.go.tito.io") |
| + |
| + s := meshTestStore(t) |
| + if err := MintMeshKey(context.Background(), s); err != nil { |
| + t.Fatalf("mint mesh key: %v", err) |
| + } |
| + homeEIP := e2eDeadEIP(t) |
| + putJSON(t, s, "control/edges/ie-1.json", `{"v":1,"box":"ie-1","eip":"`+homeEIP+ |
| + `","hosts":{"acme.go.tito.io":{"role":"home","platform":true}}}`) |
| + putJSON(t, s, "control/edges/uk-1.json", |
| + `{"v":1,"box":"uk-1","eip":"203.0.113.9","hosts":{`+ |
| + `"acme.go.tito.io":{"role":"replica","alias":true},`+ |
| + `"r7x2k9m4qa.go.tito.io":{"role":"home","platform":true}}}`) |
| + |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatalf("New: %v", err) |
| + } |
| + e.staticStore = s |
| + WithBoxLabel("uk-1")(e) |
| + hm := e.newHomeMap() |
| + if hm == nil { |
| + t.Fatal("a store-backed edge must have a home map") |
| + } |
| + refreshHomeMap(t, hm) |
| + e.homes.Store(hm) |
| + |
| + // The premise, asserted rather than assumed: this box's map really does |
| + // home the account host somewhere else. Without it the no-backhaul claim |
| + // below would be vacuous — nothing to backhaul TO. |
| + if eip, box, ok := hm.Home("acme.go.tito.io"); !ok || box != "ie-1" || eip != homeEIP { |
| + t.Fatalf("home map says acme.go.tito.io is homed at %s/%s (ok=%v), want the remote home box ie-1", eip, box, ok) |
| + } |
| + |
| + srv := httptest.NewServer(e) |
| + defer srv.Close() |
| + |
| + code, body := getHost(t, srv.URL, "acme.go.tito.io") |
| + if code != http.StatusOK { |
| + t.Fatalf("status = %d, want 200 — the account's host was not served by the local replica", code) |
| + } |
| + if body != "served by the local replica" { |
| + t.Fatalf("body = %q, want the local replica's answer — the request was not served locally", body) |
| + } |
| + // And the app saw the name the buyer asked for, not the replica's platform |
| + // hostname: the alias fold shares the primary's upstream but must not |
| + // rewrite the Host. |
| + if *seenHost != "acme.go.tito.io" { |
| + t.Fatalf("the replica's upstream saw Host %q, want acme.go.tito.io", *seenHost) |
| + } |
| +} |
diff --git a/internal/edge/homemap.go b/internal/edge/homemap.go| index 4057747db..944c8edda 100644 |
| --- a/internal/edge/homemap.go |
| +++ b/internal/edge/homemap.go |
| @@ -110,6 +110,11 @@ const edgeRoleHome = "home" |
| // not serve the catch-all, so the entry is no dial target. |
| const edgeRoleWildcard = "wildcard" |
| +// edgeRoleReplica is an account's public host published by a box that serves |
| +// it from a one-way mirror (#162). It is never a dial target for anyone: |
| +// see the switch in foldHomes for why the ignore is a decision. |
| +const edgeRoleReplica = "replica" |
| + |
| // homeMapInterval / homeMapTimeout / homeMapBootTimeout mirror the cert |
| // mirror's cadence and budgets exactly (certsync.go), for the same reasons: a |
| // home move is a placement event that converge heals, the poll runs on its own |
| @@ -560,6 +565,15 @@ func foldHomes(files map[string]edgesFileView, self string) map[string]homeEntry |
| if !h.Wildcard || !strings.HasPrefix(host, "*.") { |
| continue |
| } |
| + case edgeRoleReplica: |
| + // #162: that box serves the account from a one-way mirror |
| + // and says so, so its own edge resolves the host locally |
| + // while the FLEET goes on treating the home as the only |
| + // dial target. Dropping it here keeps this map |
| + // single-valued; publishing it as "home" would hand a third |
| + // box two targets settled by sort order, and the account's |
| + // traffic would land on a mirror. |
| + continue |
| default: |
| continue |
| } |
diff --git a/internal/edge/homemap_test.go b/internal/edge/homemap_test.go| index ce605928c..0aae1c685 100644 |
| --- a/internal/edge/homemap_test.go |
| +++ b/internal/edge/homemap_test.go |
| @@ -111,6 +111,34 @@ func TestHomeMapIgnoresUnknownRoles(t *testing.T) { |
| } |
| } |
| +// Replica placements (#162): a replica box publishes the ACCOUNT's public |
| +// host as role "replica", so its own edge serves that host from the local |
| +// mirror while every other box goes on dialling the home. This map is the |
| +// fleet's half of that bargain — it must stay single-valued on the account |
| +// host, and the value must be the HOME. Marking the replica "home" instead |
| +// would hand a third box two dial targets settled by sort order, and some of |
| +// the account's traffic would land on a mirror. |
| +func TestFoldHomesIgnoresReplicaEntries(t *testing.T) { |
| + files := map[string]edgesFileView{ |
| + "ie-1": {V: 1, Box: "ie-1", EIP: "198.51.100.4", Hosts: map[string]edgesHostView{ |
| + "acme.go.tito.io": {Role: "home", Platform: true}, |
| + }}, |
| + "uk-1": {V: 1, Box: "uk-1", EIP: "203.0.113.9", Hosts: map[string]edgesHostView{ |
| + "acme.go.tito.io": {Role: "replica", Alias: true}, |
| + "r7x2k9m4qa.go.tito.io": {Role: "home", Platform: true}, |
| + }}, |
| + } |
| + got := foldHomes(files, "us-1") |
| + if e, ok := got["acme.go.tito.io"]; !ok || e.box != "ie-1" || e.eip != "198.51.100.4" { |
| + t.Fatalf("account host resolved to %+v, want home box ie-1: a replica entry must never win the dial", e) |
| + } |
| + // The replica's OWN host stays an ordinary home: that is how the home box |
| + // and every other box reach the replica at all. |
| + if e, ok := got["r7x2k9m4qa.go.tito.io"]; !ok || e.box != "uk-1" { |
| + t.Fatalf("the replica's own host must still be dialable: %+v", e) |
| + } |
| +} |
| + |
| // A box with no discoverable public IP publishes its hosts list with eip:"" |
| // (Task 1's DR-runbook case). There is nothing to dial, so it must never be |
| // offered as a target — a "" EIP would otherwise become a dial to port 443 of |