carlos / platform Public

feat: edge serves attached hostnames off the primary route's entry (#151)

Reload maps every route_aliases row onto the primary's OWN upstream entry:
the identical proxy value and the primary's route row, so the activator's
entry/lease/live-symlink/cgroup keying, the version header and purge tenancy
all stay the primary's. Routes always win a name collision (logged); an alias
whose primary isn't in the table (not ready, not local) is skipped rather
than routed to nowhere; a ListAliases error aborts the whole reload, leaving
the old table serving, like every other registry read here.

upstream gains an `alias` flag so currentHosts (wildcard parent derivation)
never sees a customer hostname riding a platform-minted primary's row.
ServeHTTP is untouched — the map lookup already covers aliases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell f3eb2ea315fec11a9fd8866c7937547ea2c30194 parent e2539b7
2 files changed, +312 −1
  • internal/edge/alias_test.go +277 −0
  • internal/edge/edge.go +35 −1
diff --git a/internal/edge/alias_test.go b/internal/edge/alias_test.go
new file mode 100644
index 000000000..0951bc54f
--- /dev/null
+++ b/internal/edge/alias_test.go
@@ -0,0 +1,277 @@
+package edge
+
+import (
+ "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")
+ }
+}
+
+// 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.<apps-domain> 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)
+ }
+}
diff --git a/internal/edge/edge.go b/internal/edge/edge.go
index 219e8697e..f2ff4805a 100644
--- a/internal/edge/edge.go
+++ b/internal/edge/edge.go
@@ -150,6 +150,12 @@ type upstream struct {
// whether its process is live (it's the edge's child), so "dead backend"
// detection is the activator's state table, not a failed dial.
hibernating bool
+ // alias marks an entry mapped in from route_aliases: it shares the
+ // primary's proxy/route/hibernating verbatim (so hibernation wakes the
+ // primary's actEntry and #152's per-primary prober state applies), and
+ // the flag exists so wildcard parent derivation (currentHosts) never
+ // treats a customer hostname as a platform host.
+ alias bool
}
// New builds an Edge over reg and performs the startup reload. act may be nil
@@ -418,6 +424,29 @@ func (e *Edge) Reload() error {
}
m[t.Host] = upstream{proxy: proxy, route: t, hibernating: hib}
}
+ // Attached hostnames (#151): each alias resolves to the primary's OWN
+ // entry — same proxy value, same route (so activator keying, version
+ // header, and purge tenancy are all the primary's). Routes always win
+ // a collision; an alias whose primary isn't in m (not ready, not
+ // local) is skipped — never a 502 route to nowhere. Errors abort the
+ // reload like every other registry read here: a transient failure must
+ // leave the old table serving.
+ aliases, err := e.reg.ListAliases()
+ if err != nil {
+ return err
+ }
+ for _, a := range aliases {
+ if _, taken := m[a.Alias]; taken {
+ log.Printf("edge: alias %s shadowed by a real route — skipped", a.Alias)
+ continue
+ }
+ up, ok := m[a.Host]
+ if !ok {
+ continue
+ }
+ up.alias = true
+ m[a.Alias] = up
+ }
// Fleet channel auth: registry-backed, so this refreshes on the SAME poll
// that rebuilds routes — Task 1's Fingerprint moves on every fleet
// mutation (including token rotation), and the edge's existing 2s
@@ -488,11 +517,16 @@ func (e *Edge) current() map[string]upstream {
// never meant to cover. Platform-minted app routes set Platform:true; static
// parents (e.g. tito's go.tito.io) come from CARLOS_WILDCARD_DOMAINS via env,
// so they are unaffected by this filter.
+//
+// Attached hostnames (#151) are excluded by the !up.alias half: an alias
+// entry carries the PRIMARY's route row verbatim, Platform flag included, so
+// without the flag a customer domain would inherit its platform primary's
+// trust and mint a parent for a zone the platform doesn't own.
func (e *Edge) currentHosts() []string {
m := e.current()
out := make([]string, 0, len(m))
for h, up := range m {
- if up.route.Platform {
+ if up.route.Platform && !up.alias {
out = append(out, h)
}
}