From 098a2ca4851ec7eaa82efa02e95a60fae24745e9 Mon Sep 17 00:00:00 2001 From: jochens Date: Thu, 1 Oct 2026 23:31:57 +0200 Subject: [PATCH] Issue 191: a route with only an internal name is dropped as naming nothing --- .../00-report.md | 61 +++++++++++++++++++ .../01-diagnosis.md | 31 ++++++++++ 2 files changed, 92 insertions(+) create mode 100644 04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md create mode 100644 04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/01-diagnosis.md diff --git a/04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md b/04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md new file mode 100644 index 0000000..748c1ce --- /dev/null +++ b/04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md @@ -0,0 +1,61 @@ +--- +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)] +fixed-by: +amended-design: [] +--- + +# 191 — A route with only an internal name is dropped as naming nothing + +## What was observed + +A module whose endpoint reaches only the private network could not be reached by its internal name. +The module ran and answered on its own port. Its route's internal name resolved to the serving node. +The request failed during the TLS handshake: + +``` +http: TLS handshake error from …: no public route for "unifi.home-server.internal" in this mesh, + so no certificate is asked for +``` + +The proxy's own log said why, every time it re-read its routes: + +``` +unifi on home-server asked for a route and named nothing; skipped +``` + +The route it skipped was not empty. The mesh had given it an endpoint, a port, a scheme and an internal +name, and no public name: + +| route | `name` | `internal-name` | served | +|---|---|---|---| +| home-assistant | a public name | `home-assistant.home-server.internal` | under both | +| unifi | — | `unifi.home-server.internal` | under neither | + +Three other modules on the same node were skipped with the same line on the same pass. + +## Why it matters + +**Reach is decided in one place, and the proxy reads the old shape of the decision.** +[ADR 0138](../../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) +made an endpoint's reach decide which names exist. The controller composes the public name, the +internal name, or both, and composes no name that nobody asked for +([issue 140](../140-an-endpoints-reach-is-not-declared/01-resolution.md)). An endpoint that reaches +only the private network is the ordinary case for anything that should not face the internet. It is +exactly the case the proxy drops. + +**The failure is quiet and points the wrong way.** Nothing marks the module unhealthy. The handshake +error says *no public route*, which reads as a certificate fault on the reader's side. The line that +gives the real cause is one of four identical lines repeated every few seconds in a log nobody reads +until they already suspect the proxy. + +**The opposite move is not a workaround.** Giving the endpoint public reach makes the proxy serve it. +It also publishes an administration interface to the internet to get a name on the private network. + +## Open questions + +- Should the proxy refuse a route it cannot serve in a way the controller or an operator sees, not + only in its own log? The same silent skip covers a route with no usable port or an unknown scheme. +- What checks that what the controller composes and what the proxy serves stay the same shape? ADR + 0138 changed one side and nothing failed on the other. diff --git a/04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/01-diagnosis.md b/04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/01-diagnosis.md new file mode 100644 index 0000000..f8bf153 --- /dev/null +++ b/04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/01-diagnosis.md @@ -0,0 +1,31 @@ +# Diagnosis + +*2026-10-01.* + +**Ruled out first: the module itself.** Its container was up and had not restarted. The controller's +status endpoint answered on its own port with `"up": true`. The tool wrapper beside it was serving +all its tools. + +**Ruled out: name resolution.** The internal name resolved to the serving node's private-network +address, which is where the proxy listens. Plain HTTP to the name reached the proxy and got a 404. +HTTPS failed in the handshake, and the proxy logged that it had no route for the name. + +**The route as the proxy received it.** The mesh-written route file held a complete contribution +for the module: endpoint `web`, port, scheme `https`, `insecure`, a label, and `internal-name`. It had +no `name`. That is what the controller composes for an endpoint whose reach stops at the private +network (ADR 0138, `composeName`). The contribution was correct. + +**Located: `routesFrom` in the proxy.** It reads `name` first and skips the contribution if `name` +is empty. It reads `internal-name` only at the end, as a second host for a rule that already has a +public one. So the proxy can serve an internal name only next to a public one. That matched the +mesh before ADR 0138, when both names were always composed. It has been wrong since then. + +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. + +**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.