edge: :80 redirects a remote-homed host instead of 404ing it
Carried from Task 4's review. httpFallbackHandler gated routable on lookupUpstream alone, so http://example.com at a non-home edge answered the unknown-host 404 — while https://example.com on the same edge backhauls to the home box and serves the app. A first-time visitor typing a bare hostname in the wrong region never reached it. routable now also consults the home map, through the same wildcard-capable Home() the dial uses: nil-safe (no deployment store, no answer) and own-box-excluding, so a single-box deployment is untouched and a nowhere-homed host keeps today's answer byte for byte. No hop logic on this path on purpose — the hop dials 443, so a verified hop can never arrive on :80 at all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+59
−4
internal/edge/edge.go+19 −4internal/edge/homemap_test.go+40 −0
diff --git a/internal/edge/edge.go b/internal/edge/edge.go| index 96ea213d1..bb69f7d43 100644 |
| --- a/internal/edge/edge.go |
| +++ b/internal/edge/edge.go |
| @@ -1300,10 +1300,11 @@ func (e *Edge) unknownHost(w http.ResponseWriter, r *http.Request) { |
| // fallback URL instead of a redirect into a doomed TLS handshake (no cert |
| // will ever exist for them). |
| // |
| -// This gate uses lookupUpstream, NOT Allowed: "does something answer this |
| -// host on 443?" is a ROUTING question (lookupUpstream's job, including its |
| -// one-label wildcard-alias fallback), not an issuance question (Allowed's |
| -// job, deliberately exact-only — see its own doc comment). A wildcard-alias |
| +// This gate uses lookupUpstream (and, since the backhaul, the home map), NOT |
| +// Allowed: "does something answer this host on 443?" is a ROUTING question |
| +// (lookupUpstream's job, including its one-label wildcard-alias fallback, plus |
| +// the deployment's other edges), not an issuance question (Allowed's job, |
| +// deliberately exact-only — see its own doc comment). A wildcard-alias |
| // host like paul.woodstar.app resolves fine on 443 via the wildcard cert, so |
| // plain http:// must redirect it rather than 404. This is safe to widen past |
| // Allowed's exact match because a redirect grants nothing: it's a 302 to the |
| @@ -1329,6 +1330,20 @@ func (e *Edge) httpFallbackHandler() http.Handler { |
| if !routable { |
| _, routable = e.lookupUpstream(host) |
| } |
| + if !routable { |
| + // A host this box does not serve but another box HOMES: 443 |
| + // backhauls it (the hop, edge.go's backhaul), so :80 must send the |
| + // visitor there rather than 404 an app that is up — a first-time |
| + // visitor typing a bare hostname lands on whichever edge DNS |
| + // picked, which in a steered deployment is routinely not the home. |
| + // Home() is the same wildcard-capable lookup the dial uses, is |
| + // nil-safe (no deployment store ⇒ no answer), and excludes this |
| + // box's own hosts, so a single-box deployment is untouched. |
| + // |
| + // No hop logic here on purpose: the hop dials 443, so a verified |
| + // hop can never arrive on this handler at all. |
| + _, _, routable = e.homes.Load().Home(host) |
| + } |
| if !routable { |
| e.unknownHost(w, r) |
| return |
diff --git a/internal/edge/homemap_test.go b/internal/edge/homemap_test.go| index dcbf19128..687a85927 100644 |
| --- a/internal/edge/homemap_test.go |
| +++ b/internal/edge/homemap_test.go |
| @@ -976,6 +976,46 @@ func TestBackhaulSharesOneTransportAcrossAWildcardHome(t *testing.T) { |
| } |
| } |
| +// The :80 half of the backhaul, carried from Task 4's review. A host homed on |
| +// another box is answered on 443 (the hop), so plain http:// must redirect it |
| +// there like any other routable host — otherwise a first-time visitor typing a |
| +// bare hostname in the wrong region gets a 404 for an app that is up. |
| +// |
| +// The redirect grants nothing that the 443 answer does not: it is a 302 to the |
| +// SAME host, never an ACME order (httpFallbackHandler's own doc records why |
| +// this gate is wider than Allowed's). And a verified hop can never arrive |
| +// here — the sending edge dials 443 — so there is no hop logic on this path. |
| +func TestHTTPFallbackRedirectsARemoteHomedHost(t *testing.T) { |
| + e, m, _ := hopEdge(t, "annie") // serves nothing itself |
| + homedOn(t, m, "betty", "2.2.2.2", "remote.test", "*.wild.test") |
| + h := e.httpFallbackHandler() |
| + |
| + for _, host := range []string{"remote.test", "paul.wild.test"} { |
| + req := httptest.NewRequest(http.MethodGet, "http://"+host+"/deep?q=1", nil) |
| + req.Host = host |
| + rec := httptest.NewRecorder() |
| + h.ServeHTTP(rec, req) |
| + if rec.Code != http.StatusFound { |
| + t.Fatalf("%s: status = %d, want 302 — the host is homed on another box and 443 backhauls it", host, rec.Code) |
| + } |
| + if loc := rec.Header().Get("Location"); loc != "https://"+host+"/deep?q=1" { |
| + t.Fatalf("%s: Location = %q", host, loc) |
| + } |
| + } |
| + |
| + // A host nothing in the deployment homes keeps today's answer exactly. |
| + req := httptest.NewRequest(http.MethodGet, "http://nowhere.test/", nil) |
| + req.Host = "nowhere.test" |
| + rec := httptest.NewRecorder() |
| + h.ServeHTTP(rec, req) |
| + if rec.Code != http.StatusNotFound { |
| + t.Fatalf("nowhere.test: status = %d, want the unchanged 404", rec.Code) |
| + } |
| + if loc := rec.Header().Get("Location"); loc != "" { |
| + t.Fatalf("nowhere.test: Location = %q, want none", loc) |
| + } |
| +} |
| + |
| // oneKeyFailingStore fails Get for one named key — a single unreadable object |
| // mid-poll, the case the carry-forward exists for. |
| type oneKeyFailingStore struct { |