package main import ( "context" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func write(t *testing.T, body string) string { t.Helper() path := filepath.Join(t.TempDir(), "routes.json") if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } return path } // plain is the table an ordinary set of routes makes: one host, one target, no policy. func plain(routes map[string]string) map[string][]rule { out := map[string][]rule{} for host, target := range routes { out[host] = []rule{{target: target}} } return out } // allPublic is every host in a routes map, ACME-eligible — the ordinary case for a test with no // internal-name alias of its own to distinguish. func allPublic(routes map[string][]rule) map[string]bool { out := map[string]bool{} for host := range routes { out[host] = true } return out } // targetOf is where a host's first matching rule sends a request. func targetOf(routes map[string][]rule, host string) string { if rules := routes[host]; len(rules) > 0 { return rules[0].target } return "" } // A route is a grant: the consumer supplies a target, and where that machine is comes from the // mesh rather than from a naming convention the proxy has to know. func TestARouteGoesToWhereTheMeshSaysTheConsumerIs(t *testing.T) { routes, _, err := routesFrom(write(t, `{"contributions":1,"requirement":"route","given":[ {"from":"app","node":"laptop","at":"laptop.internal","values":{"name":"App.Example","port":8080}} ]}`)) if err != nil { t.Fatal(err) } // Lower-cased, because a Host header is not case-sensitive and a route that only answers the // spelling in the manifest answers half the requests made to it. if targetOf(routes, "app.example") != "http://laptop.internal:8080" { t.Fatalf("the route does not point at the consumer: %v", routes) } } // A route with an internal-name alias is reachable under both hostnames, pointed at the same // target — the same convenience a predecessor proxy gave for reaching a service over the VPN // without a public TLS round trip. func TestARouteWithAnInternalNameIsReachableUnderBoth(t *testing.T) { routes, public, err := routesFrom(write(t, `{"given":[ {"from":"app","node":"anchor","at":"anchor.internal", "values":{"name":"app.example","internal-name":"app.anchor.internal","port":8080}} ]}`)) if err != nil { t.Fatal(err) } if targetOf(routes, "app.example") != "http://anchor.internal:8080" { t.Fatalf("the public name does not point at the consumer: %v", routes) } if targetOf(routes, "app.anchor.internal") != "http://anchor.internal:8080" { t.Fatalf("the internal alias does not point at the same consumer: %v", routes) } if !public["app.example"] { t.Errorf("the public name is not eligible for a certificate: %v", public) } if public["app.anchor.internal"] { t.Errorf("the internal alias is eligible for a certificate no public CA could ever issue: %v", public) } } // A route with no internal-name composed gets no second host — the ordinary case, unchanged. func TestARouteWithNoInternalNameGetsNoAlias(t *testing.T) { routes, _, err := routesFrom(write(t, `{"given":[ {"from":"app","node":"anchor","values":{"name":"app.example","port":8080}} ]}`)) if err != nil { t.Fatal(err) } if len(routes) != 1 { t.Fatalf("a route with no internal-name grew a second host: %v", routes) } } // A workload beside the proxy is ordinary, and reaching it over loopback is both correct and the // only thing that works when there is no private network. func TestAConsumerOnTheProxysOwnMachineIsReachedOverLoopback(t *testing.T) { routes, _, err := routesFrom(write(t, `{"given":[ {"from":"app","node":"anchor","values":{"name":"app.example","port":9000}} ]}`)) if err != nil { t.Fatal(err) } if targetOf(routes, "app.example") != "http://127.0.0.1:9000" { t.Fatalf("a workload on this machine was not reachable: %v", routes) } } // Skipped rather than served wrongly. A route with no port would proxy to :0. func TestAContributionMissingWhatARouteNeedsIsSkipped(t *testing.T) { routes, _, err := routesFrom(write(t, `{"given":[ {"from":"a","node":"n","at":"n.internal","values":{"name":"no-port.example"}}, {"from":"b","node":"n","at":"n.internal","values":{"port":8080}}, {"from":"c","node":"n","at":"n.internal","values":{"name":"fine.example","port":8080}} ]}`)) if err != nil { t.Fatal(err) } if len(routes) != 1 || targetOf(routes, "fine.example") == "" { t.Fatalf("an unusable contribution was served: %v", routes) } } // A route may name a target reached over https, for a backend that terminates its own TLS — the // shape Mailu's webmail front needs, which this proxy reaches as a plain workload otherwise. func TestARouteMayTargetHttps(t *testing.T) { routes, _, err := routesFrom(write(t, `{"given":[ {"from":"mail","node":"anchor","at":"anchor.internal", "values":{"name":"mail.example","port":7443,"scheme":"https","insecure":true}} ]}`)) if err != nil { t.Fatal(err) } if targetOf(routes, "mail.example") != "https://anchor.internal:7443" { t.Fatalf("an https target was not built as one: %v", routes) } if !routes["mail.example"][0].insecure { t.Fatal("insecure was declared and not carried onto the rule") } } // A scheme that is neither http nor https is refused rather than guessed at. func TestARouteWithAnUnknownSchemeIsSkipped(t *testing.T) { routes, _, err := routesFrom(write(t, `{"given":[ {"from":"a","node":"n","at":"n.internal","values":{"name":"bad.example","port":80,"scheme":"ftp"}} ]}`)) if err != nil { t.Fatal(err) } if len(routes) != 0 { t.Fatalf("a route with an unusable scheme was served: %v", routes) } } // End to end: a backend terminating TLS with a certificate nothing would ordinarily trust is still // reached when the route declared `insecure`, and the response comes back through unmodified. func TestTheProxyReachesAnInsecureHttpsBackend(t *testing.T) { workload := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("the workload, over its own TLS")) })) defer workload.Close() target := strings.TrimPrefix(workload.URL, "https://") held := newTable() routes := map[string][]rule{"mail.example": {{target: "https://" + target, insecure: true}}} held.set(routes, allPublic(routes)) proxy := httptest.NewServer(handler(held)) defer proxy.Close() asked, err := http.NewRequest(http.MethodGet, proxy.URL, nil) if err != nil { t.Fatal(err) } asked.Host = "mail.example" answer, err := http.DefaultClient.Do(asked) if err != nil { t.Fatal(err) } defer answer.Body.Close() if answer.StatusCode != http.StatusOK { t.Fatalf("an insecure https backend was not reached: %d", answer.StatusCode) } } // End to end through the proxy itself: a request for the name reaches the workload, and a name // nobody asked for is refused in a way that says what IS served. func TestTheProxyReachesTheWorkloadAndNamesWhatItServes(t *testing.T) { workload := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("the workload")) })) defer workload.Close() target := strings.TrimPrefix(workload.URL, "http://") host, port, _ := strings.Cut(target, ":") held := newTable() held.set(plain(map[string]string{"app.example": "http://" + host + ":" + port}), allPublic(plain(map[string]string{"app.example": "http://" + host + ":" + port}))) proxy := httptest.NewServer(handler(held)) defer proxy.Close() asked, err := http.NewRequest(http.MethodGet, proxy.URL, nil) if err != nil { t.Fatal(err) } asked.Host = "app.example" answer, err := http.DefaultClient.Do(asked) if err != nil { t.Fatal(err) } defer answer.Body.Close() if answer.StatusCode != http.StatusOK { t.Fatalf("a request for a served name got %d", answer.StatusCode) } // And a name that is not served says which are — a route withdrawn and a name that never // existed are different things, and a bare 404 makes an operator go and read the mesh. other, _ := http.NewRequest(http.MethodGet, proxy.URL, nil) other.Host = "nobody.example" refused, err := http.DefaultClient.Do(other) if err != nil { t.Fatal(err) } defer refused.Body.Close() if refused.StatusCode != http.StatusNotFound { t.Fatalf("a name nobody asked for got %d", refused.StatusCode) } body := make([]byte, 256) n, _ := refused.Body.Read(body) if !strings.Contains(string(body[:n]), "app.example") { t.Fatalf("the refusal does not say what is served: %s", body[:n]) } } // The file is the whole truth about who has a route, so the table replaces rather than merges. // // Merging would keep serving a name whose module was unassigned — the stale-route fault // 08-connectivity lists as open, reintroduced one level down. A stale public name pointing at // nothing fails more visibly than a stale grant, which is exactly why it must not survive. func TestWithdrawingARouteStopsServingIt(t *testing.T) { held := newTable() initial := plain(map[string]string{ "going.example": "http://a.internal:80", "staying.example": "http://b.internal:80", }) held.set(initial, allPublic(initial)) held.set(plain(map[string]string{"staying.example": "http://b.internal:80"}), allPublic(plain(map[string]string{"staying.example": "http://b.internal:80"}))) if _, still := held.find("going.example", "/"); still { t.Fatal("a route whose module was unassigned is still served") } if _, kept := held.find("staying.example", "/"); !kept { t.Fatal("withdrawing one route took another with it") } } // A Host header carries a port and the name does not. func TestARequestNamingAPortStillFindsItsRoute(t *testing.T) { held := newTable() held.set(plain(map[string]string{"app.example": "http://a.internal:8080"}), allPublic(plain(map[string]string{"app.example": "http://a.internal:8080"}))) if _, found := held.find("app.example:8080", "/"); !found { t.Fatal("a request to app.example:8080 did not find the route for app.example") } } // Defends novox/hq 04-ISSUES/004: issuance targets staging unless something says otherwise. // // The failure this guards is not a broken proxy. It is a working one that quietly spends a // production quota which does not replenish for a week, on exactly the work most likely to // iterate. func TestTheIssuerIsStagingUnlessNamed(t *testing.T) { t.Setenv("ACME_DIRECTORY", "") if got := issuer(); !strings.Contains(got, "staging") { t.Fatalf("with nothing set the issuer is %q, and a default that spends production quota "+ "is a default nobody chose", got) } t.Setenv("ACME_DIRECTORY", "https://acme-v02.api.letsencrypt.org/directory") if got := issuer(); strings.Contains(got, "staging") { t.Fatalf("an issuer was named explicitly and %q was used instead", got) } } // A certificate is only ever asked for on a name the mesh routes here. // // **Without this, anything that can reach the port spends the quota.** A scan sending arbitrary // names, or one misconfigured client, becomes a stream of failed orders against the account's // rate limit — and the proxy would look healthy throughout. func TestNoCertificateIsAskedForOnAnUnroutedName(t *testing.T) { held := newTable() held.set(plain(map[string]string{"photos.example": "http://127.0.0.1:8080"}), allPublic(plain(map[string]string{"photos.example": "http://127.0.0.1:8080"}))) policy := onlyWhatTheMeshSaid(held) if err := policy(context.Background(), "photos.example"); err != nil { t.Errorf("a name the mesh routes here was refused a certificate: %v", err) } for _, name := range []string{"unknown.example", "", "photos.example.evil"} { if err := policy(context.Background(), name); err == nil { t.Errorf("a certificate would be ordered for %q, which the mesh never mentioned", name) } } } // A certificate is asked for on a route's public name, never on its internal-network alias — no // public CA can validate a private name, and asking anyway would only spend the account's rate // limit on an order that can never succeed. func TestNoCertificateIsAskedForOnAnInternalAlias(t *testing.T) { routes, public, err := routesFrom(write(t, `{"given":[ {"from":"app","node":"anchor","at":"anchor.internal", "values":{"name":"app.example","internal-name":"app.anchor.internal","port":8080}} ]}`)) if err != nil { t.Fatal(err) } held := newTable() held.set(routes, public) policy := onlyWhatTheMeshSaid(held) if err := policy(context.Background(), "app.example"); err != nil { t.Errorf("the route's public name was refused a certificate: %v", err) } if err := policy(context.Background(), "app.anchor.internal"); err == nil { t.Error("a certificate was ordered for the internal alias, which no public CA can validate") } } // A certificate is asked of the *internal* authority only for a name that is routed here and is // not a route's own public name — the internal-network alias, never the route it accompanies. func TestTheInternalAuthorityOnlyCertifiesInternalOnlyAliases(t *testing.T) { routes, public, err := routesFrom(write(t, `{"given":[ {"from":"app","node":"anchor","at":"anchor.internal", "values":{"name":"app.example","internal-name":"app.anchor.internal","port":8080}} ]}`)) if err != nil { t.Fatal(err) } held := newTable() held.set(routes, public) policy := onlyInternalNamesTheMeshSaid(held) if err := policy(context.Background(), "app.anchor.internal"); err != nil { t.Errorf("the internal alias was refused by its own authority: %v", err) } if err := policy(context.Background(), "app.example"); err == nil { t.Error("the internal authority certified a route's public name, which the public authority already covers") } if err := policy(context.Background(), "unrouted.internal"); err == nil { t.Error("the internal authority certified a name nobody routed here") } } // A route withdrawn stops being certifiable, without the proxy restarting. func TestWithdrawingARouteWithdrawsItsCertificate(t *testing.T) { held := newTable() held.set(plain(map[string]string{"photos.example": "http://127.0.0.1:8080"}), allPublic(plain(map[string]string{"photos.example": "http://127.0.0.1:8080"}))) policy := onlyWhatTheMeshSaid(held) if err := policy(context.Background(), "photos.example"); err != nil { t.Fatal(err) } held.set(nil, nil) if err := policy(context.Background(), "photos.example"); err == nil { t.Fatal("a withdrawn route can still order certificates, so the policy read a copy taken " + "once rather than what is served now") } }