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, Tasks 5 to 7). Every request here is driven
4// through serveDoor, the door's own handler, exactly as the unix listener
5// hands it one, except where a test says it drives the real socket.
6
7import (
8 "bytes"
9 "context"
10 "fmt"
11 "io"
12 "net"
13 "net/http"
14 "net/http/httptest"
15 "net/url"
16 "os"
17 "path/filepath"
18 "strings"
19 "sync"
20 "testing"
21 "time"
22
23 "github.com/carlosframework/platform/internal/doorkey"
24 "github.com/carlosframework/platform/internal/gate"
25 "github.com/carlosframework/platform/internal/registry"
26 "github.com/carlosframework/platform/internal/release"
27 "github.com/carlosframework/platform/internal/seeds"
28)
29
30// doorTestAddr is the box's public address in these tests. It is TEST-NET-3
31// (RFC 5737): global unicast and not private by Go's rules, so openDoor
32// accepts it.
33const doorTestAddr = "203.0.113.10"
34
35// ---- the upstream -----------------------------------------------------------
36
37// doorUpstream is a tenant app that records everything the edge handed it.
38type doorUpstream struct {
39 mu sync.Mutex
40 n int
41 hdr http.Header
42 peer string
43}
44
45func (u *doorUpstream) seen() (int, http.Header, string) {
46 u.mu.Lock()
47 defer u.mu.Unlock()
48 return u.n, u.hdr.Clone(), u.peer
49}
50
51func newDoorUpstream(t *testing.T, body string) (*httptest.Server, *doorUpstream) {
52 t.Helper()
53 u := &doorUpstream{}
54 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 u.mu.Lock()
56 u.n++
57 u.hdr = r.Header.Clone()
58 u.peer = r.RemoteAddr
59 u.mu.Unlock()
60 io.WriteString(w, body)
61 }))
62 t.Cleanup(ts.Close)
63 return ts, u
64}
65
66// ---- keys ---------------------------------------------------------------------
67
68// mint returns a raw door key for account/app and the entry a record lists
69// for it.
70func (fx *doorKeyFixture) mint(t *testing.T, account, app string) (string, doorkey.Entry) {
71 t.Helper()
72 raw, entry, err := doorkey.NewToken(account, app)
73 if err != nil {
74 t.Fatal(err)
75 }
76 return raw, entry
77}
78
79// record signs a record for account/app listing exactly entries.
80func (fx *doorKeyFixture) record(t *testing.T, account, app string, rev uint64, entries ...doorkey.Entry) []byte {
81 t.Helper()
82 now := fx.clk.now()
83 wire, err := doorkey.Sign(fx.priv, doorkey.Record{V: 1, Account: account, App: app, Revision: rev,
84 Keys: entries, SignedAt: now, ExpiresAt: now.Add(doorkey.Lifetime)})
85 if err != nil {
86 t.Fatal(err)
87 }
88 return wire
89}
90
91// ---- the scene -----------------------------------------------------------------
92
93const (
94 doorOwnHost = "own.test" // bae/titogo, the caller's app
95 doorOtherHost = "other.test" // bae/other
96 doorSplitHost = "split.test" // bae/titogo; /a/ to titogo, /b/ to other
97 doorGatedHost = "gated.test" // bae/titogo, behind the password gate
98 doorWildParent = "apps.test" // *.apps.test is an alias of own.test
99 doorTunnelHost = "tunnel.apps.test"
100 doorSteerHost = "edge.apps.test"
101 doorPassword = "correct horse"
102 doorOwnBody = "the caller's own app"
103 doorOtherBody = "another app"
104 doorSplitABody = "split a"
105 doorSplitBBody = "split b"
106 doorGatedBody = "gated page"
107 doorFrontBody = "split front"
108 doorHomeEdgeKey = "betty"
109)
110
111type doorScene struct {
112 e *Edge
113 fx *doorKeyFixture
114 store release.Store
115 // key is a valid door key for bae/titogo; otherKey for bae/other.
116 key, otherKey string
117 entry doorkey.Entry
118 own, other *doorUpstream
119 splitA *doorUpstream
120 splitB *doorUpstream
121 gated *doorUpstream
122}
123
124// newDoorScene is an edge serving two apps' routes locally, with a door-key
125// mirror over its own store and the box address set. It is built the way the
126// package's other tests build an Edge (openReg, New), with the mirror and the
127// address set the way armDoor sets them.
128func newDoorScene(t *testing.T) *doorScene {
129 t.Helper()
130 s := &doorScene{fx: newDoorKeyFixture(t), store: meshTestStore(t)}
131 ownSrv, own := newDoorUpstream(t, doorOwnBody)
132 otherSrv, other := newDoorUpstream(t, doorOtherBody)
133 aSrv, a := newDoorUpstream(t, doorSplitABody)
134 bSrv, b := newDoorUpstream(t, doorSplitBBody)
135 frontSrv, _ := newDoorUpstream(t, doorFrontBody)
136 gatedSrv, gated := newDoorUpstream(t, doorGatedBody)
137 s.own, s.other, s.splitA, s.splitB, s.gated = own, other, a, b, gated
138
139 hash, err := gate.Hash(doorPassword)
140 if err != nil {
141 t.Fatal(err)
142 }
143 reg := openReg(t)
144 for _, rt := range []registry.Route{
145 {Host: doorOwnHost, Addr: ownSrv.Listener.Addr().String(), Account: "bae", App: "titogo"},
146 {Host: doorOtherHost, Addr: otherSrv.Listener.Addr().String(), Account: "bae", App: "other"},
147 {Host: doorSplitHost, Addr: frontSrv.Listener.Addr().String(), Account: "bae", App: "titogo"},
148 {Host: "a.target.test", Addr: aSrv.Listener.Addr().String(), Account: "bae", App: "titogo"},
149 // The other app's prefix is gated, so a login POST naming it is
150 // the one that would mint a cookie if the door let it through.
151 {Host: "b.target.test", Addr: bSrv.Listener.Addr().String(), Account: "bae", App: "other", Gate: hash},
152 {Host: doorGatedHost, Addr: gatedSrv.Listener.Addr().String(), Account: "bae", App: "titogo", Gate: hash},
153 } {
154 rt.Kind, rt.Status = "service", "ready"
155 if err := reg.Upsert(rt); err != nil {
156 t.Fatalf("upsert %s: %v", rt.Host, err)
157 }
158 }
159 upsertPath(t, reg, doorSplitHost, "a", "a.target.test")
160 upsertPath(t, reg, doorSplitHost, "b", "b.target.test")
161 // A wildcard owned by the caller that would answer the reserved names
162 // if the door let them through to the lookup.
163 upsertAlias(t, reg, "*."+doorWildParent, doorOwnHost)
164
165 e, err := New(reg, nil)
166 if err != nil {
167 t.Fatal(err)
168 }
169 t.Cleanup(e.probers.Stop)
170 e.EnableTunnels(NewTunnelHub(doorTunnelHost, map[string]string{"pi-box": "sekrit"}))
171 e.steeringHost = doorSteerHost
172
173 var otherEntry doorkey.Entry
174 s.key, s.entry = s.fx.mint(t, "bae", "titogo")
175 s.otherKey, otherEntry = s.fx.mint(t, "bae", "other")
176 putJSON(t, s.store, doorkey.RecordKey("bae", "titogo"), string(s.fx.record(t, "bae", "titogo", 1, s.entry)))
177 putJSON(t, s.store, doorkey.RecordKey("bae", "other"), string(s.fx.record(t, "bae", "other", 1, otherEntry)))
178 e.doorKeys = s.fx.mirror(s.store)
179 refreshDoorKeys(t, e.doorKeys)
180 e.doorAddr = doorTestAddr
181 s.e = e
182 return s
183}
184
185// do drives one request through the door with key (none if "").
186func (s *doorScene) do(t *testing.T, method, host, target, key string, hdr map[string]string, body io.Reader) *httptest.ResponseRecorder {
187 t.Helper()
188 req := httptest.NewRequest(method, "http://"+host+target, body)
189 req.Host = host
190 // What a unix listener reports as the peer.
191 req.RemoteAddr = "@"
192 if key != "" {
193 req.Header.Set(doorkey.Header, key)
194 }
195 for k, v := range hdr {
196 req.Header.Set(k, v)
197 }
198 rec := httptest.NewRecorder()
199 s.e.serveDoor(rec, req)
200 return rec
201}
202
203func wantUnknownHost(t *testing.T, rec *httptest.ResponseRecorder, what string) {
204 t.Helper()
205 if rec.Code != http.StatusNotFound || !strings.Contains(rec.Body.String(), "unknown host") {
206 t.Fatalf("%s: %d %q, want the unknown-host answer", what, rec.Code, rec.Body.String())
207 }
208}
209
210func wantServed(t *testing.T, rec *httptest.ResponseRecorder, body, what string) {
211 t.Helper()
212 if rec.Code != http.StatusOK || rec.Body.String() != body {
213 t.Fatalf("%s: %d %q, want 200 %q", what, rec.Code, rec.Body.String(), body)
214 }
215}
216
217func upstreamCalls(u *doorUpstream) int {
218 n, _, _ := u.seen()
219 return n
220}
221
222// ---- Task 5: enforcement in ServeHTTP ------------------------------------------
223
224// 1. A door caller reaches its own app's local route.
225func TestDoorServesItsOwnAppsLocalRoute(t *testing.T) {
226 s := newDoorScene(t)
227 wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, nil, nil), doorOwnBody, "own route")
228 if upstreamCalls(s.own) != 1 {
229 t.Fatal("the caller's own upstream was not reached")
230 }
231}
232
233// 2. Another app's local route answers unknown host, and its upstream never
234// hears of it.
235func TestDoorRefusesAnotherAppsLocalRoute(t *testing.T) {
236 s := newDoorScene(t)
237 wantUnknownHost(t, s.do(t, http.MethodGet, doorOtherHost, "/", s.key, nil, nil), "another app's route")
238 if n := upstreamCalls(s.other); n != 0 {
239 t.Fatalf("the other app's upstream saw %d request(s)", n)
240 }
241 // The other app's own key reaches it, so the refusal is the scope and
242 // not the route.
243 wantServed(t, s.do(t, http.MethodGet, doorOtherHost, "/", s.otherKey, nil, nil), doorOtherBody, "other app with its own key")
244}
245
246// 3. On a path-prefix host the check is against the route the prefix picks.
247func TestDoorPathPrefixHostChecksTheFinalRoute(t *testing.T) {
248 s := newDoorScene(t)
249 wantServed(t, s.do(t, http.MethodGet, doorSplitHost, "/a/x", s.key, nil, nil), doorSplitABody, "/a/x")
250 wantUnknownHost(t, s.do(t, http.MethodGet, doorSplitHost, "/b/x", s.key, nil, nil), "/b/x")
251 if n := upstreamCalls(s.splitB); n != 0 {
252 t.Fatalf("the other app's prefix upstream saw %d request(s)", n)
253 }
254}
255
256// 4. A gate login POST whose next names another app's prefix is refused
257// before the gate judges the password: the password is right, and still no
258// cookie comes back.
259func TestDoorGateLoginPostForAnotherAppsPrefixIsUnknownHost(t *testing.T) {
260 s := newDoorScene(t)
261 for _, next := range []string{"/b/x", "/%62/x", "/b?tab=1"} {
262 form := url.Values{"password": {doorPassword}, "next": {next}}
263 rec := s.do(t, http.MethodPost, doorSplitHost, gate.LoginPath, s.key,
264 map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, strings.NewReader(form.Encode()))
265 wantUnknownHost(t, rec, "login POST next="+next)
266 if c := rec.Result().Cookies(); len(c) != 0 {
267 t.Fatalf("next=%s: the gate judged the password and set %v", next, c)
268 }
269 }
270 // The control: the same POST from the other app's own key does get a
271 // cookie, so the refusal above is the door's and not a broken form.
272 form := url.Values{"password": {doorPassword}, "next": {"/b/x"}}
273 rec := s.do(t, http.MethodPost, doorSplitHost, gate.LoginPath, s.otherKey,
274 map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, strings.NewReader(form.Encode()))
275 if rec.Code != http.StatusSeeOther || len(rec.Result().Cookies()) == 0 {
276 t.Fatalf("control: the other app's own login = %d, cookies %v", rec.Code, rec.Result().Cookies())
277 }
278}
279
280// 5. The tunnel and steering-probe names answer unknown host to a door
281// caller in any spelling, although the caller's own wildcard would match
282// them, and neither the hub nor the probe runs.
283func TestDoorRefusesReservedHostnames(t *testing.T) {
284 s := newDoorScene(t)
285 // Preconditions: the public door answers both names itself.
286 pub := httptest.NewServer(s.e)
287 t.Cleanup(pub.Close)
288 if code, body := getHost(t, pub.URL, doorSteerHost); code != http.StatusOK || !strings.Contains(body, steeringProbeBody) {
289 t.Fatalf("precondition: the public steering probe answered %d %q", code, body)
290 }
291 for _, host := range []string{
292 doorTunnelHost, strings.ToUpper(doorTunnelHost), doorTunnelHost + ".", "Tunnel.Apps.Test.",
293 doorSteerHost, strings.ToUpper(doorSteerHost), doorSteerHost + ".", "Edge.Apps.Test.",
294 } {
295 for _, path := range []string{"/", tunnelRegisterPath, storeDoorPrefix + "x"} {
296 rec := s.do(t, http.MethodGet, host, path, s.key, map[string]string{
297 "Carlos-Tunnel-Name": "pi-box", "Authorization": "Bearer sekrit"}, nil)
298 wantUnknownHost(t, rec, host+path)
299 if strings.Contains(rec.Body.String(), steeringProbeBody) {
300 t.Fatalf("%s%s: the steering probe answered a door caller", host, path)
301 }
302 }
303 }
304 if n := upstreamCalls(s.own); n != 0 {
305 t.Fatalf("the wildcard's upstream saw %d request(s) for a reserved name", n)
306 }
307 // And an ordinary name under the same wildcard is the caller's.
308 wantServed(t, s.do(t, http.MethodGet, "anything."+doorWildParent, "/", s.key, nil, nil), doorOwnBody, "wildcard name")
309}
310
311// 6. A host served on another box: the door checks the owner that box
312// published, and dials the box Owner named.
313func TestDoorRemoteHostChecksThePublishedOwner(t *testing.T) {
314 s := newDoorScene(t)
315 if err := mintMeshKey(context.Background(), s.store); err != nil {
316 t.Fatal(err)
317 }
318 rec, eip := fakeHomeEdge(t, nil)
319 putJSON(t, s.store, "control/edges/"+doorHomeEdgeKey+".json", fmt.Sprintf(`{"v":1,"box":%q,"eip":%q,"hosts":{`+
320 `"example.com":{"role":"home","account":"bae","app":"titogo"},`+
321 `"foreign.example":{"role":"home","account":"bae","app":"other"},`+
322 `"mixed.example":{"role":"home"}}}`, doorHomeEdgeKey, eip))
323 m := newHomeMap(s.store, "annie")
324 refreshHomeMap(t, m)
325 s.e.homes.Store(m)
326 if acct, app, gotEIP, box, ok := m.Owner("example.com"); !ok || acct != "bae" || app != "titogo" || gotEIP != eip || box != doorHomeEdgeKey {
327 t.Fatalf("precondition: Owner = %q %q %q %q %v", acct, app, gotEIP, box, ok)
328 }
329
330 r := s.do(t, http.MethodGet, "example.com", "/", s.key, nil, nil)
331 if r.Code != http.StatusOK || r.Body.String() != homeEdgeBody {
332 t.Fatalf("owned remote host = %d %q, want the home edge's answer", r.Code, r.Body.String())
333 }
334 seen := rec.get()
335 if seen.n != 1 || seen.host != "example.com" || seen.hop != "1" || seen.auth == "" {
336 t.Fatalf("home edge saw %+v, want one signed hop for example.com", seen)
337 }
338 if _, ok := s.e.backhaulInflight.Load(doorHomeEdgeKey); !ok {
339 t.Fatalf("the hop was not counted against box %q, the box Owner named", doorHomeEdgeKey)
340 }
341
342 for _, host := range []string{"foreign.example", "mixed.example", "nobody.example"} {
343 wantUnknownHost(t, s.do(t, http.MethodGet, host, "/", s.key, nil, nil), host)
344 }
345 if got := rec.get().n; got != 1 {
346 t.Fatalf("the home edge saw %d request(s), want only the owned one", got)
347 }
348}
349
350// 7. The mark comes only from serveDoor: a public request carrying the door
351// key and any other X-Carlos-* header is an ordinary client, neither scoped
352// nor given the door's scheme and address.
353func TestDoorMarkNeverComesFromHeaders(t *testing.T) {
354 s := newDoorScene(t)
355 pub := httptest.NewServer(s.e)
356 t.Cleanup(pub.Close)
357 req, _ := http.NewRequest(http.MethodGet, pub.URL+"/", nil)
358 req.Host = doorOtherHost
359 for _, h := range []string{"X-Carlos-Door", "X-Carlos-Door-Caller", "X-Carlos-Account", "X-Carlos-App"} {
360 req.Header.Set(h, "bae/titogo")
361 }
362 req.Header.Set(doorkey.Header, s.key)
363 resp, err := http.DefaultClient.Do(req)
364 if err != nil {
365 t.Fatal(err)
366 }
367 body := readBody(t, resp)
368 resp.Body.Close()
369 if resp.StatusCode != http.StatusOK || body != doorOtherBody {
370 t.Fatalf("public request = %d %q, want the route served as to any client", resp.StatusCode, body)
371 }
372 _, hdr, _ := s.other.seen()
373 if got := hdr.Get("X-Forwarded-Proto"); got != "http" {
374 t.Fatalf("X-Forwarded-Proto = %q, want http: the door's scheme was applied to a public request", got)
375 }
376 if got := hdr.Get("X-Forwarded-For"); got == doorTestAddr {
377 t.Fatal("a public request was given the door's address")
378 }
379 // The public steering probe still answers for a request carrying a key.
380 req, _ = http.NewRequest(http.MethodGet, pub.URL+"/", nil)
381 req.Host = doorSteerHost
382 req.Header.Set(doorkey.Header, s.key)
383 resp, err = http.DefaultClient.Do(req)
384 if err != nil {
385 t.Fatal(err)
386 }
387 if body := readBody(t, resp); resp.StatusCode != http.StatusOK || !strings.Contains(body, steeringProbeBody) {
388 t.Fatalf("public probe with a door key = %d %q", resp.StatusCode, body)
389 }
390 resp.Body.Close()
391}
392
393// ---- Task 6: the handler --------------------------------------------------------
394
395// 1. No key, a malformed key, a key for an app with no record, and a revoked
396// key are 401 with no-store, and nothing upstream runs.
397func TestDoorRefusesMissingMalformedAndRevokedKeys(t *testing.T) {
398 s := newDoorScene(t)
399 stranger, _ := s.fx.mint(t, "bae", "norecord")
400 forged := s.key[:len(s.key)-1] + "A"
401 if forged == s.key {
402 forged = s.key[:len(s.key)-1] + "B"
403 }
404 check := func(what, key string) {
405 t.Helper()
406 rec := s.do(t, http.MethodGet, doorOwnHost, "/", key, nil, nil)
407 if rec.Code != http.StatusUnauthorized || strings.TrimSpace(rec.Body.String()) != "door key refused" {
408 t.Fatalf("%s: %d %q, want 401 door key refused", what, rec.Code, rec.Body.String())
409 }
410 if cc := rec.Header().Get("Cache-Control"); cc != "no-store" {
411 t.Fatalf("%s: Cache-Control = %q, want no-store", what, cc)
412 }
413 }
414 check("no key", "")
415 check("malformed key", "cdk1.bae.titogo.nope")
416 check("unknown app", stranger)
417 check("wrong secret", forged)
418
419 wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, nil, nil), doorOwnBody, "before the revoke")
420 putJSON(t, s.store, doorkey.RecordKey("bae", "titogo"), string(s.fx.record(t, "bae", "titogo", 2)))
421 refreshDoorKeys(t, s.e.doorKeys)
422 check("revoked key", s.key)
423 if n := upstreamCalls(s.own); n != 1 {
424 t.Fatalf("upstream saw %d request(s), want only the one before the revoke", n)
425 }
426}
427
428// 2. The key never reaches the app; the app's own headers do.
429func TestDoorStripsItsKeyAndPassesAppHeaders(t *testing.T) {
430 s := newDoorScene(t)
431 wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key,
432 map[string]string{"X-Tito-Auth-User": "u_123", "X-Tito-Auth-Sig": "sig"}, nil), doorOwnBody, "valid key")
433 _, hdr, _ := s.own.seen()
434 if v := hdr.Get(doorkey.Header); v != "" {
435 t.Fatalf("the door key reached the app: %q", v)
436 }
437 if hdr.Get("X-Tito-Auth-User") != "u_123" || hdr.Get("X-Tito-Auth-Sig") != "sig" {
438 t.Fatalf("the app's own headers were touched: %v", hdr)
439 }
440}
441
442// 3. Hop vocabulary from a door caller goes nowhere: not to the app, and a
443// correctly signed hop for a host not served here still meets the door's
444// owner check (unknown host), never the stale-hop 503.
445func TestDoorDropsHopVocabulary(t *testing.T) {
446 s := newDoorScene(t)
447 if err := mintMeshKey(context.Background(), s.store); err != nil {
448 t.Fatal(err)
449 }
450 m := newHomeMap(s.store, "annie")
451 refreshHomeMap(t, m)
452 s.e.homes.Store(m)
453 key, _ := m.meshKeys()
454
455 wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, map[string]string{
456 hopHeader: "1", hopAuthHeader: signHop(key, doorOwnHost, time.Now())}, nil), doorOwnBody, "local route")
457 _, hdr, _ := s.own.seen()
458 if hdr.Get(hopHeader) != "" || hdr.Get(hopAuthHeader) != "" {
459 t.Fatalf("hop headers reached the app: %v", hdr)
460 }
461
462 rec := s.do(t, http.MethodGet, "elsewhere.test", "/", s.key, map[string]string{
463 hopHeader: "1", hopAuthHeader: signHop(key, "elsewhere.test", time.Now())}, nil)
464 if rec.Code == http.StatusServiceUnavailable {
465 t.Fatal("a door caller's signed hop earned the stale-hop refusal")
466 }
467 wantUnknownHost(t, rec, "signed hop for an unserved host")
468}
469
470// 4. Every claimed address goes; the app sees the box's public address and
471// https, whether or not the caller sent any; the visitor hash and the gate's
472// limiter key read the same address.
473func TestDoorPresentsTheBoxAddress(t *testing.T) {
474 s := newDoorScene(t)
475 for _, sent := range []map[string]string{
476 {"X-Forwarded-For": "198.51.100.7", "Forwarded": "for=198.51.100.9", "X-Forwarded-Proto": "http", "X-Forwarded-Host": "evil.test"},
477 nil,
478 } {
479 wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, sent, nil), doorOwnBody, "door request")
480 _, hdr, _ := s.own.seen()
481 if got := hdr.Values("X-Forwarded-For"); len(got) != 1 || got[0] != doorTestAddr {
482 t.Fatalf("sent %v: X-Forwarded-For = %q, want exactly %s", sent, got, doorTestAddr)
483 }
484 if got := hdr.Values("X-Forwarded-Proto"); len(got) != 1 || got[0] != "https" {
485 t.Fatalf("sent %v: X-Forwarded-Proto = %q, want https", sent, got)
486 }
487 if got := hdr.Get("Forwarded"); got != "" {
488 t.Fatalf("sent %v: Forwarded = %q reached the app", sent, got)
489 }
490 if got := hdr.Get("X-Forwarded-Host"); got != doorOwnHost {
491 t.Fatalf("sent %v: X-Forwarded-Host = %q, want %s", sent, got, doorOwnHost)
492 }
493 }
494
495 req := httptest.NewRequest(http.MethodGet, "http://"+doorOwnHost+"/", nil)
496 req.RemoteAddr = "@"
497 req.Header.Set(doorkey.Header, s.key)
498 req.Header.Set("X-Forwarded-For", "198.51.100.7")
499 in, ok := s.e.doorRequest(req)
500 if !ok {
501 t.Fatal("doorRequest refused a valid key")
502 }
503 if got := clientIP(in); got != doorTestAddr {
504 t.Fatalf("clientIP = %q, want %s", got, doorTestAddr)
505 }
506 // The gate's limiter key is RemoteAddr without its port (gate.clientKey).
507 if host, _, err := net.SplitHostPort(in.RemoteAddr); err != nil || host != doorTestAddr {
508 t.Fatalf("RemoteAddr = %q, want %s:<port>", in.RemoteAddr, doorTestAddr)
509 }
510 if c, ok := doorCallerFrom(in.Context()); !ok || c.Account != "bae" || c.App != "titogo" {
511 t.Fatalf("door mark = %+v %v", c, ok)
512 }
513}
514
515// allAlternates sets every name in doorAddressHeaders, plus the four
516// stripForwardedHeaders names, to addr.
517func allAlternates(addr string) map[string]string {
518 h := map[string]string{"Forwarded": "for=" + addr, "X-Forwarded-For": addr, "X-Forwarded-Host": "evil.test", "X-Forwarded-Proto": "http"}
519 for _, n := range doorAddressHeaders {
520 h[n] = addr
521 }
522 return h
523}
524
525// 4b. None of the alternate address headers reaches the app.
526func TestDoorStripsAlternateAddressHeaders(t *testing.T) {
527 s := newDoorScene(t)
528 for _, want := range []string{"X-Forwarded", "Forwarded-For", "Client-IP", "X-Real-IP", "True-Client-IP",
529 "CF-Connecting-IP", "Fastly-Client-IP", "X-Client-IP", "X-Cluster-Client-IP", "X-Originating-IP"} {
530 found := false
531 for _, n := range doorAddressHeaders {
532 found = found || n == want
533 }
534 if !found {
535 t.Fatalf("doorAddressHeaders lacks %s", want)
536 }
537 }
538 wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, allAlternates("8.8.8.8"), nil), doorOwnBody, "door request")
539 _, hdr, _ := s.own.seen()
540 for _, n := range doorAddressHeaders {
541 if v := hdr.Values(n); len(v) != 0 {
542 t.Fatalf("%s = %q reached the app", n, v)
543 }
544 }
545 if got := hdr.Get("Forwarded"); got != "" {
546 t.Fatalf("Forwarded = %q reached the app", got)
547 }
548}
549
550// ---- the two resolver rule sets (4c) ----------------------------------------------
551
552func mustPrefixes(cidrs ...string) []*net.IPNet {
553 out := make([]*net.IPNet, 0, len(cidrs))
554 for _, c := range cidrs {
555 _, n, err := net.ParseCIDR(c)
556 if err != nil {
557 panic(err)
558 }
559 out = append(out, n)
560 }
561 return out
562}
563
564func inAny(ip net.IP, nets []*net.IPNet) bool {
565 for _, n := range nets {
566 if n.Contains(ip) {
567 return true
568 }
569 }
570 return false
571}
572
573// railsTrusted is ActionDispatch::RemoteIp::TRUSTED_PROXIES (Rails 8.0.2).
574var railsTrusted = mustPrefixes("127.0.0.0/8", "::1/128", "fc00::/7", "10.0.0.0/8", "172.16.0.0/12",
575 "192.168.0.0/16", "169.254.0.0/16", "fe80::/10")
576
577// railsIPs is RemoteIp#ips_from: a comma list, non-IP entries dropped.
578func railsIPs(values []string) []string {
579 var out []string
580 for _, v := range values {
581 for _, p := range strings.Split(v, ",") {
582 p = strings.TrimSpace(p)
583 if net.ParseIP(p) != nil {
584 out = append(out, p)
585 }
586 }
587 }
588 return out
589}
590
591func reversed(s []string) []string {
592 out := make([]string, len(s))
593 for i, v := range s {
594 out[len(s)-1-i] = v
595 }
596 return out
597}
598
599// railsRemoteIP is Rails 8.0.2's RemoteIp::GetIp#calculate_ip, with the
600// spoofing check off (its default outcome when the headers agree):
601//
602// remote_addr = ips_from(REMOTE_ADDR).last
603// client_ips = ips_from(Client-IP).reverse
604// forwarded_ips = ips_from(X-Forwarded-For).reverse
605// ips = forwarded_ips + client_ips
606// filter_proxies([remote_addr] + ips).first || ips.last || remote_addr
607func railsRemoteIP(h http.Header, peer string) string {
608 ips := append(reversed(railsIPs(h.Values("X-Forwarded-For"))), reversed(railsIPs(h.Values("Client-IP")))...)
609 for _, ip := range append([]string{peer}, ips...) {
610 if !inAny(net.ParseIP(ip), railsTrusted) {
611 return ip
612 }
613 }
614 if len(ips) > 0 {
615 return ips[len(ips)-1]
616 }
617 return peer
618}
619
620// pythonPrivate is ipaddress.IPv4Address/IPv6Address.is_private's range
621// list (CPython 3.12). 203.0.113.0/24 and 198.51.100.0/24 are on it.
622var 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",
623 "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",
624 "203.0.113.0/24", "240.0.0.0/4", "255.255.255.255/32",
625 "::1/128", "::/128", "::ffff:0:0/96", "100::/64", "2001::/23", "2001:2::/48", "2001:db8::/32",
626 "2001:10::/28", "fc00::/7", "fe80::/10")
627
628var pythonShared = mustPrefixes("100.64.0.0/10")
629
630// ipwareHeaders is python-ipware 3.0's default precedence, as HTTP header
631// names, REMOTE_ADDR last.
632var ipwareHeaders = []string{"X-Forwarded-For", "Client-IP", "X-Real-IP", "X-Forwarded", "X-Cluster-Client-IP",
633 "Forwarded-For", "Forwarded", "CF-Connecting-IP", "X-Client-IP", "True-Client-IP", "Fastly-Client-IP"}
634
635// ipwareClientIP is python-ipware 3.0's IpWare.get_client_ip(meta) with no
636// proxy count or trusted list: headers in precedence order, the left-most
637// address of each; the first global one wins at once, otherwise the first
638// private one, otherwise the first loopback one.
639func ipwareClientIP(h http.Header, peer string) (ip string, routable bool) {
640 var private, loopback []string
641 consider := func(v string) (string, bool) {
642 first := strings.TrimSpace(strings.Split(v, ",")[0])
643 p := net.ParseIP(first)
644 if p == nil {
645 return "", false
646 }
647 switch {
648 case p.IsLoopback():
649 loopback = append(loopback, first)
650 case inAny(p, pythonPrivate) || inAny(p, pythonShared):
651 private = append(private, first)
652 default:
653 return first, true
654 }
655 return "", false
656 }
657 for _, name := range ipwareHeaders {
658 if v := strings.Join(h.Values(name), ", "); strings.TrimSpace(v) != "" {
659 if ip, ok := consider(v); ok {
660 return ip, true
661 }
662 }
663 }
664 if ip, ok := consider(peer); ok {
665 return ip, true
666 }
667 if len(private) > 0 {
668 return private[0], false
669 }
670 if len(loopback) > 0 {
671 return loopback[0], false
672 }
673 return "", false
674}
675
676// resolveBoth feeds the upstream's headers and peer to both rule sets.
677func resolveBoth(t *testing.T, what string, hdr http.Header, peer, want string) {
678 t.Helper()
679 host, _, err := net.SplitHostPort(peer)
680 if err != nil {
681 host = peer
682 }
683 if !net.ParseIP(host).IsLoopback() {
684 t.Fatalf("%s: test bug: the upstream's peer %q is not loopback", what, host)
685 }
686 if got := railsRemoteIP(hdr, host); got != want {
687 t.Fatalf("%s: Rails RemoteIp resolves %q, want %s", what, got, want)
688 }
689 got, routable := ipwareClientIP(hdr, host)
690 if got != want {
691 t.Fatalf("%s: python-ipware resolves %q, want %s", what, got, want)
692 }
693 // ipware's routable flag is Python's is_global. TEST-NET-3 is on
694 // Python's private list, so the plan's documentation address is not
695 // routable there; a real EIP is.
696 if wantRoutable := !inAny(net.ParseIP(want), pythonPrivate); routable != wantRoutable {
697 t.Fatalf("%s: python-ipware routable = %v for %s, want %v", what, routable, want, wantRoutable)
698 }
699}
700
701// 4c. With every alternate address header set to a global address, both
702// resolvers still land on the box address, never loopback, a private address
703// or the caller's claim. The same inbound headers, unstripped, resolve to
704// the claim under both, which is what makes the rule sets a real test.
705func TestDoorResolvesToTheBoxAddressUnderRailsAndIpware(t *testing.T) {
706 inbound := http.Header{}
707 for k, v := range allAlternates("8.8.8.8") {
708 inbound.Set(k, v)
709 }
710 if got := railsRemoteIP(inbound, "127.0.0.1"); got != "8.8.8.8" {
711 t.Fatalf("control: Rails resolves the raw inbound headers to %q, want the claim", got)
712 }
713 inbound.Del("X-Forwarded-For")
714 if got := railsRemoteIP(inbound, "127.0.0.1"); got != "8.8.8.8" {
715 t.Fatalf("control: Rails falls back to Client-IP as %q, want the claim", got)
716 }
717 if got, _ := ipwareClientIP(inbound, "127.0.0.1"); got != "8.8.8.8" {
718 t.Fatalf("control: ipware resolves the raw inbound headers to %q, want the claim", got)
719 }
720
721 // The plan's documentation address, and one shaped like a real EIP.
722 for _, addr := range []string{doorTestAddr, "34.248.10.10"} {
723 s := newDoorScene(t)
724 s.e.doorAddr = addr
725 wantServed(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, allAlternates("8.8.8.8"), nil), doorOwnBody, "door request")
726 _, hdr, peer := s.own.seen()
727 resolveBoth(t, "served locally as "+addr, hdr, peer, addr)
728 }
729}
730
731// 5. The door is not a bypass: a gated route answers with the gate's page.
732func TestDoorGatedRouteStillAnswersWithTheGatePage(t *testing.T) {
733 s := newDoorScene(t)
734 rec := s.do(t, http.MethodGet, doorGatedHost, "/", s.key, nil, nil)
735 if rec.Code != http.StatusUnauthorized || !strings.Contains(rec.Body.String(), "Password required") {
736 t.Fatalf("gated route through the door = %d %q, want the gate's 401 page", rec.Code, rec.Body.String())
737 }
738 if n := upstreamCalls(s.gated); n != 0 {
739 t.Fatalf("the gated upstream saw %d request(s)", n)
740 }
741}
742
743// 5a. The door is not a bypass of the import gate either (seeded-instances
744// spec §5.4): the caller's own route, swapped for an uncommitted import,
745// passes the door's scope check and then meets the gate. Without the verify
746// token the answer is the gate's 503, not unknown host; with it, a GET
747// reaches the import and the app never sees the header. Another app's key
748// is still refused at the scope check, before the gate says anything.
749func TestDoorMeetsTheImportGate(t *testing.T) {
750 s := newDoorScene(t)
751 importSrv, imported := newGateBackend(t, "imported", "")
752 token, hash, err := seeds.NewVerifyToken()
753 if err != nil {
754 t.Fatal(err)
755 }
756 gateForImport(t, s.e.reg, doorOwnHost, importSrv.Listener.Addr().String(), hash, false)
757 if err := s.e.Reload(); err != nil {
758 t.Fatal(err)
759 }
760
761 wantGateRefusal(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.key, nil, nil), token, "the door with no token")
762 wantGateRefusal(t, s.do(t, http.MethodPost, doorOwnHost, "/", s.key, map[string]string{ImportVerifyHeader: token}, nil),
763 token, "the door with the token on a POST")
764 if n, _ := imported.seen(); n != 0 || upstreamCalls(s.own) != 0 {
765 t.Fatalf("the door reached a gated row: import %d, legacy %d", n, upstreamCalls(s.own))
766 }
767 wantUnknownHost(t, s.do(t, http.MethodGet, doorOwnHost, "/", s.otherKey, map[string]string{ImportVerifyHeader: token}, nil),
768 "another app's key on a gated host")
769
770 rec := s.do(t, http.MethodGet, doorOwnHost, "/", s.key, map[string]string{ImportVerifyHeader: token}, nil)
771 if rec.Code != http.StatusOK || rec.Body.String() != "imported" {
772 t.Fatalf("verify through the door = %d %q, want the import", rec.Code, rec.Body.String())
773 }
774 if n, saw := imported.seen(); n != 1 || len(saw) != 0 {
775 t.Fatalf("verify through the door: import reached %d times, header at the app %q", n, saw)
776 }
777}
778
779// ---- 5b: a backhauled door request -----------------------------------------------
780
781// 5b. Two edges joined as the backhaul e2e joins them: the door request lands
782// on betty, which does not serve the host, and is backhauled to annie. The
783// far app sees the box address and https, and so do both resolvers, when
784// annie verifies the hop and again when it cannot. Unverified, annie uses the
785// TCP peer, which in production is the sending box's own public address;
786// loopback cannot carry that, so annie's front door reports it (the harness's
787// peerAs).
788func TestDoorBackhaulPresentsTheBoxAddress(t *testing.T) {
789 e2eQuietLogs(t)
790 bucket := t.TempDir()
791 seed := release.NewDirStore(bucket)
792 ca := newE2ECA(t)
793 if err := MintMeshKey(context.Background(), seed); err != nil {
794 t.Fatal(err)
795 }
796 for _, host := range []string{e2eHomedHost, e2eSenderHost} {
797 ca.publish(t, seed, host)
798 }
799 appSrv, app := newDoorUpstream(t, e2eHomeMarker)
800 senderSrv, _ := newDoorUpstream(t, e2eSenderMarker)
801 a := newBackhaulEdge(t, bucket, "annie", ca, nil, map[string]string{e2eHomedHost: appSrv.Listener.Addr().String()})
802 b := newBackhaulEdge(t, bucket, "betty", ca, nil, map[string]string{e2eSenderHost: senderSrv.Listener.Addr().String()})
803
804 aEIP, port, err := net.SplitHostPort(a.tlsAddr)
805 if err != nil {
806 t.Fatal(err)
807 }
808 bEIP, _, _ := net.SplitHostPort(b.tlsAddr)
809 oldPort, oldCAs := backhaulPort, backhaulRootCAs
810 backhaulPort, backhaulRootCAs = port, ca.pool
811 t.Cleanup(func() { backhaulPort, backhaulRootCAs = oldPort, oldCAs })
812 putJSON(t, seed, "control/edges/annie.json", fmt.Sprintf(
813 `{"v":1,"box":"annie","eip":%q,"hosts":{%q:{"role":"home","account":"bae","app":"titogo"}}}`, aEIP, e2eHomedHost))
814 putJSON(t, seed, "control/edges/betty.json", fmt.Sprintf(
815 `{"v":1,"box":"betty","eip":%q,"hosts":{%q:{"role":"home","account":"bae","app":"other"}}}`, bEIP, e2eSenderHost))
816 for _, be := range []*backhaulEdge{a, b} {
817 if err := be.mirror.Refresh(context.Background()); err != nil {
818 t.Fatal(err)
819 }
820 refreshHomeMap(t, be.homes)
821 }
822
823 fx := newDoorKeyFixture(t)
824 raw, entry := fx.mint(t, "bae", "titogo")
825 putJSON(t, seed, doorkey.RecordKey("bae", "titogo"), string(fx.record(t, "bae", "titogo", 1, entry)))
826 b.edge.doorKeys = fx.mirror(b.store)
827 refreshDoorKeys(t, b.edge.doorKeys)
828 b.edge.doorAddr = doorTestAddr
829
830 // farXFF sends one door request and returns the far app's chain.
831 farXFF := func() []string {
832 t.Helper()
833 req := httptest.NewRequest(http.MethodGet, "http://"+e2eHomedHost+"/", nil)
834 req.Host = e2eHomedHost
835 req.RemoteAddr = "@"
836 req.Header.Set(doorkey.Header, raw)
837 rec := httptest.NewRecorder()
838 b.edge.serveDoor(rec, req)
839 if rec.Code != http.StatusOK {
840 t.Fatalf("door request = %d %q", rec.Code, rec.Body.String())
841 }
842 _, hdr, _ := app.seen()
843 return hdr.Values("X-Forwarded-For")
844 }
845
846 door := func(what string) {
847 t.Helper()
848 before := upstreamCalls(app)
849 req := httptest.NewRequest(http.MethodGet, "http://"+e2eHomedHost+"/deep", nil)
850 req.Host = e2eHomedHost
851 req.RemoteAddr = "@"
852 req.Header.Set(doorkey.Header, raw)
853 for k, v := range allAlternates("8.8.8.8") {
854 req.Header.Set(k, v)
855 }
856 rec := httptest.NewRecorder()
857 b.edge.serveDoor(rec, req)
858 if rec.Code != http.StatusOK || rec.Body.String() != e2eHomeMarker {
859 t.Fatalf("%s: %d %q, want the home edge's app", what, rec.Code, rec.Body.String())
860 }
861 n, hdr, peer := app.seen()
862 if n != before+1 {
863 t.Fatalf("%s: the far app saw %d new request(s), want one", what, n-before)
864 }
865 if got := hdr.Values("X-Forwarded-For"); len(got) != 1 || got[0] != doorTestAddr {
866 t.Fatalf("%s: X-Forwarded-For = %q, want exactly %s", what, got, doorTestAddr)
867 }
868 if got := hdr.Values("X-Forwarded-Proto"); len(got) != 1 || got[0] != "https" {
869 t.Fatalf("%s: X-Forwarded-Proto = %q, want https", what, got)
870 }
871 for _, n := range append([]string{"Forwarded", doorkey.Header, hopHeader, hopAuthHeader}, doorAddressHeaders...) {
872 if v := hdr.Values(n); len(v) != 0 {
873 t.Fatalf("%s: %s = %q reached the far app", what, n, v)
874 }
875 }
876 resolveBoth(t, what, hdr, peer, doorTestAddr)
877 }
878
879 // Verified: annie believes betty's chain, not the peer. A peer that is
880 // neither address proves which one the app was given.
881 const elsewhere = "192.0.2.77"
882 a.peerAs.Store(elsewhere)
883 door("verified hop")
884
885 // annie can no longer verify betty's hops: its mirrored key is not the
886 // one betty signs with. The chain is dropped and the peer is used, which
887 // the elsewhere peer shows; with the peer at the box's own address, as
888 // in production, the app sees the same as above.
889 a.homes.keyMu.Lock()
890 a.homes.keyCurrent, a.homes.keyNext = bytes.Repeat([]byte{7}, 32), nil
891 a.homes.keyMu.Unlock()
892 if got := farXFF(); len(got) != 1 || got[0] != elsewhere {
893 t.Fatalf("unverifiable hop: X-Forwarded-For = %q, want the peer %s (the chain must not be believed)", got, elsewhere)
894 }
895 a.peerAs.Store(doorTestAddr)
896 door("unverifiable hop")
897}
898
899// ---- Task 6: the listener ------------------------------------------------------
900
901// shortSockDir is a socket directory short enough for sun_path.
902func shortSockDir(t *testing.T) string {
903 t.Helper()
904 dir, err := os.MkdirTemp("", "door")
905 if err != nil {
906 t.Fatal(err)
907 }
908 t.Cleanup(func() { os.RemoveAll(dir) })
909 return dir
910}
911
912func unixClient(path string) *http.Client {
913 return &http.Client{Timeout: 10 * time.Second, Transport: &http.Transport{
914 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
915 var d net.Dialer
916 return d.DialContext(ctx, "unix", path)
917 },
918 }}
919}
920
921func doorGet(t *testing.T, path, host, key string) (int, string) {
922 t.Helper()
923 req, _ := http.NewRequest(http.MethodGet, "http://door/", nil)
924 req.Host = host
925 if key != "" {
926 req.Header.Set(doorkey.Header, key)
927 }
928 resp, err := unixClient(path).Do(req)
929 if err != nil {
930 t.Fatalf("door request: %v", err)
931 }
932 defer resp.Body.Close()
933 return resp.StatusCode, readBody(t, resp)
934}
935
936// 6. openDoor makes a 0660 socket that serves the door, closeDoor removes
937// it, and a door with no mirror or no public address opens nothing.
938func TestOpenDoorServesOnA0660Socket(t *testing.T) {
939 s := newDoorScene(t)
940 path := filepath.Join(shortSockDir(t), "edge-local.sock")
941 // A stale socket from a previous run is replaced.
942 stale, err := net.Listen("unix", path)
943 if err != nil {
944 t.Fatal(err)
945 }
946 stale.(*net.UnixListener).SetUnlinkOnClose(false)
947 stale.Close()
948
949 closeDoor, err := s.e.openDoor(path)
950 if err != nil {
951 t.Fatalf("openDoor: %v", err)
952 }
953 fi, err := os.Stat(path)
954 if err != nil {
955 t.Fatal(err)
956 }
957 if fi.Mode()&os.ModeSocket == 0 || fi.Mode().Perm() != 0o660 {
958 t.Fatalf("socket mode = %v, want a 0660 socket", fi.Mode())
959 }
960 if code, body := doorGet(t, path, doorOwnHost, s.key); code != http.StatusOK || body != doorOwnBody {
961 t.Fatalf("over the socket = %d %q", code, body)
962 }
963 if code, _ := doorGet(t, path, doorOwnHost, ""); code != http.StatusUnauthorized {
964 t.Fatalf("no key over the socket = %d, want 401", code)
965 }
966 closeDoor()
967 if _, err := os.Stat(path); !os.IsNotExist(err) {
968 t.Fatalf("socket still there after closeDoor: %v", err)
969 }
970 closeDoor() // idempotent
971
972 for _, tc := range []struct {
973 what string
974 arm func(e *Edge)
975 }{
976 {"nil mirror", func(e *Edge) { e.doorKeys = nil }},
977 {"no address", func(e *Edge) { e.doorAddr = "" }},
978 {"loopback", func(e *Edge) { e.doorAddr = "127.0.0.1" }},
979 {"private", func(e *Edge) { e.doorAddr = "10.0.0.5" }},
980 {"not an address", func(e *Edge) { e.doorAddr = "unknown" }},
981 } {
982 s := newDoorScene(t)
983 tc.arm(s.e)
984 p := filepath.Join(shortSockDir(t), "d.sock")
985 if c, err := s.e.openDoor(p); err == nil {
986 c()
987 t.Fatalf("%s: openDoor opened a door", tc.what)
988 }
989 if _, err := os.Stat(p); !os.IsNotExist(err) {
990 t.Fatalf("%s: openDoor left something at the path: %v", tc.what, err)
991 }
992 }
993
994 // A path that is not a socket is not removed.
995 p := filepath.Join(shortSockDir(t), "file")
996 if err := os.WriteFile(p, []byte("keep"), 0o600); err != nil {
997 t.Fatal(err)
998 }
999 if c, err := newDoorScene(t).e.openDoor(p); err == nil {
1000 c()
1001 t.Fatal("openDoor replaced a regular file")
1002 }
1003 if b, err := os.ReadFile(p); err != nil || string(b) != "keep" {
1004 t.Fatalf("the regular file was touched: %q %v", b, err)
1005 }
1006}
1007
1008// ---- Task 7: lifecycle ----------------------------------------------------------
1009
1010func TestWithEdgeDoorRecordsPathAndAddress(t *testing.T) {
1011 e := &Edge{}
1012 WithEdgeDoor("/run/carlos/edge-local.sock", doorTestAddr)(e)
1013 if e.doorPath != "/run/carlos/edge-local.sock" || e.doorAddr != doorTestAddr {
1014 t.Fatalf("doorPath %q doorAddr %q", e.doorPath, e.doorAddr)
1015 }
1016}
1017
1018// armDoor over a DirStore: before any record exists a key is refused (and
1019// nothing panics); once a signed record lands and the mirror polls, the
1020// same key is served over the real socket; closeDoor removes the socket.
1021func TestArmDoorServesOverTheSocketAndClosesIt(t *testing.T) {
1022 buf := captureLog(t)
1023 fx := newDoorKeyFixture(t)
1024 oldInterval := doorKeyInterval
1025 doorKeyInterval = 20 * time.Millisecond
1026 t.Cleanup(func() { doorKeyInterval = oldInterval })
1027
1028 backend, _ := newDoorUpstream(t, doorOwnBody)
1029 reg := openReg(t)
1030 if err := reg.Upsert(registry.Route{Host: doorOwnHost, Kind: "service", Status: "ready",
1031 Addr: backend.Listener.Addr().String(), Account: "bae", App: "titogo"}); err != nil {
1032 t.Fatal(err)
1033 }
1034 e, err := New(reg, nil)
1035 if err != nil {
1036 t.Fatal(err)
1037 }
1038 t.Cleanup(e.probers.Stop)
1039 store := release.NewDirStore(t.TempDir())
1040 e.staticStore, e.staticPub = store, fx.pub
1041 path := filepath.Join(shortSockDir(t), "edge-local.sock")
1042 WithEdgeDoor(path, doorTestAddr)(e)
1043
1044 if err := e.armDoor(context.Background()); err != nil {
1045 t.Fatalf("armDoor: %v", err)
1046 }
1047 t.Cleanup(e.closeDoor)
1048 raw, entry := fx.mint(t, "bae", "titogo")
1049 if code, body := doorGet(t, path, doorOwnHost, raw); code != http.StatusUnauthorized {
1050 t.Fatalf("before any record = %d %q, want 401", code, body)
1051 }
1052
1053 putJSON(t, store, doorkey.RecordKey("bae", "titogo"), string(fx.record(t, "bae", "titogo", 1, entry)))
1054 deadline := time.Now().Add(5 * time.Second)
1055 for {
1056 code, body := doorGet(t, path, doorOwnHost, raw)
1057 if code == http.StatusOK && body == doorOwnBody {
1058 break
1059 }
1060 if time.Now().After(deadline) {
1061 t.Fatalf("the mirror's loop never admitted the key: %d %q", code, body)
1062 }
1063 time.Sleep(20 * time.Millisecond)
1064 }
1065 if !strings.Contains(buf.String(), "edge-local door open on "+path) {
1066 t.Fatalf("no open line in the log:\n%s", buf.String())
1067 }
1068
1069 e.closeDoor()
1070 if _, err := os.Stat(path); !os.IsNotExist(err) {
1071 t.Fatalf("socket still there after closeDoor: %v", err)
1072 }
1073}
1074
1075// With no store, no public key, or no usable address, armDoor opens nothing
1076// and says why.
1077func TestArmDoorStaysClosedAndSaysWhy(t *testing.T) {
1078 fx := newDoorKeyFixture(t)
1079 for _, tc := range []struct {
1080 what, addr, want string
1081 store bool
1082 pub bool
1083 }{
1084 {"no store", doorTestAddr, "no deployment store", false, true},
1085 {"no public key", doorTestAddr, "no release public key", true, false},
1086 {"no address", "", "no usable public address", true, true},
1087 {"private address", "10.1.2.3", "no usable public address", true, true},
1088 } {
1089 buf := captureLog(t)
1090 e := &Edge{}
1091 if tc.store {
1092 e.staticStore = release.NewDirStore(t.TempDir())
1093 }
1094 if tc.pub {
1095 e.staticPub = fx.pub
1096 }
1097 path := filepath.Join(shortSockDir(t), "d.sock")
1098 WithEdgeDoor(path, tc.addr)(e)
1099 if err := e.armDoor(context.Background()); err == nil {
1100 t.Fatalf("%s: armDoor succeeded", tc.what)
1101 }
1102 if e.closeDoor != nil || e.doorKeys != nil {
1103 t.Fatalf("%s: armDoor left a door or a mirror behind", tc.what)
1104 }
1105 if _, err := os.Stat(path); !os.IsNotExist(err) {
1106 t.Fatalf("%s: something exists at the socket path: %v", tc.what, err)
1107 }
1108 if got := buf.String(); !strings.Contains(got, "edge-local door off") || !strings.Contains(got, tc.want) {
1109 t.Fatalf("%s: log = %q, want the door off and %q", tc.what, got, tc.want)
1110 }
1111 }
1112
1113 // No door path: nothing at all, not even a line.
1114 buf := captureLog(t)
1115 e := &Edge{}
1116 if err := e.armDoor(context.Background()); err != nil {
1117 t.Fatalf("armDoor with no path: %v", err)
1118 }
1119 if buf.Len() != 0 {
1120 t.Fatalf("armDoor with no path logged %q", buf.String())
1121 }
1122}
1123