Skip to content

Commit 10d8746

Browse files
scotwellsclaude
andcommitted
fix(extension-server): use a unique connector domain to avoid Envoy NACK
Online connector routing appended the connector's backend target host to the virtual host's domains. Tunnels overwhelmingly target "localhost", so every connector's virtual host received the same "localhost" domain. On a shared route configuration (the HTTP listener that merges all gateways) those duplicate, and Envoy rejects the entire xDS snapshot ("Only unique values for domains are permitted"). A single collision freezes config updates fleet-wide: new tunnels never program a route, and connectors that come online never leave the offline 503 program. Append a synthetic domain derived from the virtual host name instead. Envoy already requires virtual host names to be unique within a route configuration, so the domain can never collide. The internal tunnel listener routes on cluster metadata (tunnel address + endpoint id), not on this domain, so a synthetic value preserves tunnel routing. Adds a regression test asserting two online connectors that share a backend host on one route configuration produce no duplicate domain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2f46ca8 commit 10d8746

3 files changed

Lines changed: 89 additions & 30 deletions

File tree

internal/extensionserver/mutate/connector.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func ReplaceConnectorClusters(
7373

7474
// ApplyConnectorRoutes applies connector route mutations to a RouteConfiguration:
7575
// - Online (replaced) connector: prepend a CONNECT upgrade route targeting the
76-
// replaced cluster and append info.TargetHost to VH domains.
76+
// replaced cluster and append a unique per-connector domain to VH domains.
7777
// - Offline connector: prepend a CONNECT direct_response 503 (tunnel-control
7878
// clients) and rewrite the user-facing forwarding routes to a 503
7979
// direct_response (see the offline branch for why).
@@ -105,7 +105,7 @@ func ApplyConnectorRoutes(
105105

106106
var newRoute *routev3.Route
107107

108-
if info, ok := replaced[connectorCluster]; ok {
108+
if _, ok := replaced[connectorCluster]; ok {
109109
// Online: CONNECT route targeting the replaced cluster.
110110
newRoute, err = buildConnectRoute(
111111
"connector-connect-"+sanitizeID(vh.GetName()),
@@ -116,10 +116,15 @@ func ApplyConnectorRoutes(
116116
}
117117
// Prepend the CONNECT route (NSO inserts at /virtual_hosts/0/routes/0).
118118
vh.Routes = append([]*routev3.Route{newRoute}, vh.Routes...)
119-
// Append the connector's target host to VH domains.
120-
// Production uses the actual backend hostname (conflict C1 in design plan),
121-
// NOT a synthetic .connector.local domain.
122-
vh.Domains = appendUnique(vh.Domains, info.TargetHost)
119+
// Make the CONNECT route addressable with a domain that is unique per
120+
// virtual host. The backend host itself must NOT be used here: tunnels
121+
// overwhelmingly target "localhost", so reusing it would put the same
122+
// domain on every connector's virtual host. On a shared route
123+
// configuration (e.g. the HTTP listener that merges all gateways) those
124+
// collide, and Envoy rejects the entire snapshot — freezing config
125+
// updates fleet-wide. The internal tunnel listener routes on cluster
126+
// metadata, not this domain, so a synthetic unique value is sufficient.
127+
vh.Domains = appendUnique(vh.Domains, connectorMatchDomain(vh.GetName()))
123128
} else {
124129
// Offline connect_matcher route for tunnel-control clients.
125130
newRoute, err = buildOfflineRoute("connector-offline-" + sanitizeID(vh.GetName()))
@@ -230,6 +235,15 @@ func buildConnectorCluster(
230235
return cl, nil
231236
}
232237

238+
// connectorMatchDomain returns the synthetic domain added to an online
239+
// connector's virtual host so its CONNECT route is addressable. It is derived
240+
// from the virtual host name, which Envoy already requires to be unique within a
241+
// route configuration, so the domain can never collide with another connector on
242+
// the same (possibly shared) route configuration.
243+
func connectorMatchDomain(vhName string) string {
244+
return sanitizeID(vhName) + ".connector.internal"
245+
}
246+
233247
// buildConnectRoute builds an online CONNECT route (connect_matcher + CONNECT
234248
// upgrade) pointed at the given connector cluster. Mirrors buildConnectRoute in
235249
// test/perf/extserver/internal/mutate/connector.go.

internal/extensionserver/mutate/connector_test.go

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func TestReplaceConnectorClusters_ClusterNotInPolicyIndex_Skipped(t *testing.T)
182182

183183
// --- ApplyConnectorRoutes tests ---
184184

185-
func TestApplyConnectorRoutes_Online_PrependsCONNECTRouteAndAppendsTargetHost(t *testing.T) {
185+
func TestApplyConnectorRoutes_Online_PrependsCONNECTRouteAndAppendsUniqueDomain(t *testing.T) {
186186
idx := connectorPolicyIndex(true)
187187
clusterName := testClusterName()
188188

@@ -230,25 +230,28 @@ func TestApplyConnectorRoutes_Online_PrependsCONNECTRouteAndAppendsTargetHost(t
230230
// Original forwarding route must remain second.
231231
assert.Equal(t, "fwd", vh.Routes[1].GetName())
232232

233-
// TargetHost (actual backend host) must be appended to VH domains.
234-
// Production uses info.TargetHost, NOT a synthetic .connector.local domain (design §2.3 C1).
235-
assert.Contains(t, vh.Domains, testTargetHost,
236-
"TargetHost must be appended to VH domains (not a .connector.local synthetic domain)")
233+
// A unique synthetic domain is appended so the CONNECT route is addressable.
234+
// The raw backend host must NOT be used: tunnels commonly share a target host
235+
// (e.g. "localhost"), which collides on shared route configurations.
236+
assert.Contains(t, vh.Domains, connectorMatchDomain(vh.GetName()),
237+
"a unique per-VH synthetic domain must be appended")
238+
assert.NotContains(t, vh.Domains, testTargetHost,
239+
"the raw backend host must not be appended (it is the collision source)")
237240
}
238241

239-
func TestApplyConnectorRoutes_Online_TargetHostDeduplicated(t *testing.T) {
242+
func TestApplyConnectorRoutes_Online_DomainAppendIsIdempotent(t *testing.T) {
240243
idx := connectorPolicyIndex(true)
241244
clusterName := testClusterName()
242245
info := &extcache.ConnectorInfo{Online: true, TargetHost: testTargetHost, TargetPort: testTargetPort}
243246
replaced := map[string]*extcache.ConnectorInfo{clusterName: info}
244247
offline := map[string]*extcache.ConnectorInfo{}
245248

246-
// VH already has the target host in its domains.
249+
// VH already carries the synthetic domain from a prior mutation.
247250
rc := &routev3.RouteConfiguration{
248251
VirtualHosts: []*routev3.VirtualHost{
249252
{
250253
Name: "vh",
251-
Domains: []string{"app.local.test", testTargetHost},
254+
Domains: []string{"app.local.test", connectorMatchDomain("vh")},
252255
Routes: []*routev3.Route{routeTargeting(clusterName)},
253256
},
254257
},
@@ -257,14 +260,54 @@ func TestApplyConnectorRoutes_Online_TargetHostDeduplicated(t *testing.T) {
257260
_, _, err := ApplyConnectorRoutes(rc, idx, replaced, offline)
258261
require.NoError(t, err)
259262

260-
// Domain must not be duplicated.
263+
// Synthetic domain must not be duplicated.
261264
count := 0
262265
for _, d := range rc.VirtualHosts[0].Domains {
263-
if d == testTargetHost {
266+
if d == connectorMatchDomain("vh") {
264267
count++
265268
}
266269
}
267-
assert.Equal(t, 1, count, "target host must appear exactly once in VH domains")
270+
assert.Equal(t, 1, count, "synthetic domain must appear exactly once in VH domains")
271+
}
272+
273+
// TestApplyConnectorRoutes_Online_NoDomainCollisionAcrossConnectors is the
274+
// regression test for the duplicate-domain Envoy NACK: two online connectors
275+
// that share a backend host ("localhost") on the SAME (shared) route
276+
// configuration must not produce a duplicate domain, or Envoy rejects the whole
277+
// snapshot and freezes config updates fleet-wide.
278+
func TestApplyConnectorRoutes_Online_NoDomainCollisionAcrossConnectors(t *testing.T) {
279+
clusterA := "httproute/ds-ns/proxy-a/rule/0"
280+
clusterB := "httproute/ds-ns/proxy-b/rule/0"
281+
const sharedHost = "localhost"
282+
283+
replaced := map[string]*extcache.ConnectorInfo{
284+
clusterA: {Online: true, TargetHost: sharedHost, TargetPort: 8099},
285+
clusterB: {Online: true, TargetHost: sharedHost, TargetPort: 8099},
286+
}
287+
offline := map[string]*extcache.ConnectorInfo{}
288+
289+
// One shared route config (e.g. the merged HTTP listener) with both tunnels.
290+
rc := &routev3.RouteConfiguration{
291+
Name: "http-80",
292+
VirtualHosts: []*routev3.VirtualHost{
293+
{Name: "tunnel-a", Domains: []string{"a.example.com"}, Routes: []*routev3.Route{routeTargeting(clusterA)}},
294+
{Name: "tunnel-b", Domains: []string{"b.example.com"}, Routes: []*routev3.Route{routeTargeting(clusterB)}},
295+
},
296+
}
297+
298+
_, _, err := ApplyConnectorRoutes(rc, &extcache.PolicyIndex{}, replaced, offline)
299+
require.NoError(t, err)
300+
301+
// No domain may appear more than once across the whole route config.
302+
seen := map[string]string{}
303+
for _, vh := range rc.VirtualHosts {
304+
for _, d := range vh.Domains {
305+
if prev, dup := seen[d]; dup {
306+
t.Fatalf("duplicate domain %q across vhosts %q and %q (Envoy would NACK)", d, prev, vh.Name)
307+
}
308+
seen[d] = vh.Name
309+
}
310+
}
268311
}
269312

270313
func TestApplyConnectorRoutes_Offline_Prepends503Route_NoDomain(t *testing.T) {

internal/extensionserver/server/server_test.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ func egGatewayMeta(t *testing.T, dsNS, gwName string) *corev3.Metadata {
153153
// - Listener receives Coraza filter at HCM position 0 (disabled).
154154
// - Route configuration: CONNECT route prepended, forwarding route carries
155155
// coraza typed_per_filter_config and datum-gateway metadata.
156-
// - VH domains include the connector's TargetHost (production behavior per
157-
// design §2.3 C1 — NOT a synthetic .connector.local domain).
156+
// - VH domains include a unique per-connector synthetic domain (NOT the raw
157+
// backend host, which collides across connectors and gets NACK'd).
158158
// - ALL four resource lists appear in the response.
159159
func TestPostTranslateModify_FullSnapshot(t *testing.T) {
160160
const (
@@ -367,13 +367,14 @@ func TestPostTranslateModify_FullSnapshot(t *testing.T) {
367367
assert.Equal(t, "test-tpp", entry["name"].GetStringValue())
368368
assert.Equal(t, upstreamNS, entry["namespace"].GetStringValue())
369369

370-
// --- VH domains include connector TargetHost (NOT .connector.local) ---
371-
assert.Contains(t, vh.Domains, targetHost,
372-
"VH domains must contain connector TargetHost (design §2.3 C1: use actual hostname)")
373-
for _, d := range vh.Domains {
374-
assert.False(t, strings.HasSuffix(d, ".connector.local"),
375-
"synthetic .connector.local domain must NOT appear (design §2.3 C1 fix)")
376-
}
370+
// --- VH domains carry a unique synthetic connector domain, NOT the raw
371+
// backend host. The raw host (commonly "localhost") collides across
372+
// connectors on shared route configs and gets the snapshot NACK'd; the
373+
// synthetic domain is derived from the unique VH name. See connector.go.
374+
assert.Contains(t, vh.Domains, "vh.connector.internal",
375+
"VH domains must include the unique per-connector synthetic domain")
376+
assert.NotContains(t, vh.Domains, targetHost,
377+
"raw backend host must not be appended (duplicate-domain collision source)")
377378
}
378379

379380
// TestPostTranslateModify_SecretsPassThroughUnchanged verifies that secrets are
@@ -752,10 +753,11 @@ func TestPostTranslateModify_TwoClusterTopology_DistinctReplicaUID(t *testing.T)
752753
assert.Equal(t, "replica-tpp", entry["name"].GetStringValue())
753754
assert.Equal(t, replicaNSName, entry["namespace"].GetStringValue())
754755

755-
// --- VH domains include connector TargetHost ---
756-
assert.Contains(t, vh.Domains, targetHost,
757-
"GAP-1b regression: VH domains must include connector TargetHost; "+
758-
"old code never appended it because the connector was not resolved")
756+
// --- Connector resolved: its synthetic domain is appended ---
757+
// GAP-1b regression: old code never appended any domain because the
758+
// connector was not resolved through the DStoUS mapping.
759+
assert.Contains(t, vh.Domains, "vh.connector.internal",
760+
"GAP-1b regression: connector must resolve and append its synthetic domain")
759761
}
760762

761763
// TestPostTranslateModify_OfflineConnector_503Route verifies that an offline

0 commit comments

Comments
 (0)