| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/hex" |
| 6 | "net/http" |
| 7 | "net/http/httptest" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/edgeproof" |
| 13 | ) |
| 14 | |
| 15 | // TestClientIPLoopbackNeedsTheEdgeProof: a process on the console's box can |
| 16 | // reach 127.0.0.1:8090 as easily as the edge can, so once the edge signs its |
| 17 | // X-Forwarded-For, a loopback chain without that signature is a string the |
| 18 | // caller typed and counts against the peer (discussion 214, item 5). Before |
| 19 | // the first signed request the old trust stands, so a console rolled ahead |
| 20 | // of its edge does not put every client in one rate-limit bucket; after it, |
| 21 | // the latch holds across a restart. |
| 22 | func TestClientIPLoopbackNeedsTheEdgeProof(t *testing.T) { |
| 23 | fx := newConsoleFixture(t) |
| 24 | key := []byte("0123456789abcdef0123456789abcdef") |
| 25 | if _, err := fx.rel.Put(context.Background(), edgeproof.MeshKeyObject, []byte(hex.EncodeToString(key)), ""); err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | req := func(xff, proof string) *http.Request { |
| 29 | r := httptest.NewRequest(http.MethodPost, "/api/cli/auth/start", nil) |
| 30 | r.RemoteAddr = "127.0.0.1:50000" |
| 31 | r.Header.Set("X-Forwarded-For", xff) |
| 32 | if proof != "" { |
| 33 | r.Header.Set(edgeproof.Header, proof) |
| 34 | } |
| 35 | return r |
| 36 | } |
| 37 | |
| 38 | // Before any proof: an unsigned chain is still believed. |
| 39 | if got := fx.srv.clientIP(req("9.9.9.9", "")); got != "9.9.9.9" { |
| 40 | t.Fatalf("unsigned chain before any proof = %q, want the old trust (9.9.9.9)", got) |
| 41 | } |
| 42 | // A forged proof is not a proof, and does not close the latch. |
| 43 | if got := fx.srv.clientIP(req("9.9.9.9", edgeproof.Sign([]byte("not the mesh key"), "example.com", "9.9.9.9", time.Now()))); got != "9.9.9.9" { |
| 44 | t.Fatalf("forged proof before any real one = %q, want the old trust", got) |
| 45 | } |
| 46 | if seen, _ := fx.st.EdgeProofSeen(); seen { |
| 47 | t.Fatal("a forged proof closed the latch") |
| 48 | } |
| 49 | |
| 50 | // The edge's signed chain is believed, and closes the latch. |
| 51 | if got := fx.srv.clientIP(req("1.2.3.4", edgeproof.Sign(key, "example.com", "1.2.3.4", time.Now()))); got != "1.2.3.4" { |
| 52 | t.Fatalf("signed chain = %q, want 1.2.3.4", got) |
| 53 | } |
| 54 | // From now on: no proof, a forged proof, a proof for another chain, or a |
| 55 | // stale one all count against the peer. |
| 56 | for what, r := range map[string]*http.Request{ |
| 57 | "no proof": req("5.6.7.8", ""), |
| 58 | "forged": req("5.6.7.8", edgeproof.Sign([]byte("not the mesh key"), "example.com", "5.6.7.8", time.Now())), |
| 59 | "other chain": req("5.6.7.8", edgeproof.Sign(key, "example.com", "1.2.3.4", time.Now())), |
| 60 | "stale": req("5.6.7.8", edgeproof.Sign(key, "example.com", "5.6.7.8", time.Now().Add(-2*edgeproof.Skew))), |
| 61 | "extra xff line": func() *http.Request { |
| 62 | r := req("1.2.3.4", edgeproof.Sign(key, "example.com", "1.2.3.4", time.Now())) |
| 63 | r.Header.Add("X-Forwarded-For", "5.6.7.8") |
| 64 | return r |
| 65 | }(), |
| 66 | } { |
| 67 | if got := fx.srv.clientIP(r); got != "127.0.0.1" { |
| 68 | t.Errorf("%s after the latch = %q, want the peer 127.0.0.1", what, got) |
| 69 | } |
| 70 | } |
| 71 | // A proof is bound to the host it was minted for: a tenant app on this |
| 72 | // box sees valid proofs on its own traffic, and must not be able to |
| 73 | // replay one here as the visitor it names. |
| 74 | if got := fx.srv.clientIP(req("5.6.7.8", edgeproof.Sign(key, "tenant.example.net", "5.6.7.8", time.Now()))); got != "127.0.0.1" { |
| 75 | t.Errorf("proof minted for another host = %q, want the peer 127.0.0.1", got) |
| 76 | } |
| 77 | // The host's port and case are not part of what is bound. |
| 78 | spelled := req("5.6.7.8", edgeproof.Sign(key, "Example.COM", "5.6.7.8", time.Now())) |
| 79 | spelled.Host = "example.com:443" |
| 80 | if got := fx.srv.clientIP(spelled); got != "5.6.7.8" { |
| 81 | t.Errorf("proof with the host in another spelling = %q, want 5.6.7.8", got) |
| 82 | } |
| 83 | // A signed multi-element chain still yields its last element. |
| 84 | if got := fx.srv.clientIP(req("9.9.9.9, 1.2.3.4", edgeproof.Sign(key, "example.com", "9.9.9.9, 1.2.3.4", time.Now()))); got != "1.2.3.4" { |
| 85 | t.Fatalf("signed chain of two = %q, want 1.2.3.4", got) |
| 86 | } |
| 87 | |
| 88 | // The latch is stored: a fresh process on the same database starts closed. |
| 89 | again := &Server{cfg: fx.srv.cfg, store: fx.st} |
| 90 | if got := again.clientIP(req("5.6.7.8", "")); got != "127.0.0.1" { |
| 91 | t.Fatalf("unsigned chain after a restart = %q, want the peer", got) |
| 92 | } |
| 93 | // And a configured edge address is still believed without a proof: it is |
| 94 | // not a peer a tenant on this box can be. |
| 95 | r := req("5.6.7.8", "") |
| 96 | r.RemoteAddr = "198.51.100.9:9" |
| 97 | edged := &Server{cfg: Config{EdgeIPs: []string{"198.51.100.9"}, Releases: fx.rel}, store: fx.st} |
| 98 | if got := edged.clientIP(r); got != "5.6.7.8" { |
| 99 | t.Fatalf("configured edge peer = %q, want 5.6.7.8", got) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | // TestEdgeProofSeenMark: the stored latch is one row and keeps its first |
| 104 | // moment. |
| 105 | func TestEdgeProofSeenMark(t *testing.T) { |
| 106 | st, err := Open(filepath.Join(t.TempDir(), "console.db")) |
| 107 | if err != nil { |
| 108 | t.Fatal(err) |
| 109 | } |
| 110 | defer st.Close() |
| 111 | if seen, err := st.EdgeProofSeen(); err != nil || seen { |
| 112 | t.Fatalf("fresh store seen = %v, %v; want false, nil", seen, err) |
| 113 | } |
| 114 | for i := 0; i < 2; i++ { |
| 115 | if err := st.MarkEdgeProofSeen(time.Now()); err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | } |
| 119 | if seen, err := st.EdgeProofSeen(); err != nil || !seen { |
| 120 | t.Fatalf("marked store seen = %v, %v; want true, nil", seen, err) |
| 121 | } |
| 122 | } |
| 123 | |