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
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "strings"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/edgeproof"
13 "github.com/carlosframework/platform/internal/registry"
14)
15
16// proofBackend records the forwarded chain and proof of the last request.
17type proofBackend struct {
18 mu sync.Mutex
19 xff, proof string
20}
21
22func (b *proofBackend) get() (string, string) {
23 b.mu.Lock()
24 defer b.mu.Unlock()
25 return b.xff, b.proof
26}
27
28// proofEdge serves app.test from a recording backend. With armed, the edge
29// has a home map carrying a minted mesh key; without, it has no store at all.
30func proofEdge(t *testing.T, armed bool) (*Edge, *proofBackend, []byte) {
31 t.Helper()
32 b := &proofBackend{}
33 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34 b.mu.Lock()
35 b.xff = strings.Join(r.Header.Values("X-Forwarded-For"), ", ")
36 b.proof = r.Header.Get(edgeproof.Header)
37 b.mu.Unlock()
38 }))
39 t.Cleanup(ts.Close)
40 reg := openReg(t)
41 if err := reg.Upsert(registry.Route{Host: "app.test", Kind: "service", Addr: ts.Listener.Addr().String(), Status: "ready"}); err != nil {
42 t.Fatal(err)
43 }
44 e, err := New(reg, nil)
45 if err != nil {
46 t.Fatal(err)
47 }
48 t.Cleanup(e.probers.Stop)
49 if !armed {
50 return e, b, nil
51 }
52 s := meshTestStore(t)
53 if err := mintMeshKey(context.Background(), s); err != nil {
54 t.Fatal(err)
55 }
56 m := newHomeMap(s, "annie")
57 refreshHomeMap(t, m)
58 e.homes.Store(m)
59 key, _ := m.meshKeys()
60 return e, b, key
61}
62
63// TestEdgeSignsTheForwardedChain: what the edge proxies carries its signature
64// over the X-Forwarded-For it sends, so the console on this box can tell the
65// edge from a local process writing the same header (discussion 214, item
66// 5). A proof the client sent is replaced, never passed on.
67func TestEdgeSignsTheForwardedChain(t *testing.T) {
68 e, b, key := proofEdge(t, true)
69 forged := edgeproof.Sign(key, "app.test", "6.6.6.6", time.Now())
70 resp := hopRequest(t, e, "app.test", map[string]string{
71 "X-Forwarded-For": "6.6.6.6",
72 edgeproof.Header: forged,
73 })
74 if resp.StatusCode != http.StatusOK {
75 t.Fatalf("status = %d, want 200", resp.StatusCode)
76 }
77 xff, proof := b.get()
78 if strings.Contains(xff, "6.6.6.6") {
79 t.Fatalf("X-Forwarded-For = %q: an ordinary client's chain reached the app", xff)
80 }
81 if proof == "" || proof == forged {
82 t.Fatalf("proof = %q, want the edge's own signature (client sent %q)", proof, forged)
83 }
84 if !edgeproof.Verify(key, nil, "app.test", xff, proof, time.Now()) {
85 t.Fatalf("proof %q does not verify over the chain sent (%q)", proof, xff)
86 }
87
88 // A verified hop's chain is the one this edge sends on, and it is signed
89 // as sent.
90 resp = hopRequest(t, e, "app.test", map[string]string{
91 hopHeader: "1",
92 hopAuthHeader: signHop(key, "app.test", time.Now()),
93 "X-Forwarded-For": "203.0.113.9",
94 })
95 if resp.StatusCode != http.StatusOK {
96 t.Fatalf("hop status = %d, want 200", resp.StatusCode)
97 }
98 xff, proof = b.get()
99 if xff != "203.0.113.9" || !edgeproof.Verify(key, nil, "app.test", xff, proof, time.Now()) {
100 t.Fatalf("hop forwarded xff=%q proof=%q, want the hop's chain, signed", xff, proof)
101 }
102}
103
104// TestEdgeWithoutAKeyStripsTheProof: no mesh key, no signature — and a proof
105// header the client sent still never reaches the app.
106func TestEdgeWithoutAKeyStripsTheProof(t *testing.T) {
107 e, b, _ := proofEdge(t, false)
108 resp := hopRequest(t, e, "app.test", map[string]string{
109 edgeproof.Header: edgeproof.Sign([]byte("k"), "app.test", "1.2.3.4", time.Now()),
110 })
111 if resp.StatusCode != http.StatusOK {
112 t.Fatalf("status = %d, want 200", resp.StatusCode)
113 }
114 if _, proof := b.get(); proof != "" {
115 t.Fatalf("proof = %q, want none from an edge with no key", proof)
116 }
117}
118
119// The proof MAC must never double as a hop MAC: both are keyed by the mesh
120// key, and a tenant app sees the proof on its own traffic.
121func TestForwardProofIsNotAHopAuth(t *testing.T) {
122 key := []byte("0123456789abcdef0123456789abcdef")
123 now := time.Now()
124 if verifyHop(key, "app.test", edgeproof.Sign(key, "app.test", "", now), now) {
125 t.Fatal("a forwarded-for proof verified as a hop signature")
126 }
127 if edgeproof.Verify(key, nil, "app.test", "", signHop(key, "app.test", now), now) {
128 t.Fatal("a hop signature verified as a forwarded-for proof")
129 }
130}
131
132// TestBackhaulCarriesNoForwardProof: the proof is for the upstream an edge
133// dials, so a backhauled request leaves without one, whatever the client
134// sent. An older home edge would otherwise pass the far edge's proof to the
135// console untouched, and the console would start requiring proofs its own
136// edge does not yet send.
137func TestBackhaulCarriesNoForwardProof(t *testing.T) {
138 e, m, key := hopEdge(t, "annie", "local.test")
139 rec, eip := fakeHomeEdge(t, nil)
140 homedOn(t, m, "betty", eip, "example.com")
141 resp := hopRequest(t, e, "example.com", map[string]string{
142 edgeproof.Header: edgeproof.Sign(key, "example.com", "1.2.3.4", time.Now()),
143 })
144 if resp.StatusCode != http.StatusOK {
145 t.Fatalf("status = %d, want 200", resp.StatusCode)
146 }
147 if got := rec.get(); got.n != 1 || got.proof != "" {
148 t.Fatalf("home edge saw %d request(s) with proof %q, want one with none", got.n, got.proof)
149 }
150}
151