Issue 191: an internal name is served to the private network only

The first fix served the dropped route to anyone who sent its name. Record
why in the issue, as a progressive insight on ADR 0138, and in the to-be
connectivity design.
This commit is contained in:
2026-10-02 01:10:13 +02:00
parent 496d136d72
commit 68650cda5f
4 changed files with 71 additions and 4 deletions
@@ -1,9 +1,9 @@
---
status: located
opened: 2026-10-01
located-in: [mesh-controller examples/route-proxy/main.go (routesFrom requires a route's public `name` and treats `internal-name` only as an alias of it)]
located-in: [mesh-controller examples/route-proxy/main.go (routesFrom requires a route's public `name` and treats `internal-name` only as an alias of it; the handler serves every routed name to any source), mesh-catalog modules/route-proxy/module.json (the proxy is not told the private network's range)]
fixed-by:
amended-design: []
amended-design: [03-DESIGN/01-to-be/08-connectivity.md]
---
# 191 — A route with only an internal name is dropped as naming nothing
@@ -23,9 +23,43 @@ mesh before ADR 0138, when both names were always composed. It has been wrong si
The other half of the proxy already handles the case. Certificates for a host are split by whether it
is in the public set: hosts outside it go to the internal authority, and only hosts inside it are
eligible for ACME. A host that is only ever an internal name falls on the correct side of both checks
without change. Only reading the route was wrong.
without change. For certificates, only reading the route was wrong; who may reach the route is the next section.
**The fix.** `routesFrom` takes a route that names either host, serves each name it carries, and
marks only the public one as public. It still skips a route that names neither, with the same log line.
A test proves an internal-only route is served, certified by the internal authority, and refused by
the public one. That test fails against the code before the change.
## The first fix would have made the name public — 2026-10-02, from review
Serving the dropped route was not enough. The proxy picks a route from the name a request carries
and never from where the request came from, and it answers public and internal names on the same
listeners. Its public names resolve to an address the internet reaches. So once the internal-only
route was served, any request from the internet carrying `unifi.home-server.internal` — a name of a
fixed, guessable shape — would have reached an administration interface that reach `internal` was
chosen to keep private. Before the fix the route was unreachable from everywhere. After it, it would
have been reachable from everywhere. Two more leaks came with it: the proxy's answer for an unrouted
name listed every name it serves, internal ones included, and the handshake handed a certificate
naming the internal host to any client.
Nothing showed this while every routed endpoint also had a public name: its internal name exposed
nothing the public one did not. It is a gap in the decision's wording, not only in the proxy — ADR
0138 says the proxy *serves* the internal name without saying to whom — so it is recorded there as a
progressive insight and in the to-be connectivity design.
**Where "inside" is decided.** The mesh's guard recognises the private network by the interface a
packet arrives on, never by source address, because a source can be claimed. The proxy cannot see the
interface, so it reads the source: the mesh's range, loopback, and the ranges of the machine's own
container bridges, which are the same interfaces the guard names. A claimed source does not carry
here, as a connection needs its replies, and replies to those addresses leave by the tunnel or a
local bridge. The range reaches the proxy from the catalog as the machine's `mesh-range`, the way the
intrusion filter already receives it. With none given, the proxy serves internal names to the machine
alone: refused, not opened.
**What changed with it.** The internal name of a route that also has a public one is now served to
the private network only, like any other internal name. Outsiders have the public name, so nothing
they could reach is lost.
**Order of release.** The catalog change comes first. A proxy built from the change but started
without the range would serve internal names to its own machine alone, and every other member of the
mesh would lose them until the range arrived.