Skip to content

relay: support host aliases so in-cluster clients can bind to a community#2527

Open
jedwards27 wants to merge 4 commits into
block:mainfrom
jedwards27:judeedwards/relay-host-aliases
Open

relay: support host aliases so in-cluster clients can bind to a community#2527
jedwards27 wants to merge 4 commits into
block:mainfrom
jedwards27:judeedwards/relay-host-aliases

Conversation

@jedwards27

Copy link
Copy Markdown

relay: support host aliases so in-cluster clients can bind to a community

Problem

Since the multi-tenant communities cutover (Jul 4), the relay binds every request to a community strictly by the request Host header against communities.host. In-cluster clients — e.g. Blox workstations using the sprout-backend-blox provider — can only reach the relay via the cluster-local service host sprout-relay.sprout.svc.cluster.local:3000 (Blox egress cannot reach the Cloudflare-fronted external URL). That host has no community mapping, so every in-cluster connection fails closed with:

404 relay: no community is configured for this host

Workarounds don't exist client-side:

  • Spoofing the external Host header gets 502'd by the Envoy mesh before the relay ever sees it (the mesh routes by Host).
  • WebSocket connections can't override Host at all, so even a relay-side forwarded-host hack wouldn't cover the primary (WS) traffic.

This blocks all Blox-hosted Buzz agents. Re-verified today (Jul 23) from a fresh staging workstation — still reproducible.

Design

Adds a "CNAME equivalent": one community reachable via multiple hostnames.

  • New table community_host_aliases (migrations/0025_community_host_aliases.sql, mirrored in schema/schema.sql): (host, community_id → communities.id), unique on lower(host), with guards preventing an alias colliding with any communities.host (and vice versa on community create — collision surfaces as a distinguishable error, not a silent failure).
  • Resolver fallback: the HostResolver DB implementation resolves against communities.host first, then community_host_aliases. Fail-closed semantics preserved — unmapped hosts still return the same generic 404; no default tenant introduced.
  • Tenant binding invariants preserved: TenantContext still binds the request host (aliases do not rewrite tenant.host()), so NIP-98 u-URL matching and AUTH origin checks continue to compare against the host the client actually used. Invariant comments in tenant.rs / api/bridge.rs updated accordingly.
  • Admin surface: buzz-admin community alias add|remove|list, managing aliases for the tenant resolved from RELAY_URL (admin tenant resolution also resolves through aliases now).

Operational rollout

After deploy, map the cluster-local host to the existing community:

buzz-admin community alias add sprout-relay.sprout.svc.cluster.local:3000

No client changes needed — in-cluster agents keep using ws://sprout-relay.sprout.svc.cluster.local:3000.

Testing

  • Unit tests: alias resolves to the same community; unknown host still fails closed; alias/primary-host collisions rejected in both directions (including the create_community_with_owner path).
  • DB-backed integration tests run against Postgres locally (all pass; one pre-existing clock-precision flake in replica_fence reproduces identically on unmodified main).
  • Multitenant conformance extended (conformance_multitenant.rs): with communities A and B, an alias for A binds to A and does not leak into B.
  • cargo fmt / clippy clean; 3 passes of sq agents review completed with all findings addressed.

🤖 This PR was authored by Jude Edwards' AI agent (reviewed workflow, human-supervised). cc @tlongwell — this is the Blox relay-binding issue from our DM; figured we'd take a crack at the fix rather than just re-ping. Happy to adjust the approach if you had something different in mind for the cname-equivalent.

jedwards27 and others added 4 commits July 23, 2026 14:13
…nity

Blox in-cluster clients can only reach the relay via a cluster-local
Service DNS host, which has no communities.host row and 404s under
row-zero binding. Host-header spoofing can't fix this: the mesh routes
by Host (502 on mismatch) and WebSocket clients can't override it.

Add community_host_aliases (unique on lower(host), guarded against
collision with communities.host in both directions) and fall back to
it in Db::lookup_community_by_host_or_alias after the primary host
lookup misses. bind_community still binds the request's own host into
TenantContext, so NIP-98/NIP-42 host checks are unaffected by aliasing.

Add `buzz-admin community alias add|remove|list` to manage aliases for
the tenant resolved from RELAY_URL.

Co-Authored-By: Claude <noreply@anthropic.com>
Serialize the community/alias collision triggers with a shared advisory
lock keyed on the lowercased host, closing a race where concurrent
inserts to communities and community_host_aliases for the same host
could both pass their EXISTS checks before either commits. Also make
the operator availability endpoint check community_host_aliases so an
alias-reserved host reports available: false instead of letting a
create attempt fail on the trigger. Found by sq agents review.

Co-Authored-By: Claude <noreply@anthropic.com>
…errors

A host reserved via community_host_aliases has no communities.host row,
so ON CONFLICT (lower(host)) never catches it; the alias-collision
trigger raises unique_violation from the INSERT instead. That was
bubbling up as a raw database error rather than the typed HostExists
result the operator API expects for a taken host. Catch the 23505 and
fold it into HostExists. Found by sq agents review.

Co-Authored-By: Claude <noreply@anthropic.com>
Two more paths could hit an alias-reserved host without translating
migration 0025's trigger error into something callers can act on:

- ensure_configured_community (the operator API's legacy convergence
  mode, and startup community seeding) let the trigger's raw database
  error propagate as an internal 500 instead of the usual "community
  already exists" conflict. Added DbError::HostAliasCollision and
  mapped it in community_provisioning.rs.
- resolve_admin_tenant in buzz-admin only checked communities.host,
  so running buzz-admin with RELAY_URL set to an alias (the exact
  in-cluster case this feature exists for) couldn't resolve its own
  tenant. Switched it to lookup_community_by_host_or_alias.

Found by a third sq agents review pass; this closes out all findings
from all three passes.

Co-Authored-By: Claude <noreply@anthropic.com>
@jedwards27
jedwards27 marked this pull request as ready for review July 23, 2026 15:58
@jedwards27
jedwards27 requested a review from a team as a code owner July 23, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant