Reach the server by address when its name will not resolve

A measurement tool that cannot report from a broken network is useless
exactly when it matters, and a wedged resolver is one of the faults this
app is built to find — it should not also be the thing that stops the
finding being delivered. The profile already carries the server's
addresses; they are now kept and used when the name fails.

Safe because the pin is the trust and the name is not part of it: the
server presents the same certificate however it was reached, and a wrong
address fails the pin like anything else. Only the primaries are cached —
the alternate pair exists for NAT behaviour discovery and does not carry
the control plane, so falling back to one would fail for a second,
unrelated reason.

Substituted only when the name genuinely does not resolve, and only after
checking the candidate answers on the port: on a v4-only network a v6
address would otherwise be chosen and fail slowly, which is the wrong
answer delivered late.

The server had to meet it halfway. Sharing 443 by SNI meant a client
arriving by IP sent no server name and got the Let's Encrypt certificate,
failing the pin. A numeric host — or no SNI at all — now selects the
pinned certificate and routes to the control plane. That is sound because
the admin UI is only ever reached by name: browsers always send SNI, and
nobody bookmarks an IP for a site with a CA-issued certificate.

Verified against fmr: by IP on both families the served pin is the
control one and /v1/profile answers 401, while fmr.echo-lot.app still
serves the Let's Encrypt certificate and the admin UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-02 10:03:14 +02:00
co-authored by Claude Opus 5
parent d65dbbc75a
commit 2ed4d1f478
5 changed files with 98 additions and 9 deletions
+24 -6
View File
@@ -416,8 +416,30 @@ func serve(cfg *config.Config) error {
// port number.
ctlHandler := ctl.Handler()
sharedCert := cert
// Which side of the port a request belongs to.
//
// The control hostname is the obvious case. A bare IP is the other one, and it matters: a
// client whose DNS has failed can still reach the server by an address it cached from the
// profile, and a measurement tool that cannot report from a broken network is useless
// precisely when it is needed. That client authenticates by pin, so the name it used to get
// here is not part of the trust decision.
//
// Safe to route that way because the admin UI is only ever reached by name: browsers always
// send SNI and nobody bookmarks an IP for a site with a Let's Encrypt certificate. Anything
// addressing this server numerically is a pinned client.
isControl := func(host string) bool {
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
host = strings.Trim(host, "[]")
if cfg.ControlHostname != "" && strings.EqualFold(host, cfg.ControlHostname) {
return true
}
return net.ParseIP(host) != nil
}
pickCert := func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if cfg.ControlHostname != "" && strings.EqualFold(hi.ServerName, cfg.ControlHostname) {
// No SNI at all also means a numeric client: every browser sends it.
if hi.ServerName == "" || isControl(hi.ServerName) {
return &sharedCert, nil
}
if adminTLS != nil && adminTLS.GetCertificate != nil {
@@ -426,11 +448,7 @@ func serve(cfg *config.Config) error {
return &sharedCert, nil
}
route := func(w http.ResponseWriter, r *http.Request) {
host := r.Host
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
if cfg.ControlHostname != "" && strings.EqualFold(host, cfg.ControlHostname) {
if isControl(r.Host) {
ctlHandler.ServeHTTP(w, r)
return
}