Skip to content

fix(tcp): add SNI-based filter chain matching for TLS passthrough empty routes#8521

Merged
arkodg merged 5 commits intoenvoyproxy:mainfrom
OliverBailey:fix/tcp-tls-passthrough-empty-routes
Mar 23, 2026
Merged

fix(tcp): add SNI-based filter chain matching for TLS passthrough empty routes#8521
arkodg merged 5 commits intoenvoyproxy:mainfrom
OliverBailey:fix/tcp-tls-passthrough-empty-routes

Conversation

@OliverBailey
Copy link
Copy Markdown
Contributor

@OliverBailey OliverBailey commented Mar 13, 2026

Description

Fixes the bug where multiple TCP/TLS listeners on the same port without any attached Routes would cause Envoy to reject the entire listener configuration with:

filter chain 'EmptyCluster' has the same matching rules defined as 'EmptyCluster'. duplicate matcher is: {}

The root cause: when multiple IR listeners share the same address:port (mapping to a single xDS listener), the old code called addXdsTCPFilterChain once per IR listener, adding identical EmptyCluster filter chains to the same xDS listener — which Envoy considers invalid duplicate matchers.

Changes

internal/xds/translator/translator.go

  • Introduce emptyFilterChainAdded := make(map[string]bool) keyed by xDS listener name
  • The EmptyCluster filter chain is now added at most once per xDS listener, regardless of how many IR listeners share the same port
  • The EmptyCluster route object is also created once and reused (sharedEmptyTCPRoute)

Test coverage

  • Added tcp-multiple-tls-passthrough-no-routes test: 3 IR listeners on the same port with no routes → single EmptyCluster cluster, single filter chain on the xDS listener

After this fix

filterChains:
- name: EmptyCluster
  filters: [...]   # exactly one, regardless of how many IR listeners share the port

Closes

Closes #7866

Checklist

  • Code compiles correctly
  • All unit tests pass (go test ./internal/gatewayapi/... ./internal/xds/translator/... ./internal/ir/...)
  • Test coverage added for the exact failure scenario from the issue

@OliverBailey OliverBailey requested a review from a team as a code owner March 13, 2026 12:07
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 13, 2026

Deploy Preview for cerulean-figolla-1f9435 canceled.

Name Link
🔨 Latest commit ff7d3e3
🔍 Latest deploy log https://app.netlify.com/projects/cerulean-figolla-1f9435/deploys/69b6e19b226e780008a4dbd2

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 13, 2026

Codecov Report

❌ Patch coverage is 89.47368% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.20%. Comparing base (1de3156) to head (ff7d3e3).
⚠️ Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
internal/xds/translator/translator.go 89.47% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8521      +/-   ##
==========================================
+ Coverage   74.16%   74.20%   +0.04%     
==========================================
  Files         242      242              
  Lines       37603    37614      +11     
==========================================
+ Hits        27889    27913      +24     
+ Misses       7773     7763      -10     
+ Partials     1941     1938       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@OliverBailey OliverBailey force-pushed the fix/tcp-tls-passthrough-empty-routes branch from 371856b to 3a68ca2 Compare March 13, 2026 13:48
Comment thread internal/xds/translator/translator.go Outdated
@OliverBailey OliverBailey requested a review from stekole March 13, 2026 17:23
@arkodg arkodg requested review from sudiptob2 and removed request for stekole March 14, 2026 22:11
@arkodg arkodg added this to the v1.8.0-rc.1 Release milestone Mar 14, 2026
Comment thread internal/ir/xds.go Outdated
@OliverBailey OliverBailey requested a review from arkodg March 15, 2026 00:04
@arkodg
Copy link
Copy Markdown
Contributor

arkodg commented Mar 15, 2026

hey @OliverBailey can you sign your commit, and force push to fix DCO ?

Comment thread internal/xds/translator/translator.go Outdated
// Only add the filter chain once per Envoy listener; multiple IR listeners may share
// the same address/port and map to the same xDS listener.
filterChainExists := false
for _, fc := range xdsListener.FilterChains {
Copy link
Copy Markdown
Contributor

@arkodg arkodg Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the for loop ?
could we get by with a bool like isEmptyClusterSet and for the first pass we could create it, but for the second pass, we'd avoid it ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this approach. Only track the xDS listeners that already have the empty filter chain. It may need to be a map to allow for listeners on different ports.
emptyFilterChainAdded := make(map[string]bool)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the for-loop with emptyFilterChainAdded := make(map[string]bool) keyed by xDS listener name, per stekole's suggestion below. No loop needed — we just check/set the map entry once per xDS listener.

Comment thread internal/gatewayapi/listener.go Outdated
}
// Set Passthrough flag for TLS passthrough mode
if listener.Protocol == gwapiv1.TLSProtocolType && listener.TLS != nil && listener.TLS.Mode != nil {
irListener.Passthrough = *listener.TLS.Mode == gwapiv1.TLSModePassthrough
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets the passthrough but does anything read it?

Copy link
Copy Markdown
Contributor Author

@OliverBailey OliverBailey Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing read it; you're right.
After the translator was simplified to use a single EmptyCluster for all empty-route listeners regardless of mode, the Passthrough field became dead code.
Removed the field from TCPListener, the setter in listener.go, and all testdata references.

TLS passthrough routing is unaffected: SNI matching still works via TLSInspectorConfig.SNIs on each TCPRoute.

@OliverBailey OliverBailey force-pushed the fix/tcp-tls-passthrough-empty-routes branch from acf0dd8 to 12302aa Compare March 15, 2026 16:40
…ty routes

This fix addresses the issue where multiple TLS passthrough listeners on the
same port without routes would generate duplicate empty filter chains without
unique matching rules, causing Envoy to reject the configuration with:
"filter chain 'EmptyCluster' has the same matching rules defined as 'EmptyCluster'"

Changes:
- Add Hostnames field to TCPListener IR to capture SNI values from Gateway API
- Populate hostnames from Gateway API listener.Hostname for TLS protocol
- Detect TLS passthrough by presence of hostnames (SNI matching required)
- Create unique EmptyCluster per TLS passthrough listener with SNI-based matching
- Add TLSInspectorConfig with SNIs to enable filterChainMatch generation
- Add test coverage for multiple TLS passthrough listeners without routes

This ensures each TLS passthrough listener gets a unique filter chain with
serverNames matching, allowing Envoy to properly route based on SNI.

Builds on work from envoyproxy#7912 by @Aditya7880900936 which addressed the unique
cluster naming issue. This PR extends it to properly implement SNI-based
filter chain matching as noted in the review comments by @sudiptob2.

Fixes envoyproxy#7912

Signed-off-by: Oliver Bailey <github@obailey.co.uk>
- Fix indentation in tcp-multiple-tls-passthrough-no-routes.yaml
- List items should not be indented per project yamllint config

Signed-off-by: Oliver Bailey <github@obailey.co.uk>
Address review feedback by decoupling TLS passthrough detection from
hostname presence. Using len(hostnames) > 0 as a signal for passthrough
was fragile and could misidentify:
- TLS termination listeners with hostnames as passthrough (false positive)
- Catch-all passthrough listeners without hostnames (false negative)

Changes:
- Add explicit Passthrough bool field to TCPListener IR
- Set Passthrough from Gateway API listener.TLS.Mode == Passthrough
- Update translator to check tcpListener.Passthrough instead of len(hostnames)
- Update test data to include passthrough field in test fixtures

This makes the code more maintainable and self-documenting, with clear
separation between passthrough mode and SNI hostname configuration.

Signed-off-by: Oliver Bailey <github@obailey.co.uk>
… single EmptyCluster

- Remove Hostnames []string field from TCPListener IR; SNI is already
  captured in TLSInspectorConfig.SNIs on each TCPRoute (per arkodg)
- Replace Hostnames-based passthrough detection with the existing
  explicit Passthrough bool field throughout
- Empty-route listeners (passthrough and non-passthrough) now share a
  single EmptyCluster instead of creating per-listener duplicates
  (per arkodg)
- Update all affected testdata to reflect the above changes

Signed-off-by: Oliver Bailey <github@obailey.co.uk>
… emptyFilterChainAdded map

- Remove Passthrough bool from TCPListener IR - it was set in listener.go
  but never read by the translator (per stekole)
- Remove corresponding setter in listener.go and passthrough: true from
  testdata output files
- Replace per-listener for-loop filter chain existence check with a
  map[string]bool keyed by xDS listener name (per arkodg + stekole)
- TLS passthrough routing is unaffected: SNI matching still works via
  TLSInspectorConfig.SNIs on each TCPRoute

Signed-off-by: Oliver Bailey <github@obailey.co.uk>
@OliverBailey OliverBailey force-pushed the fix/tcp-tls-passthrough-empty-routes branch from 12302aa to ff7d3e3 Compare March 15, 2026 16:43
@OliverBailey OliverBailey requested review from arkodg and stekole March 15, 2026 16:44
@OliverBailey
Copy link
Copy Markdown
Contributor Author

hey @OliverBailey can you sign your commit, and force push to fix DCO ?

Apologies, completely forgot for that specific commit. Done, and re-signed the commits and pushed back up.

@arkodg arkodg requested review from a team and removed request for stekole March 16, 2026 03:30
@arkodg arkodg requested a review from a team March 16, 2026 03:30
Copy link
Copy Markdown
Contributor

@jukie jukie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks!

@jukie
Copy link
Copy Markdown
Contributor

jukie commented Mar 19, 2026

/retest

@arkodg arkodg merged commit c11f0c2 into envoyproxy:main Mar 23, 2026
75 of 82 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gateway generates invalid Envoy config with multiple EmptyCluster Routes

4 participants