[management] Canonicalize the proxy cluster address where it is stored

The proxy-connect path already computes the canonical form of the address
a proxy declares — ValidateDomains returns lowercase punycode — and then
throws it away, storing the string as declared. cluster_address is the key
every capability, ownership and routing lookup matches on, so one host
could sit in that column under two spellings, and the previous commit
compensated with LOWER() in the ownership query, which gives up the
cluster_address index on a query that runs for every account-scoped proxy
connect.

Keep the canonical form instead. Connect is the only writer of the column
(heartbeats touch last_seen and status), and proxy rows are session state
rebuilt on every connect rather than durable config, so the column
converges without a migration and the lookups can stay exact and indexed.

Folding happens before punycode conversion, not after: idna lowercases the
ASCII it produces but does not case-fold the unicode it consumes, so
PRÖXY.example.com and pröxy.example.com would otherwise encode to two
different labels for one host.

The agent network check keeps comparing normalised forms in memory, which
costs nothing there — it is a pass over the account's cluster list, not a
query — and covers rows written before this landed.
This commit is contained in:
mlsmaycon
2026-09-03 10:19:04 +00:00
parent d911649158
commit 61e1742885
5 changed files with 90 additions and 41 deletions
@@ -937,13 +937,16 @@ func (m *managerImpl) validateGatewayCluster(ctx context.Context, accountID, clu
// host as clusterAddr. Empty means management holds no proxy row for that host
// in this account's view.
//
// Addresses are stored as the proxy declared them and hostnames are
// case-insensitive, so identity is compared on the normalised form while the
// stored spellings are what comes back: the capability lookups match
// cluster_address exactly, and handing one a normalised address it never
// stored would silently find nothing. The cluster listing is not gated on
// heartbeats, so this answer does not change while a cluster's proxies are
// merely offline.
// Addresses are canonicalised where they are written (canonicalProxyAddress on
// the proxy-connect path), so a stored spelling normally is the normalised
// form. Identity is still compared on the normalised form rather than
// byte-equal, which costs nothing here — this is an in-memory pass over the
// account's clusters, not a query — and covers a row written before that
// landed. What comes back is the stored spelling either way, because the
// capability lookup matches cluster_address exactly and would silently find
// nothing under a spelling the store never held. The cluster listing is not
// gated on heartbeats, so this answer does not change while a cluster's
// proxies are merely offline.
func (m *managerImpl) accountClusterSpellings(ctx context.Context, accountID, clusterAddr string) ([]string, error) {
clusters, err := m.store.GetProxyClusters(ctx, accountID)
if err != nil {