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:
co-authored by
Claude Opus 5
parent
d65dbbc75a
commit
2ed4d1f478
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user