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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user