carlos / platform Public

edge: the edge-local door, dormant until CARLOS_EDGE_DOOR (#175 P7)

Tasks 5-7 of the P7 plan. A door request is a request from this box: the
door authenticates X-Carlos-Door-Key, strips it, the mesh's hop headers and
every header that claims a client address, and serves the request with the
box's own public address as its peer, so every framework resolves the same
address a public request from the box would carry. ServeHTTP keeps door
callers off the tunnel and steering-probe names, checks ownership against
the final route (after the gate login's re-resolution), and on a miss
backhauls only a host whose published owner is the caller, to the target
that same Owner read returned. The door opens after the home map and the
key mirror are armed, only with a store, a public key and a public address,
and closes before the HTTP servers on shutdown.

Built by an Opus subagent from the plan (it mutation-tested each check);
reviewed before commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Human

Prompt

Go ahead. (start P7)

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

P7 Tasks 5-7: ownership enforcement, the door handler and listener, and lifecycle.
claude agent, operated by paul@keymail.dev 1fd2c93913ecb8fdb0d824d4b107bb00742190ee parent 99516ad
7 files changed, +1503 −23
  • internal/edge/backhaul_e2e_test.go +10 −0
  • internal/edge/door.go +243 −0
  • internal/edge/door_test.go +1085 −0
  • internal/edge/edge.go +82 −2
  • internal/edge/homemap.go +8 −0
  • internal/host/agent.go +41 −21
  • internal/host/agent_test.go +34 −0

CI passed — run details

diff --git a/internal/edge/backhaul_e2e_test.go b/internal/edge/backhaul_e2e_test.go
index 5716803e0..d492b374d 100644
--- a/internal/edge/backhaul_e2e_test.go
+++ b/internal/edge/backhaul_e2e_test.go
@@ -240,6 +240,12 @@ type backhaulEdge struct {
calls atomic.Int64
// plainCalls is the same for :80.
plainCalls atomic.Int64
+ // peerAs, when set to an address, is what this box's 443 door reports as
+ // the TCP peer of every request (the port is kept). A sending box dials
+ // from its own public address in production; on loopback every box is
+ // 127.0.0.1, so a test that needs the peer to be that address says so
+ // here. Unset, the peer is the real one.
+ peerAs atomic.Value // string
client *http.Client
}
@@ -288,6 +294,10 @@ func newBackhaulEdge(t *testing.T, bucketDir, box string, ca *e2eCA, from *net.T
// 443: the edge itself, counted.
https := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
be.calls.Add(1)
+ if p, _ := be.peerAs.Load().(string); p != "" {
+ _, port, _ := net.SplitHostPort(r.RemoteAddr)
+ r.RemoteAddr = net.JoinHostPort(p, port)
+ }
e.ServeHTTP(w, r)
}))
https.TLS = tlsCfg
diff --git a/internal/edge/door.go b/internal/edge/door.go
new file mode 100644
index 000000000..96c6bad23
--- /dev/null
+++ b/internal/edge/door.go
@@ -0,0 +1,243 @@
+package edge
+
+// The edge-local door (#175 P7): a second listener, on a box-local unix
+// socket, through which an app's own processes reach any of the app's
+// instances, on this box or another, with an app-scoped door key.
+//
+// A door request is a request from this box (the P7 plan's Security model,
+// Paul's ruling of 2026-09-28). The door believes no caller-supplied visitor
+// address: it deletes every header that claims one and presents the box's own
+// public address, the one its edges file publishes. That is what the app would
+// see if the calling process requested the instance's public URL, since that
+// request would leave the box and come back through an edge from the same
+// address. So the door grants nothing a public request from this box lacks.
+// The key says which app is calling and confines it to that app's hosts; it
+// confers no privilege.
+//
+// The enforcement lives in ServeHTTP (edge.go), keyed on the door mark this
+// file puts on the request context:
+//
+// - the tunnel and steering-probe hostnames are unknown hosts to a door
+// caller, in any spelling, and their handlers never run;
+// - a host this box does not serve is dialled only when the owner its home
+// box published is the caller's app, and only at the box Owner named;
+// - a host this box serves is checked against the FINAL upstream, after
+// the gate's login re-resolution, before the gate, the cache and the
+// dispatch.
+//
+// Nothing carries the mark across a hop. A backhauled door request is an
+// ordinary hop, and acceptHop is unchanged.
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/carlosframework/platform/internal/doorkey"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// doorCallerKey marks a request that came through the door. A context value
+// and never a header, for hopVerifiedKey's reason: nothing outside serveDoor
+// can put it on a request.
+type doorCallerKey struct{}
+
+// doorCaller is the app a door key admitted.
+type doorCaller struct{ Account, App string }
+
+func doorCallerFrom(ctx context.Context) (doorCaller, bool) {
+ c, ok := ctx.Value(doorCallerKey{}).(doorCaller)
+ return c, ok
+}
+
+// doorOwnsRoute is the whole ownership rule: the route's account and app are
+// the caller's.
+func doorOwnsRoute(c doorCaller, rt registry.Route) bool {
+ return release.RouteAccount(rt) == c.Account && rt.App == c.App
+}
+
+// doorReservedHost reports whether host names one of the handlers ServeHTTP
+// answers ahead of the route lookup: the tunnel hub (and the store door on
+// its hostname) and the steering probe. Compared lower-cased with one
+// trailing dot stripped, the normalization steeringProbe and
+// canonicalizeHopHost use, so a door caller cannot reach either through a
+// spelling the public branches would not match and a wildcard route would.
+func (e *Edge) doorReservedHost(host string) bool {
+ h := strings.ToLower(strings.TrimSuffix(host, "."))
+ if e.tunnels != nil && h == strings.ToLower(strings.TrimSuffix(e.tunnels.Host(), ".")) {
+ return true
+ }
+ return e.steeringHost != "" && h == strings.ToLower(strings.TrimSuffix(e.steeringHost, "."))
+}
+
+// doorAddressHeaders are the client-address headers beyond the four
+// stripForwardedHeaders deletes, each read as the visitor's address by some
+// framework (Rails reads Client-IP; python-ipware reads X-Forwarded and
+// Forwarded-For among others).
+var doorAddressHeaders = []string{"X-Forwarded", "Forwarded-For", "Client-IP", "X-Real-IP", "True-Client-IP",
+ "CF-Connecting-IP", "Fastly-Client-IP", "X-Client-IP", "X-Cluster-Client-IP", "X-Originating-IP"}
+
+// serveDoor is the door's handler: authenticate, rewrite, and hand the
+// request to ServeHTTP marked as a door caller.
+func (e *Edge) serveDoor(w http.ResponseWriter, r *http.Request) {
+ in, ok := e.doorRequest(r)
+ if !ok {
+ doorRefused(w)
+ return
+ }
+ e.ServeHTTP(w, in)
+}
+
+// doorRequest is serveDoor's first half, split out so a test can read the
+// request ServeHTTP will see. It always strips the key and the mesh's
+// headers; ok is false when the key is not admitted.
+func (e *Edge) doorRequest(r *http.Request) (*http.Request, bool) {
+ raw := r.Header.Get(doorkey.Header)
+ r.Header.Del(doorkey.Header)
+ // The mesh's vocabulary is never a door caller's to speak.
+ r.Header.Del(hopHeader)
+ r.Header.Del(hopAuthHeader)
+ tok, err := doorkey.ParseToken(raw)
+ if err != nil {
+ return nil, false
+ }
+ rec, ok := e.doorKeys.Record(tok.Account, tok.App)
+ if !ok || !rec.Admits(tok) {
+ return nil, false
+ }
+ // No visitor address is believed: every header that claims one goes,
+ // and the request comes from this box's own public address, exactly as
+ // it would through the public URL.
+ stripForwardedHeaders(r)
+ for _, h := range doorAddressHeaders {
+ r.Header.Del(h)
+ }
+ r.RemoteAddr = net.JoinHostPort(e.doorAddr, "0")
+ return r.WithContext(context.WithValue(r.Context(), doorCallerKey{}, doorCaller{Account: tok.Account, App: tok.App})), true
+}
+
+// doorRefused answers a missing, malformed, unknown or revoked key. The body
+// is the same for all four, so the answer says nothing about which keys or
+// apps exist.
+func doorRefused(w http.ResponseWriter) {
+ w.Header().Set("Cache-Control", "no-store")
+ http.Error(w, "door key refused", http.StatusUnauthorized)
+}
+
+// usableDoorAddr reports whether addr can stand for this box on the public
+// internet: a global unicast address that is not private. Loopback, a
+// private address or a placeholder would read as local or trusted to some
+// framework, which is the one thing the door must never present.
+func usableDoorAddr(addr string) bool {
+ ip := net.ParseIP(addr)
+ return ip != nil && ip.IsGlobalUnicast() && !ip.IsPrivate()
+}
+
+// openDoor listens on path and serves the door there until closeDoor. It
+// refuses with no key mirror (every key would be refused, and a door that
+// answers nothing but 401 hides a wiring fault) and with no usable public
+// address. The socket is 0660; its group comes from /run/carlos's setgid
+// carlos-sockets (bootstrap.go), so nothing here changes group.
+func (e *Edge) openDoor(path string) (closeDoor func(), err error) {
+ if e.doorKeys == nil {
+ return nil, errors.New("no door-key mirror")
+ }
+ if !usableDoorAddr(e.doorAddr) {
+ return nil, fmt.Errorf("%q is not a public address", e.doorAddr)
+ }
+ // A socket left by a previous run is replaced. Anything else at the
+ // path is not ours to remove.
+ if fi, err := os.Lstat(path); err == nil {
+ if fi.Mode()&os.ModeSocket == 0 {
+ return nil, fmt.Errorf("%s exists and is not a socket", path)
+ }
+ if err := os.Remove(path); err != nil {
+ return nil, err
+ }
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return nil, err
+ }
+ ln, err := net.Listen("unix", path)
+ if err != nil {
+ return nil, err
+ }
+ if err := os.Chmod(path, 0o660); err != nil {
+ ln.Close()
+ os.Remove(path)
+ return nil, err
+ }
+ // ReadHeaderTimeout for the public listeners' Slowloris reason; no
+ // Read/WriteTimeout, which would cut long-lived streams.
+ srv := &http.Server{Handler: http.HandlerFunc(e.serveDoor), ReadHeaderTimeout: 10 * time.Second}
+ go func() {
+ if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ log.Printf("edge: edge-local door on %s stopped: %v", path, err)
+ }
+ }()
+ var once sync.Once
+ return func() {
+ once.Do(func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ _ = srv.Shutdown(ctx)
+ _ = os.Remove(path)
+ })
+ }, nil
+}
+
+// armDoor builds the key mirror, lands one refresh, starts its loop and opens
+// the door. RunWithActivatorReady calls it once the home map is stored and
+// running, so the first door request meets both mirrors. A door that cannot
+// open says why in the journal and returns the reason; with no door path it
+// does nothing.
+func (e *Edge) armDoor(ctx context.Context) error {
+ if e.doorPath == "" {
+ return nil
+ }
+ var why error
+ switch {
+ case e.staticStore == nil:
+ why = errors.New("no deployment store to read door keys from")
+ case len(e.staticPub) == 0:
+ why = errors.New("no release public key to verify door keys against")
+ case !usableDoorAddr(e.doorAddr):
+ why = fmt.Errorf("no usable public address for this box (%q)", e.doorAddr)
+ }
+ if why != nil {
+ log.Printf("edge: edge-local door off: %v", why)
+ return why
+ }
+ keys := newDoorKeyMirror(e.staticStore, e.staticPub)
+ rctx, cancel := context.WithTimeout(ctx, doorKeyTimeout)
+ if err := keys.Refresh(rctx); err != nil {
+ log.Printf("edge: first door-key refresh failed: %v. Every door key is refused until a later poll lands", err)
+ }
+ cancel()
+ e.doorKeys = keys
+ stop := make(chan struct{})
+ go keys.run(stop)
+ closeSrv, err := e.openDoor(e.doorPath)
+ if err != nil {
+ close(stop)
+ e.doorKeys = nil
+ log.Printf("edge: edge-local door off: cannot open %s: %v", e.doorPath, err)
+ return err
+ }
+ var once sync.Once
+ e.closeDoor = func() {
+ once.Do(func() {
+ closeSrv()
+ close(stop)
+ })
+ }
+ log.Printf("edge: edge-local door open on %s. Door requests arrive from %s", e.doorPath, e.doorAddr)
+ return nil
+}
diff --git a/internal/edge/door_test.go b/internal/edge/door_test.go
new file mode 100644
index 000000000..7abee0793
--- /dev/null
+++ b/internal/edge/door_test.go
@@ -0,0 +1,1085 @@
+package edge
+
+// The edge-local door (#175 P7, Tasks 5 to 7). Every request here is driven
+// through serveDoor, the door's own handler, exactly as the unix listener
+// hands it one, except where a test says it drives the real socket.
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/doorkey"
+ "github.com/carlosframework/platform/internal/gate"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// doorTestAddr is the box's public address in these tests. It is TEST-NET-3
+// (RFC 5737): global unicast and not private by Go's rules, so openDoor
+// accepts it.
+const doorTestAddr = "203.0.113.10"
+
+// ---- the upstream -----------------------------------------------------------
+
+// doorUpstream is a tenant app that records everything the edge handed it.
+type doorUpstream struct {
+ mu sync.Mutex
+ n int
+ hdr http.Header
+ peer string
+}
+
+func (u *doorUpstream) seen() (int, http.Header, string) {
+ u.mu.Lock()
+ defer u.mu.Unlock()
+ return u.n, u.hdr.Clone(), u.peer
+}
+
+func newDoorUpstream(t *testing.T, body string) (*httptest.Server, *doorUpstream) {
+ t.Helper()
+ u := &doorUpstream{}
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ u.mu.Lock()
+ u.n++
+ u.hdr = r.Header.Clone()
+ u.peer = r.RemoteAddr
+ u.mu.Unlock()
+ io.WriteString(w, body)
+ }))
+ t.Cleanup(ts.Close)
+ return ts, u
+}
+
+// ---- keys ---------------------------------------------------------------------
+
+// mint returns a raw door key for account/app and the entry a record lists
+// for it.
+func (fx *doorKeyFixture) mint(t *testing.T, account, app string) (string, doorkey.Entry) {
+ t.Helper()
+ raw, entry, err := doorkey.NewToken(account, app)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return raw, entry
+}
+
+// record signs a record for account/app listing exactly entries.
+func (fx *doorKeyFixture) record(t *testing.T, account, app string, rev uint64, entries ...doorkey.Entry) []byte {
+ t.Helper()
+ now := fx.clk.now()
+ wire, err := doorkey.Sign(fx.priv, doorkey.Record{V: 1, Account: account, App: app, Revision: rev,
+ Keys: entries, SignedAt: now, ExpiresAt: now.Add(doorkey.Lifetime)})
+ if err != nil {
+ t.Fatal(err)
+ }
+ return wire
+}
+
+// ---- the scene -----------------------------------------------------------------
+
+const (
+ doorOwnHost = "own.test" // bae/titogo, the caller's app
+ doorOtherHost = "other.test" // bae/other
+ doorSplitHost = "split.test" // bae/titogo; /a/ to titogo, /b/ to other
+ doorGatedHost = "gated.test" // bae/titogo, behind the password gate
+ doorWildParent = "apps.test" // *.apps.test is an alias of own.test
+ doorTunnelHost = "tunnel.apps.test"
+ doorSteerHost = "edge.apps.test"
+ doorPassword = "correct horse"
+ doorOwnBody = "the caller's own app"
+ doorOtherBody = "another app"
+ doorSplitABody = "split a"
+ doorSplitBBody = "split b"
+ doorGatedBody = "gated page"
+ doorFrontBody = "split front"
+ doorHomeEdgeKey = "betty"
+)
+
+type doorScene struct {
+ e *Edge
+ fx *doorKeyFixture
+ store release.Store
+ // key is a valid door key for bae/titogo; otherKey for bae/other.
+ key, otherKey string
+ entry doorkey.Entry
+ own, other *doorUpstream
+ splitA *doorUpstream
+ splitB *doorUpstream
+ gated *doorUpstream
+}
+
+// newDoorScene is an edge serving two apps' routes locally, with a door-key
+// mirror over its own store and the box address set. It is built the way the
+// package's other tests build an Edge (openReg, New), with the mirror and the
+// address set the way armDoor sets them.
+func newDoorScene(t *testing.T) *doorScene {
+ t.Helper()
+ s := &doorScene{fx: newDoorKeyFixture(t), store: meshTestStore(t)}
+ ownSrv, own := newDoorUpstream(t, doorOwnBody)
+ otherSrv, other := newDoorUpstream(t, doorOtherBody)
+ aSrv, a := newDoorUpstream(t, doorSplitABody)
+ bSrv, b := newDoorUpstream(t, doorSplitBBody)
+ frontSrv, _ := newDoorUpstream(t, doorFrontBody)
+ gatedSrv, gated := newDoorUpstream(t, doorGatedBody)
+ s.own, s.other, s.splitA, s.splitB, s.gated = own, other, a, b, gated
+
+ hash, err := gate.Hash(doorPassword)
+ if err != nil {
+ t.Fatal(err)
+ }
+ reg := openReg(t)
+ for _, rt := range []registry.Route{
+ {Host: doorOwnHost, Addr: ownSrv.Listener.Addr().String(), Account: "bae", App: "titogo"},
+ {Host: doorOtherHost, Addr: otherSrv.Listener.Addr().String(), Account: "bae", App: "other"},
+ {Host: doorSplitHost, Addr: frontSrv.Listener.Addr().String(), Account: "bae", App: "titogo"},
+ {Host: "a.target.test", Addr: aSrv.Listener.Addr().String(), Account: "bae", App: "titogo"},
+ // The other app's prefix is gated, so a login POST naming it is
+ // the one that would mint a cookie if the door let it through.
+ {Host: "b.target.test", Addr: bSrv.Listener.Addr().String(), Account: "bae", App: "other", Gate: hash},
+ {Host: doorGatedHost, Addr: gatedSrv.Listener.Addr().String(), Account: "bae", App: "titogo", Gate: hash},
+ } {
+ rt.Kind, rt.Status = "service", "ready"
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatalf("upsert %s: %v", rt.Host, err)
+ }
+ }
+ upsertPath(t, reg, doorSplitHost, "a", "a.target.test")
+ upsertPath(t, reg, doorSplitHost, "b", "b.target.test")
+ // A wildcard owned by the caller that would answer the reserved names
+ // if the door let them through to the lookup.
+ upsertAlias(t, reg, "*."+doorWildParent, doorOwnHost)
+
+ e, err := New(reg, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(e.probers.Stop)
+ e.EnableTunnels(NewTunnelHub(doorTunnelHost, map[string]string{"pi-box": "sekrit"}))
+ e.steeringHost = doorSteerHost
+
+ var otherEntry doorkey.Entry
+ s.key, s.entry = s.fx.mint(t, "bae", "titogo")
+ s.otherKey, otherEntry = s.fx.mint(t, "bae", "other")
+ putJSON(t, s.store, doorkey.RecordKey("bae", "titogo"), string(s.fx.record(t, "bae", "titogo", 1, s.entry)))
+ putJSON(t, s.store, doorkey.RecordKey("bae", "other"), string(s.fx.record(t, "bae", "other", 1, otherEntry)))
+ e.doorKeys = s.fx.mirror(s.store)
+ refreshDoorKeys(t, e.doorKeys)
+ e.doorAddr = doorTestAddr
+ s.e = e
+ return s
+}
+
+// do drives one request through the door with key (none if "").
+func (s *doorScene) do(t *testing.T, method, host, target, key string, hdr map[string]string, body io.Reader) *httptest.ResponseRecorder {
+ t.Helper()
+ req := httptest.NewRequest(method, "http://"+host+target, body)
+ req.Host = host
+ // What a unix listener reports as the peer.
+ req.RemoteAddr = "@"
+ if key != "" {
+ req.Header.Set(doorkey.Header, key)
+ }
+ for k, v := range hdr {
+ req.Header.Set(k, v)
+ }
+ rec := httptest.NewRecorder()
+ s.e.serveDoor(rec, req)
+ return rec
+}
+
+func wantUnknownHost(t *testing.T, rec *httptest.ResponseRecorder, what string) {
+ t.Helper()
+ if rec.Code != http.StatusNotFound || !strings.Contains(rec.Body.String(), "unknown host") {
+ t.Fatalf("%s: %d %q, want the unknown-host answer", what, rec.Code, rec.Body.String())
+ }
+}
+
+func wantServed(t *testing.T, rec *httptest.ResponseRecorder, body, what string) {
+ t.Helper()
+ if rec.Code != http.StatusOK || rec.Body.String() != body {
+ t.Fatalf("%s: %d %q, want 200 %q", what, rec.Code, rec.Body.String(), body)
+ }
+}
+
+func upstreamCalls(u *doorUpstream) int {
+ n, _, _ := u.seen()
+ return n
+}
+
+// ---- Task 5: enforcement in ServeHTTP ------------------------------------------
+
+// 1. A door caller reaches its own app's local route.
+func TestDoorServesItsOwnAppsLocalRoute(t *testing.T) {
+ s := newDoorScene(t)
+ wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, nil, nil), doorOwnBody, "own route")
+ if upstreamCalls(s.own) != 1 {
+ t.Fatal("the caller's own upstream was not reached")
+ }
+}
+
+// 2. Another app's local route answers unknown host, and its upstream never
+// hears of it.
+func TestDoorRefusesAnotherAppsLocalRoute(t *testing.T) {
+ s := newDoorScene(t)
+ wantUnknownHost(t, s.do(t, http.MethodGet, doorOtherHost, "/", s.key, nil, nil), "another app's route")
+ if n := upstreamCalls(s.other); n != 0 {
+ t.Fatalf("the other app's upstream saw %d request(s)", n)
+ }
+ // The other app's own key reaches it, so the refusal is the scope and
+ // not the route.
+ wantServed(t, s.do(t, http.MethodGet, doorOtherHost, "/", s.otherKey, nil, nil), doorOtherBody, "other app with its own key")
+}
+
+// 3. On a path-prefix host the check is against the route the prefix picks.
+func TestDoorPathPrefixHostChecksTheFinalRoute(t *testing.T) {
+ s := newDoorScene(t)
+ wantServed(t, s.do(t, http.MethodGet, doorSplitHost, "/a/x", s.key, nil, nil), doorSplitABody, "/a/x")
+ wantUnknownHost(t, s.do(t, http.MethodGet, doorSplitHost, "/b/x", s.key, nil, nil), "/b/x")
+ if n := upstreamCalls(s.splitB); n != 0 {
+ t.Fatalf("the other app's prefix upstream saw %d request(s)", n)
+ }
+}
+
+// 4. A gate login POST whose next names another app's prefix is refused
+// before the gate judges the password: the password is right, and still no
+// cookie comes back.
+func TestDoorGateLoginPostForAnotherAppsPrefixIsUnknownHost(t *testing.T) {
+ s := newDoorScene(t)
+ for _, next := range []string{"/b/x", "/%62/x", "/b?tab=1"} {
+ form := url.Values{"password": {doorPassword}, "next": {next}}
+ rec := s.do(t, http.MethodPost, doorSplitHost, gate.LoginPath, s.key,
+ map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, strings.NewReader(form.Encode()))
+ wantUnknownHost(t, rec, "login POST next="+next)
+ if c := rec.Result().Cookies(); len(c) != 0 {
+ t.Fatalf("next=%s: the gate judged the password and set %v", next, c)
+ }
+ }
+ // The control: the same POST from the other app's own key does get a
+ // cookie, so the refusal above is the door's and not a broken form.
+ form := url.Values{"password": {doorPassword}, "next": {"/b/x"}}
+ rec := s.do(t, http.MethodPost, doorSplitHost, gate.LoginPath, s.otherKey,
+ map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, strings.NewReader(form.Encode()))
+ if rec.Code != http.StatusSeeOther || len(rec.Result().Cookies()) == 0 {
+ t.Fatalf("control: the other app's own login = %d, cookies %v", rec.Code, rec.Result().Cookies())
+ }
+}
+
+// 5. The tunnel and steering-probe names answer unknown host to a door
+// caller in any spelling, although the caller's own wildcard would match
+// them, and neither the hub nor the probe runs.
+func TestDoorRefusesReservedHostnames(t *testing.T) {
+ s := newDoorScene(t)
+ // Preconditions: the public door answers both names itself.
+ pub := httptest.NewServer(s.e)
+ t.Cleanup(pub.Close)
+ if code, body := getHost(t, pub.URL, doorSteerHost); code != http.StatusOK || !strings.Contains(body, steeringProbeBody) {
+ t.Fatalf("precondition: the public steering probe answered %d %q", code, body)
+ }
+ for _, host := range []string{
+ doorTunnelHost, strings.ToUpper(doorTunnelHost), doorTunnelHost + ".", "Tunnel.Apps.Test.",
+ doorSteerHost, strings.ToUpper(doorSteerHost), doorSteerHost + ".", "Edge.Apps.Test.",
+ } {
+ for _, path := range []string{"/", tunnelRegisterPath, storeDoorPrefix + "x"} {
+ rec := s.do(t, http.MethodGet, host, path, s.key, map[string]string{
+ "Carlos-Tunnel-Name": "pi-box", "Authorization": "Bearer sekrit"}, nil)
+ wantUnknownHost(t, rec, host+path)
+ if strings.Contains(rec.Body.String(), steeringProbeBody) {
+ t.Fatalf("%s%s: the steering probe answered a door caller", host, path)
+ }
+ }
+ }
+ if n := upstreamCalls(s.own); n != 0 {
+ t.Fatalf("the wildcard's upstream saw %d request(s) for a reserved name", n)
+ }
+ // And an ordinary name under the same wildcard is the caller's.
+ wantServed(t, s.do(t, http.MethodGet, "anything."+doorWildParent, "/", s.key, nil, nil), doorOwnBody, "wildcard name")
+}
+
+// 6. A host served on another box: the door checks the owner that box
+// published, and dials the box Owner named.
+func TestDoorRemoteHostChecksThePublishedOwner(t *testing.T) {
+ s := newDoorScene(t)
+ if err := mintMeshKey(context.Background(), s.store); err != nil {
+ t.Fatal(err)
+ }
+ rec, eip := fakeHomeEdge(t, nil)
+ putJSON(t, s.store, "control/edges/"+doorHomeEdgeKey+".json", fmt.Sprintf(`{"v":1,"box":%q,"eip":%q,"hosts":{`+
+ `"example.com":{"role":"home","account":"bae","app":"titogo"},`+
+ `"foreign.example":{"role":"home","account":"bae","app":"other"},`+
+ `"mixed.example":{"role":"home"}}}`, doorHomeEdgeKey, eip))
+ m := newHomeMap(s.store, "annie")
+ refreshHomeMap(t, m)
+ s.e.homes.Store(m)
+ if acct, app, gotEIP, box, ok := m.Owner("example.com"); !ok || acct != "bae" || app != "titogo" || gotEIP != eip || box != doorHomeEdgeKey {
+ t.Fatalf("precondition: Owner = %q %q %q %q %v", acct, app, gotEIP, box, ok)
+ }
+
+ r := s.do(t, http.MethodGet, "example.com", "/", s.key, nil, nil)
+ if r.Code != http.StatusOK || r.Body.String() != homeEdgeBody {
+ t.Fatalf("owned remote host = %d %q, want the home edge's answer", r.Code, r.Body.String())
+ }
+ seen := rec.get()
+ if seen.n != 1 || seen.host != "example.com" || seen.hop != "1" || seen.auth == "" {
+ t.Fatalf("home edge saw %+v, want one signed hop for example.com", seen)
+ }
+ if _, ok := s.e.backhaulInflight.Load(doorHomeEdgeKey); !ok {
+ t.Fatalf("the hop was not counted against box %q, the box Owner named", doorHomeEdgeKey)
+ }
+
+ for _, host := range []string{"foreign.example", "mixed.example", "nobody.example"} {
+ wantUnknownHost(t, s.do(t, http.MethodGet, host, "/", s.key, nil, nil), host)
+ }
+ if got := rec.get().n; got != 1 {
+ t.Fatalf("the home edge saw %d request(s), want only the owned one", got)
+ }
+}
+
+// 7. The mark comes only from serveDoor: a public request carrying the door
+// key and any other X-Carlos-* header is an ordinary client, neither scoped
+// nor given the door's scheme and address.
+func TestDoorMarkNeverComesFromHeaders(t *testing.T) {
+ s := newDoorScene(t)
+ pub := httptest.NewServer(s.e)
+ t.Cleanup(pub.Close)
+ req, _ := http.NewRequest(http.MethodGet, pub.URL+"/", nil)
+ req.Host = doorOtherHost
+ for _, h := range []string{"X-Carlos-Door", "X-Carlos-Door-Caller", "X-Carlos-Account", "X-Carlos-App"} {
+ req.Header.Set(h, "bae/titogo")
+ }
+ req.Header.Set(doorkey.Header, s.key)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ body := readBody(t, resp)
+ resp.Body.Close()
+ if resp.StatusCode != http.StatusOK || body != doorOtherBody {
+ t.Fatalf("public request = %d %q, want the route served as to any client", resp.StatusCode, body)
+ }
+ _, hdr, _ := s.other.seen()
+ if got := hdr.Get("X-Forwarded-Proto"); got != "http" {
+ t.Fatalf("X-Forwarded-Proto = %q, want http: the door's scheme was applied to a public request", got)
+ }
+ if got := hdr.Get("X-Forwarded-For"); got == doorTestAddr {
+ t.Fatal("a public request was given the door's address")
+ }
+ // The public steering probe still answers for a request carrying a key.
+ req, _ = http.NewRequest(http.MethodGet, pub.URL+"/", nil)
+ req.Host = doorSteerHost
+ req.Header.Set(doorkey.Header, s.key)
+ resp, err = http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if body := readBody(t, resp); resp.StatusCode != http.StatusOK || !strings.Contains(body, steeringProbeBody) {
+ t.Fatalf("public probe with a door key = %d %q", resp.StatusCode, body)
+ }
+ resp.Body.Close()
+}
+
+// ---- Task 6: the handler --------------------------------------------------------
+
+// 1. No key, a malformed key, a key for an app with no record, and a revoked
+// key are 401 with no-store, and nothing upstream runs.
+func TestDoorRefusesMissingMalformedAndRevokedKeys(t *testing.T) {
+ s := newDoorScene(t)
+ stranger, _ := s.fx.mint(t, "bae", "norecord")
+ forged := s.key[:len(s.key)-1] + "A"
+ if forged == s.key {
+ forged = s.key[:len(s.key)-1] + "B"
+ }
+ check := func(what, key string) {
+ t.Helper()
+ rec := s.do(t, http.MethodGet, doorOwnHost, "/", key, nil, nil)
+ if rec.Code != http.StatusUnauthorized || strings.TrimSpace(rec.Body.String()) != "door key refused" {
+ t.Fatalf("%s: %d %q, want 401 door key refused", what, rec.Code, rec.Body.String())
+ }
+ if cc := rec.Header().Get("Cache-Control"); cc != "no-store" {
+ t.Fatalf("%s: Cache-Control = %q, want no-store", what, cc)
+ }
+ }
+ check("no key", "")
+ check("malformed key", "cdk1.bae.titogo.nope")
+ check("unknown app", stranger)
+ check("wrong secret", forged)
+
+ wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, nil, nil), doorOwnBody, "before the revoke")
+ putJSON(t, s.store, doorkey.RecordKey("bae", "titogo"), string(s.fx.record(t, "bae", "titogo", 2)))
+ refreshDoorKeys(t, s.e.doorKeys)
+ check("revoked key", s.key)
+ if n := upstreamCalls(s.own); n != 1 {
+ t.Fatalf("upstream saw %d request(s), want only the one before the revoke", n)
+ }
+}
+
+// 2. The key never reaches the app; the app's own headers do.
+func TestDoorStripsItsKeyAndPassesAppHeaders(t *testing.T) {
+ s := newDoorScene(t)
+ wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key,
+ map[string]string{"X-Tito-Auth-User": "u_123", "X-Tito-Auth-Sig": "sig"}, nil), doorOwnBody, "valid key")
+ _, hdr, _ := s.own.seen()
+ if v := hdr.Get(doorkey.Header); v != "" {
+ t.Fatalf("the door key reached the app: %q", v)
+ }
+ if hdr.Get("X-Tito-Auth-User") != "u_123" || hdr.Get("X-Tito-Auth-Sig") != "sig" {
+ t.Fatalf("the app's own headers were touched: %v", hdr)
+ }
+}
+
+// 3. Hop vocabulary from a door caller goes nowhere: not to the app, and a
+// correctly signed hop for a host not served here still meets the door's
+// owner check (unknown host), never the stale-hop 503.
+func TestDoorDropsHopVocabulary(t *testing.T) {
+ s := newDoorScene(t)
+ if err := mintMeshKey(context.Background(), s.store); err != nil {
+ t.Fatal(err)
+ }
+ m := newHomeMap(s.store, "annie")
+ refreshHomeMap(t, m)
+ s.e.homes.Store(m)
+ key, _ := m.meshKeys()
+
+ wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, map[string]string{
+ hopHeader: "1", hopAuthHeader: signHop(key, doorOwnHost, time.Now())}, nil), doorOwnBody, "local route")
+ _, hdr, _ := s.own.seen()
+ if hdr.Get(hopHeader) != "" || hdr.Get(hopAuthHeader) != "" {
+ t.Fatalf("hop headers reached the app: %v", hdr)
+ }
+
+ rec := s.do(t, http.MethodGet, "elsewhere.test", "/", s.key, map[string]string{
+ hopHeader: "1", hopAuthHeader: signHop(key, "elsewhere.test", time.Now())}, nil)
+ if rec.Code == http.StatusServiceUnavailable {
+ t.Fatal("a door caller's signed hop earned the stale-hop refusal")
+ }
+ wantUnknownHost(t, rec, "signed hop for an unserved host")
+}
+
+// 4. Every claimed address goes; the app sees the box's public address and
+// https, whether or not the caller sent any; the visitor hash and the gate's
+// limiter key read the same address.
+func TestDoorPresentsTheBoxAddress(t *testing.T) {
+ s := newDoorScene(t)
+ for _, sent := range []map[string]string{
+ {"X-Forwarded-For": "198.51.100.7", "Forwarded": "for=198.51.100.9", "X-Forwarded-Proto": "http", "X-Forwarded-Host": "evil.test"},
+ nil,
+ } {
+ wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, sent, nil), doorOwnBody, "door request")
+ _, hdr, _ := s.own.seen()
+ if got := hdr.Values("X-Forwarded-For"); len(got) != 1 || got[0] != doorTestAddr {
+ t.Fatalf("sent %v: X-Forwarded-For = %q, want exactly %s", sent, got, doorTestAddr)
+ }
+ if got := hdr.Values("X-Forwarded-Proto"); len(got) != 1 || got[0] != "https" {
+ t.Fatalf("sent %v: X-Forwarded-Proto = %q, want https", sent, got)
+ }
+ if got := hdr.Get("Forwarded"); got != "" {
+ t.Fatalf("sent %v: Forwarded = %q reached the app", sent, got)
+ }
+ if got := hdr.Get("X-Forwarded-Host"); got != doorOwnHost {
+ t.Fatalf("sent %v: X-Forwarded-Host = %q, want %s", sent, got, doorOwnHost)
+ }
+ }
+
+ req := httptest.NewRequest(http.MethodGet, "http://"+doorOwnHost+"/", nil)
+ req.RemoteAddr = "@"
+ req.Header.Set(doorkey.Header, s.key)
+ req.Header.Set("X-Forwarded-For", "198.51.100.7")
+ in, ok := s.e.doorRequest(req)
+ if !ok {
+ t.Fatal("doorRequest refused a valid key")
+ }
+ if got := clientIP(in); got != doorTestAddr {
+ t.Fatalf("clientIP = %q, want %s", got, doorTestAddr)
+ }
+ // The gate's limiter key is RemoteAddr without its port (gate.clientKey).
+ if host, _, err := net.SplitHostPort(in.RemoteAddr); err != nil || host != doorTestAddr {
+ t.Fatalf("RemoteAddr = %q, want %s:<port>", in.RemoteAddr, doorTestAddr)
+ }
+ if c, ok := doorCallerFrom(in.Context()); !ok || c.Account != "bae" || c.App != "titogo" {
+ t.Fatalf("door mark = %+v %v", c, ok)
+ }
+}
+
+// allAlternates sets every name in doorAddressHeaders, plus the four
+// stripForwardedHeaders names, to addr.
+func allAlternates(addr string) map[string]string {
+ h := map[string]string{"Forwarded": "for=" + addr, "X-Forwarded-For": addr, "X-Forwarded-Host": "evil.test", "X-Forwarded-Proto": "http"}
+ for _, n := range doorAddressHeaders {
+ h[n] = addr
+ }
+ return h
+}
+
+// 4b. None of the alternate address headers reaches the app.
+func TestDoorStripsAlternateAddressHeaders(t *testing.T) {
+ s := newDoorScene(t)
+ for _, want := range []string{"X-Forwarded", "Forwarded-For", "Client-IP", "X-Real-IP", "True-Client-IP",
+ "CF-Connecting-IP", "Fastly-Client-IP", "X-Client-IP", "X-Cluster-Client-IP", "X-Originating-IP"} {
+ found := false
+ for _, n := range doorAddressHeaders {
+ found = found || n == want
+ }
+ if !found {
+ t.Fatalf("doorAddressHeaders lacks %s", want)
+ }
+ }
+ wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, allAlternates("8.8.8.8"), nil), doorOwnBody, "door request")
+ _, hdr, _ := s.own.seen()
+ for _, n := range doorAddressHeaders {
+ if v := hdr.Values(n); len(v) != 0 {
+ t.Fatalf("%s = %q reached the app", n, v)
+ }
+ }
+ if got := hdr.Get("Forwarded"); got != "" {
+ t.Fatalf("Forwarded = %q reached the app", got)
+ }
+}
+
+// ---- the two resolver rule sets (4c) ----------------------------------------------
+
+func mustPrefixes(cidrs ...string) []*net.IPNet {
+ out := make([]*net.IPNet, 0, len(cidrs))
+ for _, c := range cidrs {
+ _, n, err := net.ParseCIDR(c)
+ if err != nil {
+ panic(err)
+ }
+ out = append(out, n)
+ }
+ return out
+}
+
+func inAny(ip net.IP, nets []*net.IPNet) bool {
+ for _, n := range nets {
+ if n.Contains(ip) {
+ return true
+ }
+ }
+ return false
+}
+
+// railsTrusted is ActionDispatch::RemoteIp::TRUSTED_PROXIES (Rails 8.0.2).
+var railsTrusted = mustPrefixes("127.0.0.0/8", "::1/128", "fc00::/7", "10.0.0.0/8", "172.16.0.0/12",
+ "192.168.0.0/16", "169.254.0.0/16", "fe80::/10")
+
+// railsIPs is RemoteIp#ips_from: a comma list, non-IP entries dropped.
+func railsIPs(values []string) []string {
+ var out []string
+ for _, v := range values {
+ for _, p := range strings.Split(v, ",") {
+ p = strings.TrimSpace(p)
+ if net.ParseIP(p) != nil {
+ out = append(out, p)
+ }
+ }
+ }
+ return out
+}
+
+func reversed(s []string) []string {
+ out := make([]string, len(s))
+ for i, v := range s {
+ out[len(s)-1-i] = v
+ }
+ return out
+}
+
+// railsRemoteIP is Rails 8.0.2's RemoteIp::GetIp#calculate_ip, with the
+// spoofing check off (its default outcome when the headers agree):
+//
+// remote_addr = ips_from(REMOTE_ADDR).last
+// client_ips = ips_from(Client-IP).reverse
+// forwarded_ips = ips_from(X-Forwarded-For).reverse
+// ips = forwarded_ips + client_ips
+// filter_proxies([remote_addr] + ips).first || ips.last || remote_addr
+func railsRemoteIP(h http.Header, peer string) string {
+ ips := append(reversed(railsIPs(h.Values("X-Forwarded-For"))), reversed(railsIPs(h.Values("Client-IP")))...)
+ for _, ip := range append([]string{peer}, ips...) {
+ if !inAny(net.ParseIP(ip), railsTrusted) {
+ return ip
+ }
+ }
+ if len(ips) > 0 {
+ return ips[len(ips)-1]
+ }
+ return peer
+}
+
+// pythonPrivate is ipaddress.IPv4Address/IPv6Address.is_private's range
+// list (CPython 3.12). 203.0.113.0/24 and 198.51.100.0/24 are on it.
+var pythonPrivate = mustPrefixes("0.0.0.0/8", "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12",
+ "192.0.0.0/29", "192.0.0.170/31", "192.0.2.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24",
+ "203.0.113.0/24", "240.0.0.0/4", "255.255.255.255/32",
+ "::1/128", "::/128", "::ffff:0:0/96", "100::/64", "2001::/23", "2001:2::/48", "2001:db8::/32",
+ "2001:10::/28", "fc00::/7", "fe80::/10")
+
+var pythonShared = mustPrefixes("100.64.0.0/10")
+
+// ipwareHeaders is python-ipware 3.0's default precedence, as HTTP header
+// names, REMOTE_ADDR last.
+var ipwareHeaders = []string{"X-Forwarded-For", "Client-IP", "X-Real-IP", "X-Forwarded", "X-Cluster-Client-IP",
+ "Forwarded-For", "Forwarded", "CF-Connecting-IP", "X-Client-IP", "True-Client-IP", "Fastly-Client-IP"}
+
+// ipwareClientIP is python-ipware 3.0's IpWare.get_client_ip(meta) with no
+// proxy count or trusted list: headers in precedence order, the left-most
+// address of each; the first global one wins at once, otherwise the first
+// private one, otherwise the first loopback one.
+func ipwareClientIP(h http.Header, peer string) (ip string, routable bool) {
+ var private, loopback []string
+ consider := func(v string) (string, bool) {
+ first := strings.TrimSpace(strings.Split(v, ",")[0])
+ p := net.ParseIP(first)
+ if p == nil {
+ return "", false
+ }
+ switch {
+ case p.IsLoopback():
+ loopback = append(loopback, first)
+ case inAny(p, pythonPrivate) || inAny(p, pythonShared):
+ private = append(private, first)
+ default:
+ return first, true
+ }
+ return "", false
+ }
+ for _, name := range ipwareHeaders {
+ if v := strings.Join(h.Values(name), ", "); strings.TrimSpace(v) != "" {
+ if ip, ok := consider(v); ok {
+ return ip, true
+ }
+ }
+ }
+ if ip, ok := consider(peer); ok {
+ return ip, true
+ }
+ if len(private) > 0 {
+ return private[0], false
+ }
+ if len(loopback) > 0 {
+ return loopback[0], false
+ }
+ return "", false
+}
+
+// resolveBoth feeds the upstream's headers and peer to both rule sets.
+func resolveBoth(t *testing.T, what string, hdr http.Header, peer, want string) {
+ t.Helper()
+ host, _, err := net.SplitHostPort(peer)
+ if err != nil {
+ host = peer
+ }
+ if !net.ParseIP(host).IsLoopback() {
+ t.Fatalf("%s: test bug: the upstream's peer %q is not loopback", what, host)
+ }
+ if got := railsRemoteIP(hdr, host); got != want {
+ t.Fatalf("%s: Rails RemoteIp resolves %q, want %s", what, got, want)
+ }
+ got, routable := ipwareClientIP(hdr, host)
+ if got != want {
+ t.Fatalf("%s: python-ipware resolves %q, want %s", what, got, want)
+ }
+ // ipware's routable flag is Python's is_global. TEST-NET-3 is on
+ // Python's private list, so the plan's documentation address is not
+ // routable there; a real EIP is.
+ if wantRoutable := !inAny(net.ParseIP(want), pythonPrivate); routable != wantRoutable {
+ t.Fatalf("%s: python-ipware routable = %v for %s, want %v", what, routable, want, wantRoutable)
+ }
+}
+
+// 4c. With every alternate address header set to a global address, both
+// resolvers still land on the box address, never loopback, a private address
+// or the caller's claim. The same inbound headers, unstripped, resolve to
+// the claim under both, which is what makes the rule sets a real test.
+func TestDoorResolvesToTheBoxAddressUnderRailsAndIpware(t *testing.T) {
+ inbound := http.Header{}
+ for k, v := range allAlternates("8.8.8.8") {
+ inbound.Set(k, v)
+ }
+ if got := railsRemoteIP(inbound, "127.0.0.1"); got != "8.8.8.8" {
+ t.Fatalf("control: Rails resolves the raw inbound headers to %q, want the claim", got)
+ }
+ inbound.Del("X-Forwarded-For")
+ if got := railsRemoteIP(inbound, "127.0.0.1"); got != "8.8.8.8" {
+ t.Fatalf("control: Rails falls back to Client-IP as %q, want the claim", got)
+ }
+ if got, _ := ipwareClientIP(inbound, "127.0.0.1"); got != "8.8.8.8" {
+ t.Fatalf("control: ipware resolves the raw inbound headers to %q, want the claim", got)
+ }
+
+ // The plan's documentation address, and one shaped like a real EIP.
+ for _, addr := range []string{doorTestAddr, "34.248.10.10"} {
+ s := newDoorScene(t)
+ s.e.doorAddr = addr
+ wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, allAlternates("8.8.8.8"), nil), doorOwnBody, "door request")
+ _, hdr, peer := s.own.seen()
+ resolveBoth(t, "served locally as "+addr, hdr, peer, addr)
+ }
+}
+
+// 5. The door is not a bypass: a gated route answers with the gate's page.
+func TestDoorGatedRouteStillAnswersWithTheGatePage(t *testing.T) {
+ s := newDoorScene(t)
+ rec := s.do(t, http.MethodGet, doorGatedHost, "/", s.key, nil, nil)
+ if rec.Code != http.StatusUnauthorized || !strings.Contains(rec.Body.String(), "Password required") {
+ t.Fatalf("gated route through the door = %d %q, want the gate's 401 page", rec.Code, rec.Body.String())
+ }
+ if n := upstreamCalls(s.gated); n != 0 {
+ t.Fatalf("the gated upstream saw %d request(s)", n)
+ }
+}
+
+// ---- 5b: a backhauled door request -----------------------------------------------
+
+// 5b. Two edges joined as the backhaul e2e joins them: the door request lands
+// on betty, which does not serve the host, and is backhauled to annie. The
+// far app sees the box address and https, and so do both resolvers, when
+// annie verifies the hop and again when it cannot. Unverified, annie uses the
+// TCP peer, which in production is the sending box's own public address;
+// loopback cannot carry that, so annie's front door reports it (the harness's
+// peerAs).
+func TestDoorBackhaulPresentsTheBoxAddress(t *testing.T) {
+ e2eQuietLogs(t)
+ bucket := t.TempDir()
+ seed := release.NewDirStore(bucket)
+ ca := newE2ECA(t)
+ if err := MintMeshKey(context.Background(), seed); err != nil {
+ t.Fatal(err)
+ }
+ for _, host := range []string{e2eHomedHost, e2eSenderHost} {
+ ca.publish(t, seed, host)
+ }
+ appSrv, app := newDoorUpstream(t, e2eHomeMarker)
+ senderSrv, _ := newDoorUpstream(t, e2eSenderMarker)
+ a := newBackhaulEdge(t, bucket, "annie", ca, nil, map[string]string{e2eHomedHost: appSrv.Listener.Addr().String()})
+ b := newBackhaulEdge(t, bucket, "betty", ca, nil, map[string]string{e2eSenderHost: senderSrv.Listener.Addr().String()})
+
+ aEIP, port, err := net.SplitHostPort(a.tlsAddr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ bEIP, _, _ := net.SplitHostPort(b.tlsAddr)
+ oldPort, oldCAs := backhaulPort, backhaulRootCAs
+ backhaulPort, backhaulRootCAs = port, ca.pool
+ t.Cleanup(func() { backhaulPort, backhaulRootCAs = oldPort, oldCAs })
+ putJSON(t, seed, "control/edges/annie.json", fmt.Sprintf(
+ `{"v":1,"box":"annie","eip":%q,"hosts":{%q:{"role":"home","account":"bae","app":"titogo"}}}`, aEIP, e2eHomedHost))
+ putJSON(t, seed, "control/edges/betty.json", fmt.Sprintf(
+ `{"v":1,"box":"betty","eip":%q,"hosts":{%q:{"role":"home","account":"bae","app":"other"}}}`, bEIP, e2eSenderHost))
+ for _, be := range []*backhaulEdge{a, b} {
+ if err := be.mirror.Refresh(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+ refreshHomeMap(t, be.homes)
+ }
+
+ fx := newDoorKeyFixture(t)
+ raw, entry := fx.mint(t, "bae", "titogo")
+ putJSON(t, seed, doorkey.RecordKey("bae", "titogo"), string(fx.record(t, "bae", "titogo", 1, entry)))
+ b.edge.doorKeys = fx.mirror(b.store)
+ refreshDoorKeys(t, b.edge.doorKeys)
+ b.edge.doorAddr = doorTestAddr
+
+ // farXFF sends one door request and returns the far app's chain.
+ farXFF := func() []string {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, "http://"+e2eHomedHost+"/", nil)
+ req.Host = e2eHomedHost
+ req.RemoteAddr = "@"
+ req.Header.Set(doorkey.Header, raw)
+ rec := httptest.NewRecorder()
+ b.edge.serveDoor(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("door request = %d %q", rec.Code, rec.Body.String())
+ }
+ _, hdr, _ := app.seen()
+ return hdr.Values("X-Forwarded-For")
+ }
+
+ door := func(what string) {
+ t.Helper()
+ before := upstreamCalls(app)
+ req := httptest.NewRequest(http.MethodGet, "http://"+e2eHomedHost+"/deep", nil)
+ req.Host = e2eHomedHost
+ req.RemoteAddr = "@"
+ req.Header.Set(doorkey.Header, raw)
+ for k, v := range allAlternates("8.8.8.8") {
+ req.Header.Set(k, v)
+ }
+ rec := httptest.NewRecorder()
+ b.edge.serveDoor(rec, req)
+ if rec.Code != http.StatusOK || rec.Body.String() != e2eHomeMarker {
+ t.Fatalf("%s: %d %q, want the home edge's app", what, rec.Code, rec.Body.String())
+ }
+ n, hdr, peer := app.seen()
+ if n != before+1 {
+ t.Fatalf("%s: the far app saw %d new request(s), want one", what, n-before)
+ }
+ if got := hdr.Values("X-Forwarded-For"); len(got) != 1 || got[0] != doorTestAddr {
+ t.Fatalf("%s: X-Forwarded-For = %q, want exactly %s", what, got, doorTestAddr)
+ }
+ if got := hdr.Values("X-Forwarded-Proto"); len(got) != 1 || got[0] != "https" {
+ t.Fatalf("%s: X-Forwarded-Proto = %q, want https", what, got)
+ }
+ for _, n := range append([]string{"Forwarded", doorkey.Header, hopHeader, hopAuthHeader}, doorAddressHeaders...) {
+ if v := hdr.Values(n); len(v) != 0 {
+ t.Fatalf("%s: %s = %q reached the far app", what, n, v)
+ }
+ }
+ resolveBoth(t, what, hdr, peer, doorTestAddr)
+ }
+
+ // Verified: annie believes betty's chain, not the peer. A peer that is
+ // neither address proves which one the app was given.
+ const elsewhere = "192.0.2.77"
+ a.peerAs.Store(elsewhere)
+ door("verified hop")
+
+ // annie can no longer verify betty's hops: its mirrored key is not the
+ // one betty signs with. The chain is dropped and the peer is used, which
+ // the elsewhere peer shows; with the peer at the box's own address, as
+ // in production, the app sees the same as above.
+ a.homes.keyMu.Lock()
+ a.homes.keyCurrent, a.homes.keyNext = bytes.Repeat([]byte{7}, 32), nil
+ a.homes.keyMu.Unlock()
+ if got := farXFF(); len(got) != 1 || got[0] != elsewhere {
+ t.Fatalf("unverifiable hop: X-Forwarded-For = %q, want the peer %s (the chain must not be believed)", got, elsewhere)
+ }
+ a.peerAs.Store(doorTestAddr)
+ door("unverifiable hop")
+}
+
+// ---- Task 6: the listener ------------------------------------------------------
+
+// shortSockDir is a socket directory short enough for sun_path.
+func shortSockDir(t *testing.T) string {
+ t.Helper()
+ dir, err := os.MkdirTemp("", "door")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { os.RemoveAll(dir) })
+ return dir
+}
+
+func unixClient(path string) *http.Client {
+ return &http.Client{Timeout: 10 * time.Second, Transport: &http.Transport{
+ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ var d net.Dialer
+ return d.DialContext(ctx, "unix", path)
+ },
+ }}
+}
+
+func doorGet(t *testing.T, path, host, key string) (int, string) {
+ t.Helper()
+ req, _ := http.NewRequest(http.MethodGet, "http://door/", nil)
+ req.Host = host
+ if key != "" {
+ req.Header.Set(doorkey.Header, key)
+ }
+ resp, err := unixClient(path).Do(req)
+ if err != nil {
+ t.Fatalf("door request: %v", err)
+ }
+ defer resp.Body.Close()
+ return resp.StatusCode, readBody(t, resp)
+}
+
+// 6. openDoor makes a 0660 socket that serves the door, closeDoor removes
+// it, and a door with no mirror or no public address opens nothing.
+func TestOpenDoorServesOnA0660Socket(t *testing.T) {
+ s := newDoorScene(t)
+ path := filepath.Join(shortSockDir(t), "edge-local.sock")
+ // A stale socket from a previous run is replaced.
+ stale, err := net.Listen("unix", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ stale.(*net.UnixListener).SetUnlinkOnClose(false)
+ stale.Close()
+
+ closeDoor, err := s.e.openDoor(path)
+ if err != nil {
+ t.Fatalf("openDoor: %v", err)
+ }
+ fi, err := os.Stat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fi.Mode()&os.ModeSocket == 0 || fi.Mode().Perm() != 0o660 {
+ t.Fatalf("socket mode = %v, want a 0660 socket", fi.Mode())
+ }
+ if code, body := doorGet(t, path, doorOwnHost, s.key); code != http.StatusOK || body != doorOwnBody {
+ t.Fatalf("over the socket = %d %q", code, body)
+ }
+ if code, _ := doorGet(t, path, doorOwnHost, ""); code != http.StatusUnauthorized {
+ t.Fatalf("no key over the socket = %d, want 401", code)
+ }
+ closeDoor()
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Fatalf("socket still there after closeDoor: %v", err)
+ }
+ closeDoor() // idempotent
+
+ for _, tc := range []struct {
+ what string
+ arm func(e *Edge)
+ }{
+ {"nil mirror", func(e *Edge) { e.doorKeys = nil }},
+ {"no address", func(e *Edge) { e.doorAddr = "" }},
+ {"loopback", func(e *Edge) { e.doorAddr = "127.0.0.1" }},
+ {"private", func(e *Edge) { e.doorAddr = "10.0.0.5" }},
+ {"not an address", func(e *Edge) { e.doorAddr = "unknown" }},
+ } {
+ s := newDoorScene(t)
+ tc.arm(s.e)
+ p := filepath.Join(shortSockDir(t), "d.sock")
+ if c, err := s.e.openDoor(p); err == nil {
+ c()
+ t.Fatalf("%s: openDoor opened a door", tc.what)
+ }
+ if _, err := os.Stat(p); !os.IsNotExist(err) {
+ t.Fatalf("%s: openDoor left something at the path: %v", tc.what, err)
+ }
+ }
+
+ // A path that is not a socket is not removed.
+ p := filepath.Join(shortSockDir(t), "file")
+ if err := os.WriteFile(p, []byte("keep"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if c, err := newDoorScene(t).e.openDoor(p); err == nil {
+ c()
+ t.Fatal("openDoor replaced a regular file")
+ }
+ if b, err := os.ReadFile(p); err != nil || string(b) != "keep" {
+ t.Fatalf("the regular file was touched: %q %v", b, err)
+ }
+}
+
+// ---- Task 7: lifecycle ----------------------------------------------------------
+
+func TestWithEdgeDoorRecordsPathAndAddress(t *testing.T) {
+ e := &Edge{}
+ WithEdgeDoor("/run/carlos/edge-local.sock", doorTestAddr)(e)
+ if e.doorPath != "/run/carlos/edge-local.sock" || e.doorAddr != doorTestAddr {
+ t.Fatalf("doorPath %q doorAddr %q", e.doorPath, e.doorAddr)
+ }
+}
+
+// armDoor over a DirStore: before any record exists a key is refused (and
+// nothing panics); once a signed record lands and the mirror polls, the
+// same key is served over the real socket; closeDoor removes the socket.
+func TestArmDoorServesOverTheSocketAndClosesIt(t *testing.T) {
+ buf := captureLog(t)
+ fx := newDoorKeyFixture(t)
+ oldInterval := doorKeyInterval
+ doorKeyInterval = 20 * time.Millisecond
+ t.Cleanup(func() { doorKeyInterval = oldInterval })
+
+ backend, _ := newDoorUpstream(t, doorOwnBody)
+ reg := openReg(t)
+ if err := reg.Upsert(registry.Route{Host: doorOwnHost, Kind: "service", Status: "ready",
+ Addr: backend.Listener.Addr().String(), Account: "bae", App: "titogo"}); err != nil {
+ t.Fatal(err)
+ }
+ e, err := New(reg, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(e.probers.Stop)
+ store := release.NewDirStore(t.TempDir())
+ e.staticStore, e.staticPub = store, fx.pub
+ path := filepath.Join(shortSockDir(t), "edge-local.sock")
+ WithEdgeDoor(path, doorTestAddr)(e)
+
+ if err := e.armDoor(context.Background()); err != nil {
+ t.Fatalf("armDoor: %v", err)
+ }
+ t.Cleanup(e.closeDoor)
+ raw, entry := fx.mint(t, "bae", "titogo")
+ if code, body := doorGet(t, path, doorOwnHost, raw); code != http.StatusUnauthorized {
+ t.Fatalf("before any record = %d %q, want 401", code, body)
+ }
+
+ putJSON(t, store, doorkey.RecordKey("bae", "titogo"), string(fx.record(t, "bae", "titogo", 1, entry)))
+ deadline := time.Now().Add(5 * time.Second)
+ for {
+ code, body := doorGet(t, path, doorOwnHost, raw)
+ if code == http.StatusOK && body == doorOwnBody {
+ break
+ }
+ if time.Now().After(deadline) {
+ t.Fatalf("the mirror's loop never admitted the key: %d %q", code, body)
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ if !strings.Contains(buf.String(), "edge-local door open on "+path) {
+ t.Fatalf("no open line in the log:\n%s", buf.String())
+ }
+
+ e.closeDoor()
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Fatalf("socket still there after closeDoor: %v", err)
+ }
+}
+
+// With no store, no public key, or no usable address, armDoor opens nothing
+// and says why.
+func TestArmDoorStaysClosedAndSaysWhy(t *testing.T) {
+ fx := newDoorKeyFixture(t)
+ for _, tc := range []struct {
+ what, addr, want string
+ store bool
+ pub bool
+ }{
+ {"no store", doorTestAddr, "no deployment store", false, true},
+ {"no public key", doorTestAddr, "no release public key", true, false},
+ {"no address", "", "no usable public address", true, true},
+ {"private address", "10.1.2.3", "no usable public address", true, true},
+ } {
+ buf := captureLog(t)
+ e := &Edge{}
+ if tc.store {
+ e.staticStore = release.NewDirStore(t.TempDir())
+ }
+ if tc.pub {
+ e.staticPub = fx.pub
+ }
+ path := filepath.Join(shortSockDir(t), "d.sock")
+ WithEdgeDoor(path, tc.addr)(e)
+ if err := e.armDoor(context.Background()); err == nil {
+ t.Fatalf("%s: armDoor succeeded", tc.what)
+ }
+ if e.closeDoor != nil || e.doorKeys != nil {
+ t.Fatalf("%s: armDoor left a door or a mirror behind", tc.what)
+ }
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Fatalf("%s: something exists at the socket path: %v", tc.what, err)
+ }
+ if got := buf.String(); !strings.Contains(got, "edge-local door off") || !strings.Contains(got, tc.want) {
+ t.Fatalf("%s: log = %q, want the door off and %q", tc.what, got, tc.want)
+ }
+ }
+
+ // No door path: nothing at all, not even a line.
+ buf := captureLog(t)
+ e := &Edge{}
+ if err := e.armDoor(context.Background()); err != nil {
+ t.Fatalf("armDoor with no path: %v", err)
+ }
+ if buf.Len() != 0 {
+ t.Fatalf("armDoor with no path logged %q", buf.String())
+ }
+}
diff --git a/internal/edge/edge.go b/internal/edge/edge.go
index 840f58a85..d612fc368 100644
--- a/internal/edge/edge.go
+++ b/internal/edge/edge.go
@@ -245,6 +245,16 @@ type Edge struct {
// subdomain a backhaul target, so a per-host key would let a stranger
// grow this map without limit. Bounded by the deployment's box count.
backhaulInflight sync.Map // box → *atomic.Int64
+ // The edge-local door (door.go, #175 P7). doorPath and doorAddr come from
+ // WithEdgeDoor: the socket to listen on ("" = no door, the default) and
+ // the box's public address, which every door request presents as its
+ // own. doorKeys is the signed door-key mirror armDoor builds; nil admits
+ // nothing. closeDoor, set once the door is open, shuts it and removes the
+ // socket. All four are written before the door listens and never after.
+ doorPath string
+ doorAddr string
+ doorKeys *doorKeyMirror
+ closeDoor func()
}
// RunOption configures the Edge that Run/RunWithActivator* builds, before it
@@ -276,6 +286,15 @@ func WithIssuerContender(c func() bool) RunOption { return func(e *Edge) { e.con
// See homemap.go.
func WithBoxLabel(label string) RunOption { return func(e *Edge) { e.box = label } }
+// WithEdgeDoor asks for the edge-local door (door.go, #175 P7) on the unix
+// socket at path, presenting publicAddr, the box's public address, as the
+// address of every door request. The door opens once the home map and the
+// door-key mirror are armed, and only with a deployment store, a release
+// public key and a public address; otherwise the edge logs why it is off.
+func WithEdgeDoor(path, publicAddr string) RunOption {
+ return func(e *Edge) { e.doorPath, e.doorAddr = path, publicAddr }
+}
+
// upstream is one served host: the handler for its backend, plus — when the
// activator manages it — the route itself, so a request can trigger the wake.
type upstream struct {
@@ -1319,6 +1338,16 @@ func (e *Edge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
+ // The edge-local door's mark (door.go), read before anything answers. A
+ // door caller never reaches the steering probe, the tunnel hub or the
+ // store door: those names are unknown hosts to it in any spelling, and
+ // the two branches below are skipped for it outright. Only serveDoor on
+ // this box sets the mark; nothing carries it across a hop.
+ door, isDoor := doorCallerFrom(r.Context())
+ if isDoor && e.doorReservedHost(host) {
+ e.unknownHost(w, r)
+ return
+ }
// The deployment's steering health check (steeringprobe.go), answered
// FIRST — ahead of the tunnel hub, the hop, routing and, decisively, the
// unknown-host fallback. A -fallback edge redirects an unknown host with
@@ -1326,7 +1355,7 @@ func (e *Edge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// mechanism that withdraws a dead edge would report every edge alive
// forever. Inert (one comparison against "") unless this deployment
// steers.
- if e.steeringProbe(w, r, host) {
+ if !isDoor && e.steeringProbe(w, r, host) {
return
}
// The reserved tunnel hostname is answered by the hub itself — before the
@@ -1339,7 +1368,7 @@ func (e *Edge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Being ahead of the cache path is load-bearing too, not just tidy: the
// hub ends its handshake in a hijack, so it must be handed the REAL
// ResponseWriter, never a recordingResponseWriter wrapping it.
- if e.tunnels != nil && host == e.tunnels.Host() {
+ if !isDoor && e.tunnels != nil && host == e.tunnels.Host() {
// The fleet store door (storedoor.go) shares this hostname, under
// /store/ — the same host gate as registration, which is the whole
// reason it is reachable at all: it is not a registry route, so no
@@ -1378,6 +1407,20 @@ func (e *Edge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
e.homes.Load().staleHopRefusal(w, host)
return
}
+ // A door caller reaches a host served elsewhere only when the owner
+ // its home box published is the caller's app, and is dialled at
+ // exactly the box that same Owner read named, so the check and the
+ // dial cannot disagree. A hostname split between apps publishes no
+ // owner and is refused here. The far edge sees an ordinary hop.
+ if isDoor {
+ if acct, app, eip, box, ok := e.homes.Load().Owner(host); ok && acct == door.Account && app == door.App {
+ if e.backhaulTo(w, r, host, eip, box) {
+ return
+ }
+ }
+ e.unknownHost(w, r)
+ return
+ }
// Nothing here serves this host — but another box in the deployment
// may (backhaul, below). It answers false when no box homes the host,
// or when nothing could prove the hop, and today's unknown-host answer
@@ -1445,6 +1488,16 @@ func (e *Edge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
+ // The door's scope, checked against the FINAL upstream: after the login
+ // re-resolution above, so a login POST whose next names another app's
+ // prefix is refused before the gate judges its password, and before the
+ // meter, the gate, the cache and the dispatch below, which all use this
+ // same up.
+ if isDoor && !doorOwnsRoute(door, up.route) {
+ e.unknownHost(w, r)
+ return
+ }
+
// Observe every route this box serves except the platform's own status
// host (not billable, and not a member's app) — wrapping w here, once,
// before the upgrade/cache branching below, is what makes all three
@@ -2622,6 +2675,20 @@ func (e *Edge) backhaul(w http.ResponseWriter, r *http.Request, host string) boo
if !ok {
return false
}
+ return e.backhaulTo(w, r, host, eip, box)
+}
+
+// backhaulTo is backhaul's dial half: sign, contain, and forward to the box at
+// eip. Split out so the edge-local door dials exactly the target its own
+// Owner read returned (door.go); backhaul is Home plus this, unchanged. It
+// answers false only when no mesh key is mirrored, and it repeats backhaul's
+// arrived-hop guard so neither caller can forward a hop onward.
+func (e *Edge) backhaulTo(w http.ResponseWriter, r *http.Request, host, eip, box string) bool {
+ m := e.homes.Load()
+ if hopVerified(r.Context()) {
+ m.staleHopRefusal(w, host)
+ return true
+ }
// Sign with the mirror's cached pair, .next preferred (signingKey, mesh.go)
// — never a store read on the request path. NO KEY MEANS NO DIAL: signHop
// would happily HMAC under an empty key, the home edge would refuse the
@@ -2928,6 +2995,9 @@ func RunWithActivatorReady(args []string, act Activator, cache *Cache, ready fun
}
if *dev {
+ if e.doorPath != "" {
+ log.Printf("edge: edge-local door off in dev mode")
+ }
// ReadHeaderTimeout guards against Slowloris-style slow-header
// connection exhaustion. No ReadTimeout/WriteTimeout — those would
// break long-lived proxied WebSockets.
@@ -2996,6 +3066,13 @@ func RunWithActivatorReady(args []string, act Activator, cache *Cache, ready fun
go hm.run(make(chan struct{})) // process-lifetime; never stopped, like the cert mirror's
}
+ // The edge-local door (door.go, #175 P7), armed right after the home map
+ // it dials through: its own key mirror, one synchronous refresh, its
+ // loop, then the socket. Off, with the reason in the journal, when there
+ // is no store, no release key or no public address. Closed in the
+ // shutdown below, ahead of the public listeners.
+ _ = e.armDoor(context.Background())
+
m := &autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: certCache,
@@ -3112,6 +3189,9 @@ func RunWithActivatorReady(args []string, act Activator, cache *Cache, ready fun
// hijacked (not drained); they reconnect through the new edge.
<-stop
log.Printf("edge: draining and shutting down")
+ if e.closeDoor != nil {
+ e.closeDoor()
+ }
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = httpsSrv.Shutdown(ctx)
diff --git a/internal/edge/homemap.go b/internal/edge/homemap.go
index 6912e1b71..d51daa8be 100644
--- a/internal/edge/homemap.go
+++ b/internal/edge/homemap.go
@@ -955,6 +955,14 @@ func (m *homeMap) logStaleHop(host string) {
// before it.
func setForwarded(pr *httputil.ProxyRequest) {
pr.SetXForwarded()
+ // A door request is a request from this box (door.go): its RemoteAddr is
+ // already the box's public address, so SetXForwarded wrote the right
+ // chain. Only the scheme differs from the public route to the same URL,
+ // which would have arrived over TLS.
+ if _, ok := doorCallerFrom(pr.In.Context()); ok {
+ pr.Out.Header.Set("X-Forwarded-Proto", "https")
+ return
+ }
if !hopVerified(pr.In.Context()) {
return
}
diff --git a/internal/host/agent.go b/internal/host/agent.go
index af51537a3..255b399d9 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -507,6 +507,32 @@ func RunAgent(args []string) {
// The ready hook fires synchronously, just before the blocking serve, so
// tunnelLoop is handed off to its own goroutine rather than called here —
// calling it directly would block the front door forever.
+ edgeOpts := append([]edge.RunOption{
+ // Certificate issuance is a deployment-global duty, so it runs where
+ // the housekeeper runs (housekeeper.go). The gate goes DOWN into the
+ // edge as a func — this package imports internal/edge and never the
+ // reverse — and it is the GATE, not the bare flag: an ACME order runs
+ // for seconds to minutes, and the context ends the instant this box
+ // stops being the housekeeper. Armed above, so this closure is live
+ // and correct by the time the edge reads it.
+ edge.WithIssuerGate(housekeeperGate),
+ // Whether this box contends in that election AT ALL — the cert
+ // backfill's eligibility (#318). Mode is decided once from the
+ // environment, so the closure is stable; see housekeeperContends
+ // for the fleet-box exclusion it exists to state.
+ edge.WithIssuerContender(housekeeperContends),
+ // This box's own label, for the home map's own-box exclusion
+ // (internal/edge/homemap.go). metricsBoxLabel is the SAME call
+ // edgesFileLoop publishes under, deliberately: the map excludes our
+ // hosts by matching the box field of the file we ourselves wrote, so
+ // the two derivations must be one derivation. Without it the edge
+ // would offer nothing (fail-closed), never dial itself.
+ edge.WithBoxLabel(metricsBoxLabel(os.Getenv, readMachineID())),
+ },
+ // The edge-local door (#175 P7), only when CARLOS_EDGE_DOOR names a
+ // socket. It presents this box's public address, the one the edges
+ // file publishes; with none, the edge keeps the door shut and says so.
+ edgeDoorOptions(os.Getenv, readPublicIP)...)
edge.RunWithActivatorReady(args, act, cache, func(e *edge.Edge) {
go tunnelLoop(e)
// The edge owns the health probers, so its state table is only
@@ -532,27 +558,7 @@ func RunAgent(args []string) {
e.EnableAnalytics(am)
setAnalyticsSink(e.SetAnalyticsOn)
go usageLoop(m.Drain, am)
- },
- // Certificate issuance is a deployment-global duty, so it runs where
- // the housekeeper runs (housekeeper.go). The gate goes DOWN into the
- // edge as a func — this package imports internal/edge and never the
- // reverse — and it is the GATE, not the bare flag: an ACME order runs
- // for seconds to minutes, and the context ends the instant this box
- // stops being the housekeeper. Armed above, so this closure is live
- // and correct by the time the edge reads it.
- edge.WithIssuerGate(housekeeperGate),
- // Whether this box contends in that election AT ALL — the cert
- // backfill's eligibility (#318). Mode is decided once from the
- // environment, so the closure is stable; see housekeeperContends
- // for the fleet-box exclusion it exists to state.
- edge.WithIssuerContender(housekeeperContends),
- // This box's own label, for the home map's own-box exclusion
- // (internal/edge/homemap.go). metricsBoxLabel is the SAME call
- // edgesFileLoop publishes under, deliberately: the map excludes our
- // hosts by matching the box field of the file we ourselves wrote, so
- // the two derivations must be one derivation. Without it the edge
- // would offer nothing (fail-closed), never dial itself.
- edge.WithBoxLabel(metricsBoxLabel(os.Getenv, readMachineID())))
+ }, edgeOpts...)
// The edge has drained; take the children down too (they're this process's
// children — orphaning them would strand their leases until TTL expiry).
@@ -2146,6 +2152,20 @@ func readPublicIP() string {
return ip
}
+// edgeDoorOptions is the edge-local door's option (#175 P7): none unless
+// CARLOS_EDGE_DOOR names a socket path, and then edge.WithEdgeDoor with the
+// box's public address. publicAddr is a func so the address is read only for
+// a box that asked for the door: off EC2, readPublicIP can block for its
+// whole timeout. An empty address still passes through, and the edge logs
+// that the door is off for want of one.
+func edgeDoorOptions(getenv func(string) string, publicAddr func() string) []edge.RunOption {
+ p := strings.TrimSpace(getenv("CARLOS_EDGE_DOOR"))
+ if p == "" {
+ return nil
+ }
+ return []edge.RunOption{edge.WithEdgeDoor(p, publicAddr())}
+}
+
// ReadPublicIP is readPublicIP for callers outside this package — the relay
// advertises the box's Elastic IP as its relay address and learns it exactly
// as the edge does (relay spec §3, "Listeners"). Same contract: "" means
diff --git a/internal/host/agent_test.go b/internal/host/agent_test.go
index 41caac85a..ca22797b1 100644
--- a/internal/host/agent_test.go
+++ b/internal/host/agent_test.go
@@ -2309,3 +2309,37 @@ func TestServeAgentSocketStopsRetryingOnContextCancel(t *testing.T) {
t.Fatal("serveAgentSocket did not stop retrying after context cancellation")
}
}
+
+// The edge-local door is dormant unless CARLOS_EDGE_DOOR names a path, and
+// the box's public address is read only when it does: off EC2 that read
+// can block for its whole timeout.
+func TestEdgeDoorOptions(t *testing.T) {
+ reads := 0
+ publicAddr := func() string { reads++; return "203.0.113.10" }
+ for _, unset := range []string{"", " "} {
+ env := func(k string) string {
+ if k == "CARLOS_EDGE_DOOR" {
+ return unset
+ }
+ return ""
+ }
+ if opts := edgeDoorOptions(env, publicAddr); len(opts) != 0 {
+ t.Fatalf("CARLOS_EDGE_DOOR=%q: %d option(s), want none", unset, len(opts))
+ }
+ }
+ if reads != 0 {
+ t.Fatalf("the public address was read %d time(s) with the door off", reads)
+ }
+ env := func(k string) string {
+ if k == "CARLOS_EDGE_DOOR" {
+ return " /run/carlos/edge-local.sock "
+ }
+ return ""
+ }
+ if opts := edgeDoorOptions(env, publicAddr); len(opts) != 1 {
+ t.Fatalf("CARLOS_EDGE_DOOR set: %d option(s), want one", len(opts))
+ }
+ if reads != 1 {
+ t.Fatalf("the public address was read %d time(s), want once", reads)
+ }
+}