package edge import ( "context" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/edgeproof" "github.com/carlosframework/platform/internal/registry" ) // proofBackend records the forwarded chain and proof of the last request. type proofBackend struct { mu sync.Mutex xff, proof string } func (b *proofBackend) get() (string, string) { b.mu.Lock() defer b.mu.Unlock() return b.xff, b.proof } // proofEdge serves app.test from a recording backend. With armed, the edge // has a home map carrying a minted mesh key; without, it has no store at all. func proofEdge(t *testing.T, armed bool) (*Edge, *proofBackend, []byte) { t.Helper() b := &proofBackend{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b.mu.Lock() b.xff = strings.Join(r.Header.Values("X-Forwarded-For"), ", ") b.proof = r.Header.Get(edgeproof.Header) b.mu.Unlock() })) t.Cleanup(ts.Close) reg := openReg(t) if err := reg.Upsert(registry.Route{Host: "app.test", Kind: "service", Addr: ts.Listener.Addr().String(), Status: "ready"}); err != nil { t.Fatal(err) } e, err := New(reg, nil) if err != nil { t.Fatal(err) } t.Cleanup(e.probers.Stop) if !armed { return e, b, nil } s := meshTestStore(t) if err := mintMeshKey(context.Background(), s); err != nil { t.Fatal(err) } m := newHomeMap(s, "annie") refreshHomeMap(t, m) e.homes.Store(m) key, _ := m.meshKeys() return e, b, key } // TestEdgeSignsTheForwardedChain: what the edge proxies carries its signature // over the X-Forwarded-For it sends, so the console on this box can tell the // edge from a local process writing the same header (discussion 214, item // 5). A proof the client sent is replaced, never passed on. func TestEdgeSignsTheForwardedChain(t *testing.T) { e, b, key := proofEdge(t, true) forged := edgeproof.Sign(key, "app.test", "6.6.6.6", time.Now()) resp := hopRequest(t, e, "app.test", map[string]string{ "X-Forwarded-For": "6.6.6.6", edgeproof.Header: forged, }) if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } xff, proof := b.get() if strings.Contains(xff, "6.6.6.6") { t.Fatalf("X-Forwarded-For = %q: an ordinary client's chain reached the app", xff) } if proof == "" || proof == forged { t.Fatalf("proof = %q, want the edge's own signature (client sent %q)", proof, forged) } if !edgeproof.Verify(key, nil, "app.test", xff, proof, time.Now()) { t.Fatalf("proof %q does not verify over the chain sent (%q)", proof, xff) } // A verified hop's chain is the one this edge sends on, and it is signed // as sent. resp = hopRequest(t, e, "app.test", map[string]string{ hopHeader: "1", hopAuthHeader: signHop(key, "app.test", time.Now()), "X-Forwarded-For": "203.0.113.9", }) if resp.StatusCode != http.StatusOK { t.Fatalf("hop status = %d, want 200", resp.StatusCode) } xff, proof = b.get() if xff != "203.0.113.9" || !edgeproof.Verify(key, nil, "app.test", xff, proof, time.Now()) { t.Fatalf("hop forwarded xff=%q proof=%q, want the hop's chain, signed", xff, proof) } } // TestEdgeWithoutAKeyStripsTheProof: no mesh key, no signature — and a proof // header the client sent still never reaches the app. func TestEdgeWithoutAKeyStripsTheProof(t *testing.T) { e, b, _ := proofEdge(t, false) resp := hopRequest(t, e, "app.test", map[string]string{ edgeproof.Header: edgeproof.Sign([]byte("k"), "app.test", "1.2.3.4", time.Now()), }) if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } if _, proof := b.get(); proof != "" { t.Fatalf("proof = %q, want none from an edge with no key", proof) } } // The proof MAC must never double as a hop MAC: both are keyed by the mesh // key, and a tenant app sees the proof on its own traffic. func TestForwardProofIsNotAHopAuth(t *testing.T) { key := []byte("0123456789abcdef0123456789abcdef") now := time.Now() if verifyHop(key, "app.test", edgeproof.Sign(key, "app.test", "", now), now) { t.Fatal("a forwarded-for proof verified as a hop signature") } if edgeproof.Verify(key, nil, "app.test", "", signHop(key, "app.test", now), now) { t.Fatal("a hop signature verified as a forwarded-for proof") } } // TestBackhaulCarriesNoForwardProof: the proof is for the upstream an edge // dials, so a backhauled request leaves without one, whatever the client // sent. An older home edge would otherwise pass the far edge's proof to the // console untouched, and the console would start requiring proofs its own // edge does not yet send. func TestBackhaulCarriesNoForwardProof(t *testing.T) { e, m, key := hopEdge(t, "annie", "local.test") rec, eip := fakeHomeEdge(t, nil) homedOn(t, m, "betty", eip, "example.com") resp := hopRequest(t, e, "example.com", map[string]string{ edgeproof.Header: edgeproof.Sign(key, "example.com", "1.2.3.4", time.Now()), }) if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } if got := rec.get(); got.n != 1 || got.proof != "" { t.Fatalf("home edge saw %d request(s) with proof %q, want one with none", got.n, got.proof) } }