package console import ( "context" "encoding/hex" "net/http" "net/http/httptest" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/edgeproof" ) // TestClientIPLoopbackNeedsTheEdgeProof: a process on the console's box can // reach 127.0.0.1:8090 as easily as the edge can, so once the edge signs its // X-Forwarded-For, a loopback chain without that signature is a string the // caller typed and counts against the peer (discussion 214, item 5). Before // the first signed request the old trust stands, so a console rolled ahead // of its edge does not put every client in one rate-limit bucket; after it, // the latch holds across a restart. func TestClientIPLoopbackNeedsTheEdgeProof(t *testing.T) { fx := newConsoleFixture(t) key := []byte("0123456789abcdef0123456789abcdef") if _, err := fx.rel.Put(context.Background(), edgeproof.MeshKeyObject, []byte(hex.EncodeToString(key)), ""); err != nil { t.Fatal(err) } req := func(xff, proof string) *http.Request { r := httptest.NewRequest(http.MethodPost, "/api/cli/auth/start", nil) r.RemoteAddr = "127.0.0.1:50000" r.Header.Set("X-Forwarded-For", xff) if proof != "" { r.Header.Set(edgeproof.Header, proof) } return r } // Before any proof: an unsigned chain is still believed. if got := fx.srv.clientIP(req("9.9.9.9", "")); got != "9.9.9.9" { t.Fatalf("unsigned chain before any proof = %q, want the old trust (9.9.9.9)", got) } // A forged proof is not a proof, and does not close the latch. 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" { t.Fatalf("forged proof before any real one = %q, want the old trust", got) } if seen, _ := fx.st.EdgeProofSeen(); seen { t.Fatal("a forged proof closed the latch") } // The edge's signed chain is believed, and closes the latch. 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" { t.Fatalf("signed chain = %q, want 1.2.3.4", got) } // From now on: no proof, a forged proof, a proof for another chain, or a // stale one all count against the peer. for what, r := range map[string]*http.Request{ "no proof": req("5.6.7.8", ""), "forged": req("5.6.7.8", edgeproof.Sign([]byte("not the mesh key"), "example.com", "5.6.7.8", time.Now())), "other chain": req("5.6.7.8", edgeproof.Sign(key, "example.com", "1.2.3.4", time.Now())), "stale": req("5.6.7.8", edgeproof.Sign(key, "example.com", "5.6.7.8", time.Now().Add(-2*edgeproof.Skew))), "extra xff line": func() *http.Request { r := req("1.2.3.4", edgeproof.Sign(key, "example.com", "1.2.3.4", time.Now())) r.Header.Add("X-Forwarded-For", "5.6.7.8") return r }(), } { if got := fx.srv.clientIP(r); got != "127.0.0.1" { t.Errorf("%s after the latch = %q, want the peer 127.0.0.1", what, got) } } // A proof is bound to the host it was minted for: a tenant app on this // box sees valid proofs on its own traffic, and must not be able to // replay one here as the visitor it names. 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" { t.Errorf("proof minted for another host = %q, want the peer 127.0.0.1", got) } // The host's port and case are not part of what is bound. spelled := req("5.6.7.8", edgeproof.Sign(key, "Example.COM", "5.6.7.8", time.Now())) spelled.Host = "example.com:443" if got := fx.srv.clientIP(spelled); got != "5.6.7.8" { t.Errorf("proof with the host in another spelling = %q, want 5.6.7.8", got) } // A signed multi-element chain still yields its last element. 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" { t.Fatalf("signed chain of two = %q, want 1.2.3.4", got) } // The latch is stored: a fresh process on the same database starts closed. again := &Server{cfg: fx.srv.cfg, store: fx.st} if got := again.clientIP(req("5.6.7.8", "")); got != "127.0.0.1" { t.Fatalf("unsigned chain after a restart = %q, want the peer", got) } // And a configured edge address is still believed without a proof: it is // not a peer a tenant on this box can be. r := req("5.6.7.8", "") r.RemoteAddr = "198.51.100.9:9" edged := &Server{cfg: Config{EdgeIPs: []string{"198.51.100.9"}, Releases: fx.rel}, store: fx.st} if got := edged.clientIP(r); got != "5.6.7.8" { t.Fatalf("configured edge peer = %q, want 5.6.7.8", got) } } // TestEdgeProofSeenMark: the stored latch is one row and keeps its first // moment. func TestEdgeProofSeenMark(t *testing.T) { st, err := Open(filepath.Join(t.TempDir(), "console.db")) if err != nil { t.Fatal(err) } defer st.Close() if seen, err := st.EdgeProofSeen(); err != nil || seen { t.Fatalf("fresh store seen = %v, %v; want false, nil", seen, err) } for i := 0; i < 2; i++ { if err := st.MarkEdgeProofSeen(time.Now()); err != nil { t.Fatal(err) } } if seen, err := st.EdgeProofSeen(); err != nil || !seen { t.Fatalf("marked store seen = %v, %v; want true, nil", seen, err) } }