route-proxy serves a route's internal-name alias, never certifies it

A route now consumed with two hosts when the mesh composed both — the
same host under internal-name reaches the same rule as its public name,
restoring the convenience a predecessor proxy gave for reaching a service
over the VPN without a public TLS round trip (the field composeName now
writes, feat/route-carries-internal-alias — this branch depends on that
one landing for internal-name to ever be populated; builds and tests
clean without it, just serves nothing extra).

Never certified: onlyWhatTheMeshSaid used routed(), which answered yes
for any host in the table regardless of how it got there. A new
eligibleForACME() checks a parallel 'public' set instead — every host
reached through a route's own name, never one reached only through its
internal-name — so an internal alias is proxied but never given its own
failing ACME order. routed() is unchanged and still used for the 404
message, which legitimately wants 'is this host served at all.'
This commit is contained in:
2026-09-25 17:24:13 +02:00
parent 752abaa81d
commit 6da55bdfea
3 changed files with 127 additions and 24 deletions
+40 -10
View File
@@ -97,10 +97,10 @@ func issuer() string {
// to what it may serve.
func onlyWhatTheMeshSaid(held *table) autocert.HostPolicy {
return func(_ context.Context, host string) error {
if held.routed(host) {
if held.eligibleForACME(host) {
return nil
}
return fmt.Errorf("no route for %q in this mesh, so no certificate is asked for", host)
return fmt.Errorf("no public route for %q in this mesh, so no certificate is asked for", host)
}
}
@@ -163,9 +163,14 @@ type rule struct {
type table struct {
mu sync.RWMutex
to map[string][]rule
// public is which routed hosts are eligible for a real certificate — every host reached as a
// route's own `name`, never one reached only as its `internal-name`. A private alias can never
// pass ACME's own validation (it has no public DNS to prove it against), so asking for it is
// not merely pointless but the failing order onlyWhatTheMeshSaid exists to prevent.
public map[string]bool
}
func (t *table) set(routes map[string][]rule) {
func (t *table) set(routes map[string][]rule, public map[string]bool) {
made := map[string][]rule{}
for host, rules := range routes {
kept := make([]rule, 0, len(rules))
@@ -192,6 +197,7 @@ func (t *table) set(routes map[string][]rule) {
}
t.mu.Lock()
t.to = made
t.public = public
t.mu.Unlock()
}
@@ -234,6 +240,16 @@ func (t *table) find(host, path string) (rule, bool) {
//
// Separate from find because certificate issuance is a question about the *name*: a host whose only
// rules are path-scoped is still a name this proxy answers to, and still needs a certificate.
// eligibleForACME says whether this proxy may ask a certificate authority for this name — every
// host reached as a route's own public `name`, never one reached only as its `internal-name`
// alias, which no public CA can ever validate.
func (t *table) eligibleForACME(host string) bool {
t.mu.RLock()
defer t.mu.RUnlock()
bare := bareHost(host)
return len(t.to[bare]) > 0 && t.public[bare]
}
func (t *table) routed(host string) bool {
t.mu.RLock()
defer t.mu.RUnlock()
@@ -282,7 +298,7 @@ func run() error {
held := newTable()
read := func() {
routes, err := routesFrom(path)
routes, public, err := routesFrom(path)
if err != nil {
// Kept serving what it had. A file being rewritten is momentarily unreadable, and
// dropping every route because one read landed mid-write would turn an ordinary
@@ -290,7 +306,7 @@ func run() error {
log.Printf("cannot read %s, keeping what is already served: %v", path, err)
return
}
held.set(routes)
held.set(routes, public)
log.Printf("serving %d route(s): %s", len(routes), strings.Join(held.names(), ", "))
}
read()
@@ -512,18 +528,21 @@ func boolByte(b bool) byte {
return 0
}
// routesFrom reads what the mesh wrote and turns it into host → the rules for that host.
func routesFrom(path string) (map[string][]rule, error) {
// 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.
func routesFrom(path string) (map[string][]rule, map[string]bool, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
return nil, nil, err
}
var said given
if err := json.Unmarshal(raw, &said); err != nil {
return nil, err
return nil, nil, err
}
out := map[string][]rule{}
public := map[string]bool{}
for _, c := range said.Given {
name, _ := c.Values["name"].(string)
if name == "" {
@@ -531,6 +550,7 @@ func routesFrom(path string) (map[string][]rule, error) {
continue
}
host := strings.ToLower(name)
public[host] = true
made := rule{path: asPath(c.Values["path"])}
if p, ok := asWhole(c.Values["priority"]); ok {
@@ -579,8 +599,18 @@ func routesFrom(path string) (map[string][]rule, error) {
}
out[host] = append(out[host], made)
// The internal-network alias, 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) != "" {
out[strings.ToLower(internal)] = append(out[strings.ToLower(internal)], made)
}
}
return out, nil
return out, public, nil
}
// asWhole is any whole number the mesh wrote, whatever its magnitude.