From 9acb5f1292eaabf11f348180f615ea0ea0be3407 Mon Sep 17 00:00:00 2001 From: jochens Date: Thu, 1 Oct 2026 23:31:57 +0200 Subject: [PATCH] route-proxy: serve a route that names only its internal host Since ADR 0138 an endpoint that reaches only the private network gets an internal-name and no name, and the proxy skipped it as naming nothing, so every internal-only module was unreachable by name (novox/hq issue 191). --- examples/route-proxy/main.go | 39 +++++++++++++++++-------- examples/route-proxy/routes_test.go | 44 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/examples/route-proxy/main.go b/examples/route-proxy/main.go index 5b9a39a..484ce2a 100644 --- a/examples/route-proxy/main.go +++ b/examples/route-proxy/main.go @@ -716,6 +716,12 @@ func boolByte(b bool) byte { // routesFrom reads what the mesh wrote and turns it into host → the rules for that host, and // which of those hosts is a public name — the second is `name`, ACME-eligible; a host reached // only through `internal-name` never appears there. +// +// **A route may carry either name, or both** (novox/hq ADR 0138). How far an endpoint reaches +// decides which names the mesh composes, so an endpoint that reaches only the private network +// arrives with an `internal-name` and no `name`. That is a whole route, not a malformed one: it is +// served under its internal name and certified by the internal authority. Only a route with +// neither name has nothing to be served under (novox/hq issue 191). func routesFrom(path string) (map[string][]rule, map[string]bool, error) { raw, err := os.ReadFile(path) if err != nil { @@ -730,12 +736,18 @@ func routesFrom(path string) (map[string][]rule, map[string]bool, error) { public := map[string]bool{} for _, c := range said.Given { name, _ := c.Values["name"].(string) - if name == "" { + name = strings.TrimSpace(name) + internal, _ := c.Values["internal-name"].(string) + internal = strings.TrimSpace(internal) + if name == "" && internal == "" { log.Printf("%s on %s asked for a route and named nothing; skipped", c.From, c.Node) continue } - host := strings.ToLower(name) - public[host] = true + // What the route is called in a log line: its public name when it has one. + called := name + if called == "" { + called = internal + } made := rule{path: asPath(c.Values["path"])} if p, ok := asWhole(c.Values["priority"]); ok { @@ -752,7 +764,7 @@ func routesFrom(path string) (map[string][]rule, map[string]bool, error) { if looksLikeACredential(named) { log.Printf("%s on %s declared route %q with a credential in the declaration rather "+ "than the name of a secret; the whole route is refused (novox/hq ADR 0108)", - c.From, c.Node, name) + c.From, c.Node, called) continue } users, err := usersFrom(named) @@ -770,7 +782,7 @@ func routesFrom(path string) (map[string][]rule, map[string]bool, error) { port, ok := asPort(c.Values["port"]) if !ok { log.Printf("%s on %s asked for route %q and gave no usable port; skipped", - c.From, c.Node, name) + c.From, c.Node, called) continue } // Where the mesh says that machine is. Empty means it is this one — a workload beside @@ -791,7 +803,7 @@ func routesFrom(path string) (map[string][]rule, map[string]bool, error) { } if scheme != "http" && scheme != "https" { log.Printf("%s on %s asked for route %q with scheme %q, which is neither http "+ - "nor https; skipped", c.From, c.Node, name, scheme) + "nor https; skipped", c.From, c.Node, called, scheme) continue } made.insecure, _ = c.Values["insecure"].(bool) @@ -802,7 +814,7 @@ func routesFrom(path string) (map[string][]rule, map[string]bool, error) { bytes, whole := asWhole(asked) if !whole || bytes <= 0 { log.Printf("%s on %s asked for route %q with a max-request-body of %v, which is "+ - "not a whole positive number of bytes; skipped", c.From, c.Node, name, asked) + "not a whole positive number of bytes; skipped", c.From, c.Node, called, asked) continue } made.maxRequestBody = int64(bytes) @@ -810,15 +822,20 @@ func routesFrom(path string) (map[string][]rule, map[string]bool, error) { made.target = fmt.Sprintf("%s://%s:%d", scheme, at, port) } - out[host] = append(out[host], made) + if name != "" { + host := strings.ToLower(name) + out[host] = append(out[host], made) + public[host] = true + } - // The internal-network alias, the same rule under a second host — a predecessor proxy + // The internal-network name, the same rule under a second host — a predecessor proxy // answered both for one route, as a convenience (reaching a service over the VPN without a // public TLS round trip), not as an access boundary; composing it here restores exactly // that, nothing more. Absent whenever the node composed no internal name (novox/hq ADR // 0056's internalDomain half) — the same "nothing to join a label to" case the public name - // already has. - if internal, _ := c.Values["internal-name"].(string); strings.TrimSpace(internal) != "" { + // already has. And the only name, when the endpoint reaches no further than the private + // network. + if internal != "" { out[strings.ToLower(internal)] = append(out[strings.ToLower(internal)], made) } } diff --git a/examples/route-proxy/routes_test.go b/examples/route-proxy/routes_test.go index 3bb71a3..1ad9a26 100644 --- a/examples/route-proxy/routes_test.go +++ b/examples/route-proxy/routes_test.go @@ -90,6 +90,50 @@ func TestARouteWithAnInternalNameIsReachableUnderBoth(t *testing.T) { } } +// A route whose endpoint reaches only the private network carries an internal name and no public +// one (novox/hq ADR 0138), and is served under that name rather than skipped as naming nothing — +// skipping it left every internal-only module unreachable by name (novox/hq issue 191). +func TestARouteWithOnlyAnInternalNameIsServed(t *testing.T) { + routes, public, err := routesFrom(write(t, `{"given":[ + {"from":"app","node":"anchor","at":"anchor.internal", + "values":{"internal-name":"App.Anchor.Internal","port":8443,"scheme":"https","insecure":true}} + ]}`)) + if err != nil { + t.Fatal(err) + } + if targetOf(routes, "app.anchor.internal") != "https://anchor.internal:8443" { + t.Fatalf("the internal-only route is not served: %v", routes) + } + if len(routes) != 1 { + t.Errorf("an internal-only route made hosts it never named: %v", routes) + } + if len(public) != 0 { + t.Errorf("an internal-only route made a name eligible for a public certificate: %v", public) + } + + held := newTable() + held.set(routes, public) + if err := onlyInternalNamesTheMeshSaid(held)(context.Background(), "app.anchor.internal"); err != nil { + t.Errorf("the internal authority refused the internal-only route's name: %v", err) + } + if err := onlyWhatTheMeshSaid(held)(context.Background(), "app.anchor.internal"); err == nil { + t.Error("a public certificate was ordered for an internal-only name") + } +} + +// A route with neither name has nothing to be served under, and is still skipped. +func TestARouteWithNeitherNameIsSkipped(t *testing.T) { + routes, public, err := routesFrom(write(t, `{"given":[ + {"from":"app","node":"anchor","at":"anchor.internal","values":{"internal-name":" ","port":8080}} + ]}`)) + if err != nil { + t.Fatal(err) + } + if len(routes) != 0 || len(public) != 0 { + t.Errorf("a route that named nothing was served: %v %v", routes, 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":[