package edge import ( "maps" "net/http" "net/http/httptest" "slices" "testing" "github.com/carlosframework/platform/internal/registry" ) // upsertAlias writes one route_aliases row, the shape the reconcile pass // (internal/domains) derives from a claim record. The edge only ever reads // Alias and Host; account/app are carried for realism. func upsertAlias(t *testing.T, reg *registry.Registry, alias, host string) { t.Helper() if err := reg.UpsertAlias(registry.RouteAlias{ Alias: alias, Host: host, Account: "acct", App: "app", CreatedAt: 1, }); err != nil { t.Fatalf("upsert alias %s -> %s: %v", alias, host, err) } } // TestAliasServesPrimaryBackend is the feature itself: a hostname attached to // a ready route reaches that route's backend, with the customer's own Host // header preserved end to end (the app must see the name the browser asked // for, not the platform hostname). func TestAliasServesPrimaryBackend(t *testing.T) { backend, seenHost := hostEchoBackend(t, "hello from the primary") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "app.acct.oncarlos.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } srv := httptest.NewServer(e) defer srv.Close() code, body := getHost(t, srv.URL, "www.customer.example") if code != http.StatusOK { t.Fatalf("status = %d, want 200 for an attached hostname", code) } if body != "hello from the primary" { t.Fatalf("body = %q, want the primary route's backend body", body) } if *seenHost != "www.customer.example" { t.Fatalf("backend saw Host %q, want www.customer.example (inbound Host must be preserved)", *seenHost) } } // TestAliasSharesUpstreamEntry is the composition pin (#152, multi-upstream // HA): the alias must map to the primary's OWN entry — the identical proxy // handler value and the primary's route row — never a proxy rebuilt for the // alias hostname. Everything downstream is keyed off route.Host (activator // entry, lease, live symlink, cgroup) and off the proxy value's per-primary // state, so a second handler would silently fork both. func TestAliasSharesUpstreamEntry(t *testing.T) { backend, _ := hostEchoBackend(t, "primary") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "app.acct.oncarlos.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } m := e.current() primary, ok := m["app.acct.oncarlos.com"] if !ok { t.Fatalf("primary route missing from the upstream table") } alias, ok := m["www.customer.example"] if !ok { t.Fatalf("alias missing from the upstream table") } // Identity, not equivalent behaviour: the route carries no Version, so the // handler is the *httputil.ReverseProxy pointer itself and == compares the // pointer, which is exactly the claim being pinned. if alias.proxy != primary.proxy { t.Fatalf("alias proxy %p is not the primary's proxy %p — an alias must never get a rebuilt handler", alias.proxy, primary.proxy) } if alias.route.Host != "app.acct.oncarlos.com" { t.Fatalf("alias route.Host = %q, want the PRIMARY's host (activator/version/purge keying all read it)", alias.route.Host) } if !alias.alias { t.Fatalf("alias entry not marked alias — wildcard parent derivation would treat a customer domain as a platform host") } if primary.alias { t.Fatalf("primary entry marked alias") } } // TestAliasSharesMultiUpstreamEntry is TestAliasSharesUpstreamEntry against // the shape the #152 claim is actually about: a primary carrying an Addrs set // and a probe config, i.e. one served by multiProxy rather than the legacy // single-addr tcpProxy. Two things must hold, and only this shape can show // either. // // The alias must ride the primary's OWN multiProxy value. That handler is // closed over the PRIMARY's host and reads its live set from proberSet under // that key, so an alias handed a rebuilt proxy would look up a host nothing // probes, find an empty live set, and answer 503 + Retry-After for a route // whose upstreams are all healthy. // // And the alias must add NO prober of its own: probeSpecs is built from the // route pass only, before aliases are folded in, so an alias appearing there // would double every upstream's probe traffic and put a duplicate host on the // dashboard's per-upstream table. func TestAliasSharesMultiUpstreamEntry(t *testing.T) { addrA, _ := countingBackend(t, "A") addrB, _ := countingBackend(t, "B") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "ha.acct.oncarlos.com", Kind: "service", Status: "ready", Addrs: addrA + "," + addrB, Health: healthJSON(t, frozenHealth()), }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "www.customer.example", "ha.acct.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } defer e.probers.Stop() m := e.current() primary, ok := m["ha.acct.oncarlos.com"] if !ok { t.Fatalf("primary route missing from the upstream table") } alias, ok := m["www.customer.example"] if !ok { t.Fatalf("alias missing from the upstream table") } // Identity, as in the single-addr pin above: no Version on the route, so // the handler is the *httputil.ReverseProxy multiProxy returned and == // compares that pointer. if alias.proxy != primary.proxy { t.Fatalf("alias proxy %p is not the primary's multiProxy %p — a rebuilt handler would read an unprobed live set and 503 a healthy route", alias.proxy, primary.proxy) } if alias.route.Host != "ha.acct.oncarlos.com" { t.Fatalf("alias route.Host = %q, want the PRIMARY's host", alias.route.Host) } // Exactly one probed host, and it is the primary. st := e.probers.State() if len(st) != 1 { t.Fatalf("probed hosts = %v, want exactly one (the primary) — an alias must never get a prober of its own", slices.Sorted(maps.Keys(st))) } if _, ok := st["ha.acct.oncarlos.com"]; !ok { t.Fatalf("probed hosts = %v, want the primary probed", slices.Sorted(maps.Keys(st))) } // And the whole point of the identity: a request over the attached // hostname reaches a live upstream instead of multiProxy's dark-set 503. srv := httptest.NewServer(e) defer srv.Close() code, body := getHost(t, srv.URL, "www.customer.example") if code != http.StatusOK || (body != "A" && body != "B") { t.Fatalf("alias request = %d %q, want 200 from one of the primary's upstreams", code, body) } } // TestAliasAllowed covers the autocert HostPolicy: an attached hostname is a // host this box serves, so it may be certed; a stranger's SNI may not. func TestAliasAllowed(t *testing.T) { backend, _ := hostEchoBackend(t, "primary") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "app.acct.oncarlos.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } if !e.Allowed("www.customer.example") { t.Fatalf("Allowed(www.customer.example) = false, want true for an attached hostname") } if e.Allowed("stranger.example") { t.Fatalf("Allowed(stranger.example) = true, want false") } } // TestAliasNeverShadowsRoute: routes always win. An alias row whose hostname // collides with a real route must not displace it — the route's own backend // keeps answering. func TestAliasNeverShadowsRoute(t *testing.T) { one, _ := hostEchoBackend(t, "one") two, _ := hostEchoBackend(t, "two") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "one.example.com", Kind: "service", Addr: one.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert one: %v", err) } if err := reg.Upsert(registry.Route{ Host: "two.example.com", Kind: "service", Addr: two.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert two: %v", err) } // An alias claiming a hostname that is already a route of its own. upsertAlias(t, reg, "two.example.com", "one.example.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } srv := httptest.NewServer(e) defer srv.Close() code, body := getHost(t, srv.URL, "two.example.com") if code != http.StatusOK || body != "two" { t.Fatalf("got %d %q, want 200 \"two\" — a real route must win a collision with an alias", code, body) } if up := e.current()["two.example.com"]; up.alias { t.Fatalf("route entry for two.example.com was overwritten by the alias") } } // TestAliasSkipsMissingPrimary: an alias whose primary is not in the table // (not ready here, or not local to this box) is skipped entirely rather than // mapped to nothing — it must 404 as an unknown host and must not be certed. func TestAliasSkipsMissingPrimary(t *testing.T) { backend, _ := hostEchoBackend(t, "should never be reached") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "pending.acct.oncarlos.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "pending", }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "www.customer.example", "pending.acct.oncarlos.com") // And one pointing at a host with no route row at all. upsertAlias(t, reg, "gone.customer.example", "nosuch.acct.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } srv := httptest.NewServer(e) defer srv.Close() for _, host := range []string{"www.customer.example", "gone.customer.example"} { if code, _ := getHost(t, srv.URL, host); code != http.StatusNotFound { t.Fatalf("status for %s = %d, want 404 (alias with no ready primary)", host, code) } if e.Allowed(host) { t.Fatalf("Allowed(%s) = true, want false — no certificate for an alias to nowhere", host) } } } // routeRecordingActivator records the route each wake was handed, so a test // can prove which host the activator was keyed on. type routeRecordingActivator struct { routes []registry.Route } func (a *routeRecordingActivator) ServeHTTP(w http.ResponseWriter, r *http.Request, rt registry.Route, proxy http.Handler) bool { a.routes = append(a.routes, rt) proxy.ServeHTTP(w, r) // "wake" is instant; proxy straight through return true } func (a *routeRecordingActivator) RetireExcept(keep map[string]bool) {} // TestAliasWakesPrimaryOnce is the #129-class regression pin: a request to an // attached hostname wakes the PRIMARY's single activator entry. The activator // keys its entry, lease, live symlink and cgroup on route.Host, so an alias // arriving with its own host would start a second copy of the app beside the // first and fight it for the socket. func TestAliasWakesPrimaryOnce(t *testing.T) { backend, _ := hostEchoBackend(t, "woken") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "hib.acct.oncarlos.com", Kind: "instance", Addr: backend.Listener.Addr().String(), Status: "ready", Hibernate: true, }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "www.customer.example", "hib.acct.oncarlos.com") act := &routeRecordingActivator{} e, err := New(reg, act) if err != nil { t.Fatalf("New: %v", err) } srv := httptest.NewServer(e) defer srv.Close() code, body := getHost(t, srv.URL, "www.customer.example") if code != http.StatusOK || body != "woken" { t.Fatalf("got %d %q, want 200 woken", code, body) } if len(act.routes) != 1 { t.Fatalf("activator saw %d wakes, want exactly 1", len(act.routes)) } if got := act.routes[0].Host; got != "hib.acct.oncarlos.com" { t.Fatalf("activator was handed route.Host = %q, want the primary hib.acct.oncarlos.com", got) } } // TestCurrentHostsSkipsAliases: wildcard parent derivation reads currentHosts, // so a customer hostname riding a platform-minted primary must never appear // there — otherwise a name like x.y. could mint a wildcard parent // and burn an ACME issuance on a domain the wildcard never covered. func TestCurrentHostsSkipsAliases(t *testing.T) { backend, _ := hostEchoBackend(t, "primary") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "app.acct.oncarlos.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "ready", Platform: true, }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "shop.acct.oncarlos.com", "app.acct.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } got := e.currentHosts() if !slices.Contains(got, "app.acct.oncarlos.com") { t.Fatalf("currentHosts = %v, want it to contain the platform-minted primary", got) } if slices.Contains(got, "shop.acct.oncarlos.com") { t.Fatalf("currentHosts = %v, must NOT contain an attached hostname", got) } } // TestWildcardAliasRequestRouting is Task 4's sub-change 3: a catch-all // alias row ("*.", the shape Task 3's reconcile pass derives from a // claim's Catchall+Wildcard capability) must resolve one-label subdomains at // request time, folding into the SAME upstream map entry the exact primary // host resolves to (#151/#152's "identical map value" rule — activator // keying, version header and purge tenancy all read off that one entry). // Exact hosts always win a collision with the wildcard fallback, a wildcard // covers exactly one label (mirroring the TLS cert), and the apex must never // match its own wildcard. func TestWildcardAliasRequestRouting(t *testing.T) { backend, seenHost := hostEchoBackend(t, "hello from the primary") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "woodstar-relay.bab.oncarlos.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "*.woodstar.app", "woodstar-relay.bab.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } // Entry identity: paul.woodstar.app must resolve to the SAME upstream // map value as the primary host — same *httputil.ReverseProxy pointer, // same route row — not a value that merely behaves the same. m := e.current() primary, ok := m["woodstar-relay.bab.oncarlos.com"] if !ok { t.Fatalf("primary route missing from the upstream table") } star, ok := m["*.woodstar.app"] if !ok { t.Fatalf("catch-all alias missing from the upstream table") } if star.proxy != primary.proxy { t.Fatalf("*.woodstar.app proxy %p is not the primary's proxy %p", star.proxy, primary.proxy) } if star.route.Host != primary.route.Host { t.Fatalf("*.woodstar.app route.Host = %q, want the primary's %q", star.route.Host, primary.route.Host) } up, ok := e.lookupUpstream("paul.woodstar.app") if !ok { t.Fatalf("lookupUpstream(paul.woodstar.app) missed") } if up.proxy != primary.proxy { t.Fatalf("lookupUpstream(paul.woodstar.app) resolved to a different entry than the primary (proxy %p vs %p)", up.proxy, primary.proxy) } srv := httptest.NewServer(e) defer srv.Close() code, body := getHost(t, srv.URL, "paul.woodstar.app") if code != http.StatusOK || body != "hello from the primary" { t.Fatalf("got %d %q, want 200 from the primary's backend", code, body) } if *seenHost != "paul.woodstar.app" { t.Fatalf("backend saw Host %q, want paul.woodstar.app (inbound Host preserved)", *seenHost) } // Two labels deep: a wildcard covers exactly one label, mirroring the // TLS cert — a.b.woodstar.app must miss. if _, ok := e.lookupUpstream("a.b.woodstar.app"); ok { t.Fatalf("lookupUpstream(a.b.woodstar.app) hit, want a miss (two labels — wildcard covers one)") } // The apex itself must never match its own wildcard: "woodstar.app" has // no dot before "app", so the fallback would (wrongly) probe "*.app", // never "*.woodstar.app" — this pins that the apex is never routed by // its own catch-all. if _, ok := e.lookupUpstream("woodstar.app"); ok { t.Fatalf("lookupUpstream(woodstar.app) hit, want a miss (apex must not match its own wildcard)") } // An exact alias row wins over the wildcard fallback. exactBackend, _ := hostEchoBackend(t, "exact wins") if err := reg.Upsert(registry.Route{ Host: "other.bab.oncarlos.com", Kind: "service", Addr: exactBackend.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert other: %v", err) } upsertAlias(t, reg, "paul.woodstar.app", "other.bab.oncarlos.com") if err := e.Reload(); err != nil { t.Fatalf("reload: %v", err) } up, ok = e.lookupUpstream("paul.woodstar.app") if !ok { t.Fatalf("lookupUpstream(paul.woodstar.app) missed after exact alias added") } if up.route.Host != "other.bab.oncarlos.com" { t.Fatalf("lookupUpstream(paul.woodstar.app) route.Host = %q, want the EXACT alias's primary other.bab.oncarlos.com", up.route.Host) } } // TestHostPolicyExcludesWildcardFallback is the CRITICAL-1 review fix: the // autocert HostPolicy (Allowed) must stay on the EXACT map only, never // lookupUpstream's wildcard fallback. A catch-all alias ("*.") gets // its TLS coverage from the WILDCARD cert (wrapGetCertificate checks certFor // before ever reaching autocert) — Allowed only gates a NEW per-host ACME // order, which is unbounded (attacker-chosen labels) and shared across every // tenant on the box (one ACME account). Before this fix, a stranger's SNI // random.woodstar.app would pass Allowed and drive a fresh per-host order // for as long as the wildcard cert wasn't yet obtained — the normal state // right after an attach, before the customer's CNAME exists. // // Request-time routing (lookupUpstream, unaffected by this fix) must still // resolve the same one-label wildcard host — the policy split is HostPolicy // vs. serving, not a routing regression. func TestHostPolicyExcludesWildcardFallback(t *testing.T) { backend, _ := hostEchoBackend(t, "primary") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "woodstar-relay.bab.oncarlos.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "*.woodstar.app", "woodstar-relay.bab.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } // (a) Request routing is unchanged: the wildcard-alias host still // resolves at request time. if _, ok := e.lookupUpstream("paul.woodstar.app"); !ok { t.Fatalf("lookupUpstream(paul.woodstar.app) missed — routing must still resolve via the wildcard alias") } // (b) HostPolicy refuses ANY one-label host under the catch-all that // isn't an exact route/alias row of its own — only exact hosts are // certable via the per-host path. if e.Allowed("random.woodstar.app") { t.Fatalf("Allowed(random.woodstar.app) = true, want false — HostPolicy must not follow the wildcard fallback (unbounded per-host ACME orders from attacker-chosen SNI)") } if e.Allowed("paul.woodstar.app") { t.Fatalf("Allowed(paul.woodstar.app) = true, want false — same rule, no exact row for this host either") } // An EXACT alias row is still certable, same as any other routed host. upsertAlias(t, reg, "paul.woodstar.app", "woodstar-relay.bab.oncarlos.com") if err := e.Reload(); err != nil { t.Fatalf("reload: %v", err) } if !e.Allowed("paul.woodstar.app") { t.Fatalf("Allowed(paul.woodstar.app) = false, want true once it has its OWN exact alias row") } // The still-wildcard-only host stays refused. if e.Allowed("random.woodstar.app") { t.Fatalf("Allowed(random.woodstar.app) = true, want false — still no exact row for it") } // The wildcard label itself must never be certable — even though it is // the exact map key backing the catch-all fallback, refused locally // rather than resting on autocert's own IDN rejection of "*". if e.Allowed("*.woodstar.app") { t.Fatalf("Allowed(*.woodstar.app) = true, want false — the wildcard label itself must never be certable") } } // TestHTTPFallbackRedirectsWildcardAlias is the round-2 review fix: the :80 // handler (httpFallbackHandler) is a THIRD Allowed() consumer the first fix // pass missed. It answers a routing question ("does something serve this // host on 443?"), not an issuance question, so it must use lookupUpstream — // any host that routes via the *.woodstar.app catch-all (paul.woodstar.app, // random.woodstar.app, any label) gets the classic 302 to https, even though // none of them has its own exact row and Allowed() (HostPolicy) refuses all // of them. A host that doesn't route at all still gets unknownHost (404 with // no fallback URL configured). func TestHTTPFallbackRedirectsWildcardAlias(t *testing.T) { backend, _ := hostEchoBackend(t, "primary") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "woodstar-relay.bab.oncarlos.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "ready", }); err != nil { t.Fatalf("upsert: %v", err) } upsertAlias(t, reg, "*.woodstar.app", "woodstar-relay.bab.oncarlos.com") e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } h := e.httpFallbackHandler() for _, host := range []string{"paul.woodstar.app", "random.woodstar.app"} { req := httptest.NewRequest(http.MethodGet, "http://"+host+"/", nil) req.Host = host rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("%s: status = %d, want 302 — it routes via the *.woodstar.app catch-all even with no exact row", host, rec.Code) } if loc := rec.Header().Get("Location"); loc != "https://"+host+"/" { t.Fatalf("%s: Location = %q, want https://%s/", host, loc, host) } } // A host that doesn't route at all (no exact route, no matching // wildcard alias) still hits unknownHost — no fallback URL configured // here, so a plain 404. req := httptest.NewRequest(http.MethodGet, "http://nowhere.example.com/", nil) req.Host = "nowhere.example.com" rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("nowhere.example.com: status = %d, want 404 (unknownHost, no fallback URL)", rec.Code) } } // TestHTTPFallbackRedirectsTunnelHost is the final-review fix: the old inline // :80 closure gated on Allowed(host), which special-cases the reserved tunnel // hostname (see Allowed's own e.tunnels check); the extracted handler gates // on lookupUpstream alone, which has no tunnel branch at all, so plain // http:/// regressed to unknownHost instead of the classic // redirect into the hub's own TLS handshake on 443. func TestHTTPFallbackRedirectsTunnelHost(t *testing.T) { e, err := New(openReg(t), nil) if err != nil { t.Fatalf("New: %v", err) } e.EnableTunnels(NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"})) if err := e.Reload(); err != nil { t.Fatalf("reload: %v", err) } h := e.httpFallbackHandler() req := httptest.NewRequest(http.MethodGet, "http://tunnel.test/register", nil) req.Host = "tunnel.test" rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("tunnel.test: status = %d, want 302 — the reserved tunnel hostname answers on 443 and must redirect like any other routable host", rec.Code) } if loc := rec.Header().Get("Location"); loc != "https://tunnel.test/register" { t.Fatalf("tunnel.test: Location = %q, want https://tunnel.test/register", loc) } }