carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package edge
2
3// The edge-local door (#175 P7): a second listener, on a box-local unix
4// socket, through which an app's own processes reach any of the app's
5// instances, on this box or another, with an app-scoped door key.
6//
7// A door request is a request from this box (the P7 plan's Security model,
8// Paul's ruling of 2026-09-28). The door believes no caller-supplied visitor
9// address: it deletes every header that claims one and presents the box's own
10// public address, the one its edges file publishes. That is what the app would
11// see if the calling process requested the instance's public URL, since that
12// request would leave the box and come back through an edge from the same
13// address. So the door grants nothing a public request from this box lacks.
14// The key says which app is calling and confines it to that app's hosts; it
15// confers no privilege.
16//
17// The enforcement lives in ServeHTTP (edge.go), keyed on the door mark this
18// file puts on the request context:
19//
20// - the tunnel and steering-probe hostnames are unknown hosts to a door
21// caller, in any spelling, and their handlers never run;
22// - a host this box does not serve is dialled only when the owner its home
23// box published is the caller's app, and only at the box Owner named;
24// - a host this box serves is checked against the FINAL upstream, after
25// the gate's login re-resolution, before the gate, the cache and the
26// dispatch.
27//
28// Nothing carries the mark across a hop. A backhauled door request is an
29// ordinary hop, and acceptHop is unchanged.
30
31import (
32 "context"
33 "errors"
34 "fmt"
35 "log"
36 "net"
37 "net/http"
38 "os"
39 "path/filepath"
40 "strings"
41 "sync"
42 "time"
43
44 "github.com/carlosframework/platform/internal/doorkey"
45 "github.com/carlosframework/platform/internal/registry"
46 "github.com/carlosframework/platform/internal/release"
47)
48
49// doorCallerKey marks a request that came through the door. A context value
50// and never a header, for hopVerifiedKey's reason: nothing outside serveDoor
51// can put it on a request.
52type doorCallerKey struct{}
53
54// doorCaller is the app a door key admitted.
55type doorCaller struct{ Account, App string }
56
57func doorCallerFrom(ctx context.Context) (doorCaller, bool) {
58 c, ok := ctx.Value(doorCallerKey{}).(doorCaller)
59 return c, ok
60}
61
62// doorOwnsRoute is the whole ownership rule: the route's account and app are
63// the caller's.
64func doorOwnsRoute(c doorCaller, rt registry.Route) bool {
65 return release.RouteAccount(rt) == c.Account && rt.App == c.App
66}
67
68// doorReservedHost reports whether host names one of the handlers ServeHTTP
69// answers ahead of the route lookup: the tunnel hub (and the store door on
70// its hostname) and the steering probe. Compared lower-cased with one
71// trailing dot stripped, the normalization steeringProbe and
72// canonicalizeHopHost use, so a door caller cannot reach either through a
73// spelling the public branches would not match and a wildcard route would.
74func (e *Edge) doorReservedHost(host string) bool {
75 h := strings.ToLower(strings.TrimSuffix(host, "."))
76 if e.tunnels != nil && h == strings.ToLower(strings.TrimSuffix(e.tunnels.Host(), ".")) {
77 return true
78 }
79 return e.steeringHost != "" && h == strings.ToLower(strings.TrimSuffix(e.steeringHost, "."))
80}
81
82// doorAddressHeaders are the client-address headers beyond the four
83// stripForwardedHeaders deletes, each read as the visitor's address by some
84// framework (Rails reads Client-IP; python-ipware reads X-Forwarded and
85// Forwarded-For among others).
86var doorAddressHeaders = []string{"X-Forwarded", "Forwarded-For", "Client-IP", "X-Real-IP", "True-Client-IP",
87 "CF-Connecting-IP", "Fastly-Client-IP", "X-Client-IP", "X-Cluster-Client-IP", "X-Originating-IP"}
88
89// serveDoor is the door's handler: authenticate, rewrite, and hand the
90// request to ServeHTTP marked as a door caller.
91func (e *Edge) serveDoor(w http.ResponseWriter, r *http.Request) {
92 in, ok := e.doorRequest(r)
93 if !ok {
94 doorRefused(w)
95 return
96 }
97 e.ServeHTTP(w, in)
98}
99
100// doorRequest is serveDoor's first half, split out so a test can read the
101// request ServeHTTP will see. It always strips the key and the mesh's
102// headers; ok is false when the key is not admitted.
103func (e *Edge) doorRequest(r *http.Request) (*http.Request, bool) {
104 raw := r.Header.Get(doorkey.Header)
105 r.Header.Del(doorkey.Header)
106 // The mesh's vocabulary is never a door caller's to speak.
107 r.Header.Del(hopHeader)
108 r.Header.Del(hopAuthHeader)
109 tok, err := doorkey.ParseToken(raw)
110 if err != nil {
111 return nil, false
112 }
113 rec, ok := e.doorKeys.Record(tok.Account, tok.App)
114 if !ok || !rec.Admits(tok) {
115 return nil, false
116 }
117 // No visitor address is believed: every header that claims one goes,
118 // and the request comes from this box's own public address, exactly as
119 // it would through the public URL.
120 stripForwardedHeaders(r)
121 for _, h := range doorAddressHeaders {
122 r.Header.Del(h)
123 }
124 r.RemoteAddr = net.JoinHostPort(e.doorAddr, "0")
125 return r.WithContext(context.WithValue(r.Context(), doorCallerKey{}, doorCaller{Account: tok.Account, App: tok.App})), true
126}
127
128// doorRefused answers a missing, malformed, unknown or revoked key. The body
129// is the same for all four, so the answer says nothing about which keys or
130// apps exist.
131func doorRefused(w http.ResponseWriter) {
132 w.Header().Set("Cache-Control", "no-store")
133 http.Error(w, "door key refused", http.StatusUnauthorized)
134}
135
136// usableDoorAddr reports whether addr can stand for this box on the public
137// internet: a global unicast address that is not private. Loopback, a
138// private address or a placeholder would read as local or trusted to some
139// framework, which is the one thing the door must never present.
140func usableDoorAddr(addr string) bool {
141 ip := net.ParseIP(addr)
142 return ip != nil && ip.IsGlobalUnicast() && !ip.IsPrivate()
143}
144
145// openDoor listens on path and serves the door there until closeDoor. It
146// refuses with no key mirror (every key would be refused, and a door that
147// answers nothing but 401 hides a wiring fault) and with no usable public
148// address. The socket is 0660; its group comes from /run/carlos's setgid
149// carlos-sockets (bootstrap.go), so nothing here changes group.
150func (e *Edge) openDoor(path string) (closeDoor func(), err error) {
151 if e.doorKeys == nil {
152 return nil, errors.New("no door-key mirror")
153 }
154 if !usableDoorAddr(e.doorAddr) {
155 return nil, fmt.Errorf("%q is not a public address", e.doorAddr)
156 }
157 // A socket left by a previous run is replaced. Anything else at the
158 // path is not ours to remove.
159 if fi, err := os.Lstat(path); err == nil {
160 if fi.Mode()&os.ModeSocket == 0 {
161 return nil, fmt.Errorf("%s exists and is not a socket", path)
162 }
163 if err := os.Remove(path); err != nil {
164 return nil, err
165 }
166 } else if !errors.Is(err, os.ErrNotExist) {
167 return nil, err
168 }
169 // Bind in a private (0700) directory beside the target, chmod there, and
170 // only then rename into place, so the socket is never reachable at the
171 // umask's mode (Fable's final review). The rename stays on one
172 // filesystem, and the directory's setgid group still applies because the
173 // socket is created under /run/carlos.
174 stage, err := os.MkdirTemp(filepath.Dir(path), ".door-")
175 if err != nil {
176 return nil, err
177 }
178 defer os.RemoveAll(stage)
179 staged := filepath.Join(stage, "s")
180 ln, err := net.Listen("unix", staged)
181 if err != nil {
182 return nil, err
183 }
184 if ul, ok := ln.(*net.UnixListener); ok {
185 // The listener unlinks its own path on Close; after the rename that
186 // path is gone, and closeDoor removes the real one itself.
187 ul.SetUnlinkOnClose(false)
188 }
189 if err := os.Chmod(staged, 0o660); err != nil {
190 ln.Close()
191 return nil, err
192 }
193 if err := os.Rename(staged, path); err != nil {
194 ln.Close()
195 return nil, err
196 }
197 // ReadHeaderTimeout for the public listeners' Slowloris reason; no
198 // Read/WriteTimeout, which would cut long-lived streams.
199 srv := &http.Server{Handler: http.HandlerFunc(e.serveDoor), ReadHeaderTimeout: 10 * time.Second}
200 go func() {
201 if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
202 log.Printf("edge: edge-local door on %s stopped: %v", path, err)
203 }
204 }()
205 var once sync.Once
206 return func() {
207 once.Do(func() {
208 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
209 defer cancel()
210 _ = srv.Shutdown(ctx)
211 _ = os.Remove(path)
212 })
213 }, nil
214}
215
216// armDoor builds the key mirror, lands one refresh, starts its loop and opens
217// the door. RunWithActivatorReady calls it once the home map is stored and
218// running, so the first door request meets both mirrors. A door that cannot
219// open says why in the journal and returns the reason; with no door path it
220// does nothing.
221func (e *Edge) armDoor(ctx context.Context) error {
222 if e.doorPath == "" {
223 return nil
224 }
225 var why error
226 switch {
227 case e.staticStore == nil:
228 why = errors.New("no deployment store to read door keys from")
229 case len(e.staticPub) == 0:
230 why = errors.New("no release public key to verify door keys against")
231 case !usableDoorAddr(e.doorAddr):
232 why = fmt.Errorf("no usable public address for this box (%q)", e.doorAddr)
233 }
234 if why != nil {
235 log.Printf("edge: edge-local door off: %v", why)
236 return why
237 }
238 keys := newDoorKeyMirror(e.staticStore, e.staticPub)
239 rctx, cancel := context.WithTimeout(ctx, doorKeyTimeout)
240 if err := keys.Refresh(rctx); err != nil {
241 log.Printf("edge: first door-key refresh failed: %v. Every door key is refused until a later poll lands", err)
242 }
243 cancel()
244 e.doorKeys = keys
245 stop := make(chan struct{})
246 go keys.run(stop)
247 closeSrv, err := e.openDoor(e.doorPath)
248 if err != nil {
249 close(stop)
250 e.doorKeys = nil
251 log.Printf("edge: edge-local door off: cannot open %s: %v", e.doorPath, err)
252 return err
253 }
254 var once sync.Once
255 e.closeDoor = func() {
256 once.Do(func() {
257 closeSrv()
258 close(stop)
259 })
260 }
261 log.Printf("edge: edge-local door open on %s. Door requests arrive from %s", e.doorPath, e.doorAddr)
262 return nil
263}
264