carlos / platform Public

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>
Paul Campbell pushed by paul@keymail.dev 7bb1c764a731b8c1a294dda3b1763249f26f4790 parent 15c3673
2 files changed, +59 −4
  • internal/edge/edge.go +19 −4
  • internal/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 {