Skip to content

fix: resolve socket settings across listeners sharing an address and port - #9673

Open
kadircanyildirm-crypto wants to merge 4 commits into
envoyproxy:mainfrom
kadircanyildirm-crypto:fix/shared-socket-listener-settings
Open

fix: resolve socket settings across listeners sharing an address and port#9673
kadircanyildirm-crypto wants to merge 4 commits into
envoyproxy:mainfrom
kadircanyildirm-crypto:fix/shared-socket-listener-settings

Conversation

@kadircanyildirm-crypto

Copy link
Copy Markdown

What this PR does / why we need it:

Listeners that share an address and port collapse into a single xDS listener, and buildXdsTCPListener only runs for the first of them. It is the only place that writes socket_options, per_connection_buffer_limit_bytes and max_connections_to_accept_per_socket_event, and nothing patches them afterwards, so the shared socket simply keeps whatever the first listener happened to carry.

What turns that from an ordering quirk into a bug is the fallback: buildPerConnectionBufferLimitBytes and buildMaxAcceptPerSocketEvent return 32768 and 1 when a listener has no Connection at all. So a listener with no ClientTrafficPolicy attached doesn't just win a race — it actively replaces a value another listener on the same socket had configured with a default nobody asked for.

This is the first of the two changes @zhaohuabing asked for in #9652 (comment) — fix the default overwrite unconditionally, no status involved. The status message for the case where two listeners both set a value explicitly is a separate follow-up.

How it works

buildSocketSettings walks the HTTP and TCP listeners once, in translation order, and resolves the three settings per address and port before any listener is built. Each field is resolved on its own, so a listener that only sets bufferLimit doesn't stop another one from contributing maxAcceptPerSocketEvent. When more than one listener sets the same field the first one still wins, which keeps today's behaviour for configurations that were already explicit — the only sockets whose output changes are the ones that were silently getting a default.

HTTP and TCP listeners are walked together because they really can land on the same socket: getProtocolForListener maps both HTTPS and TLS passthrough to https/tls on purpose so they can share a port, but HTTPS ends up in xdsIR.HTTP and passthrough in xdsIR.TCP.

connection.limit is deliberately left out. It is a network filter and stays per filter chain, so it is genuinely unaffected by the collapse.

Testing

New golden case multiple-listeners-same-port-socket-settings: three listeners on port 10080, where the first sets nothing, the second sets the keepalive and the buffer limit, and the TCP passthrough listener sets the max accept. The socket now gets 1048576 / 64 / the keepalive socket options instead of 32768 / 1 / none.

All 273 existing xds-ir golden files are unchanged, which is the point — this only moves the cases that were previously losing their configuration.

Which issue(s) this PR fixes:

Part of #9652


PR Checklist

  • Authorship & ownership: Coding agents / AI assistants are welcome, but I have reviewed every change, understand how and why it works, can explain and maintain it, and take full responsibility for this PR. I have not submitted generated output I do not understand.
  • DCO: All commits are signed off (git commit -s). See DCO: Sign your work.
  • API agreed first: N/A: no changes under /api.
  • Required checks pass: golangci-lint, go vet and gofmt are clean, and ./internal/xds/..., ./internal/gatewayapi/... and ./internal/cmd/egctl/... pass.
  • Tests added/updated: New/changed code is covered by appropriate tests.
  • Docs: N/A: no user-facing API or configuration surface changes; the fix is in how existing settings are applied.
  • Release notes: added release-notes/current/bug_fixes/9652-shared-socket-listener-settings.md.
  • Generated files committed: N/A: no API, helm chart or module changes.
  • Scope & compatibility: The PR is reasonably scoped and preserves backward compatibility. Sockets that were already getting explicit settings are unchanged; only sockets that were silently falling back to a default now pick up the configured value.
  • Codex review: Requested a Codex review and addressed all of its comments.
  • Copilot review: Requested a Copilot review and addressed all of its comments.

@kadircanyildirm-crypto
kadircanyildirm-crypto requested a review from a team as a code owner August 5, 2026 08:55
@netlify

netlify Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deploy Preview for cerulean-figolla-1f9435 ready!

Name Link
🔨 Latest commit 4cc118f
🔍 Latest deploy log https://app.netlify.com/projects/cerulean-figolla-1f9435/deploys/6a9c453f621afa0008e79121
😎 Deploy Preview https://deploy-preview-9673--cerulean-figolla-1f9435.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e5fec26fe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread internal/xds/translator/listener.go Outdated
Comment on lines +253 to +254
if settings.maxAcceptPerSocketEvent == nil {
settings.maxAcceptPerSocketEvent = connection.MaxAcceptPerSocketEvent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat defaulted max accepts as unset when merging sockets

With Gateway API input, ClientConnection.MaxAcceptPerSocketEvent is defaulted to 1 whenever a policy contains any spec.connection (api/v1alpha1/connection_types.go:46), and buildConnection copies any non-nil value into the IR. In a shared-port socket where an earlier listener sets only connection.bufferLimit and a later listener explicitly sets maxAcceptPerSocketEvent: 64, this merge records the defaulted 1 at the earlier listener and the later explicit value can never win, so the real Gateway API path still emits the default max accept that this PR is trying to stop from shadowing configured settings.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, and it is real — I checked the generated CRD and maxAcceptPerSocketEvent does carry default: 1 (charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml:208). So any ClientTrafficPolicy with a connection block reaches the IR with MaxAcceptPerSocketEvent = 1 even when the user never wrote it, and buildConnection copies it through.

Two things are tangled here, though.

The case this PR is for — a listener with no ClientTrafficPolicy at all — is unaffected: buildConnection(nil) returns nil, so nothing is contributed and the configured listener wins. That is the case @zhaohuabing asked to fix unconditionally.

Your case is a listener whose policy sets only bufferLimit. From the IR that is indistinguishable from someone writing maxAcceptPerSocketEvent: 1 on purpose, because the defaulting happens in the API server before the controller ever sees the object. Under the rule we agreed on (when two listeners both set a field, the first wins and the status says so) it is technically correct — but it is a bad shape, since the user never typed that 1.

The clean fix is to drop +kubebuilder:default=1 from ClientConnection.MaxAcceptPerSocketEvent. buildMaxAcceptPerSocketEvent already returns 1 for nil and is the only consumer of the field, so the emitted xDS would be byte-identical while the IR regains the unset-vs-set distinction. That is a change under /api though, which the contributing guide wants agreed before implementation.

@zhaohuabing — happy to do it either way: fold it into this PR, or send it as its own small API PR and rebase this one on top. Which do you prefer?

@@ -0,0 +1 @@
Fixed Gateway listeners that share an address and port silently losing their client connection settings. Those listeners collapse into a single xDS listener, and the TCP keepalive, connection buffer limit and max accept per socket event were taken from whichever listener happened to be translated first, so a listener without a ClientTrafficPolicy would replace the values configured on another listener on the same socket with the hardcoded defaults. These settings are now resolved across every listener on the socket, and a listener that leaves one unset no longer overrides a listener that sets it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add a breaking-change note for the xDS change

This fragment is under bug_fixes/, but the change intentionally modifies generated xDS listener content for existing configurations, such as perConnectionBufferLimitBytes, maxConnectionsToAcceptPerSocketEvent, and socketOptions on shared listeners. EnvoyPatchPolicies and extension servers can target those fields, so this needs a breaking-change fragment as well; otherwise users relying on patches or hooks will not see the compatibility warning in the generated release notes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I looked at how breaking_changes/ has been used and I don't think this one qualifies, but I did make the note explicit — please tell me if you disagree.

The v1.8.3 breaking entry is the Lua one, where the filter names and config layout moved, so patches keyed on envoy.filters.http.lua/<index> stopped matching. Here nothing structural moves: the same three fields stay on the same listener, only their values change on sockets that were previously being handed a default. A JSON patch or extension hook pointing at per_connection_buffer_limit_bytes matches exactly as before.

It is also the change @zhaohuabing explicitly asked to make unconditionally, in contrast with rejecting the config, which he did call out as the breaking option.

That said, the value change is observable, so I've spelled it out in the bug-fix fragment rather than leaving it implied — it now names the three fields and says the names and layout are unchanged. Happy to add a breaking_changes/ fragment too if a maintainer reads it the other way.

@kadircanyildirm-crypto
kadircanyildirm-crypto force-pushed the fix/shared-socket-listener-settings branch from a615cf9 to b49afd4 Compare August 5, 2026 09:09
@HusseinKabbout

Copy link
Copy Markdown
Contributor

I took a quick peek and one issue (besides that obvious 100% AI copy-paste) I saw, is that this PR is specific to socket settings but it should be about any setting that is listener specific. Maybe @zhaohuabing has a different opinion / view on this matter but we need to make sure that all settings from https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener.proto#config-listener-v3-listener that are accounted for. EG probably does not expose all of them but we need to find a way to make sure that the behaviour is the same for all of them.

@kadircanyildirm-crypto

Copy link
Copy Markdown
Author

Fair point on the scope. Here is what I found when I went through the Listener proto against what the translator actually writes.

EG only sets five things at the listener level today:

  • socket_options, from tcpKeepalive
  • per_connection_buffer_limit_bytes, from connection.bufferLimit
  • max_connections_to_accept_per_socket_event, from connection.maxAcceptPerSocketEvent
  • listener_filters: proxy_protocol and tls_inspector
  • address.socket_address.ipv4_compat, from the EnvoyProxy IPFamily

The first three are what this PR covers. access_log is also listener level but comes from the proxy-wide config, so it is the same for every listener on the socket.

The rest of the proto is not plumbed at all right now: enable_reuse_port, connection_balance_config, listener_filters_timeout, traffic_direction, additional_addresses, drain_type, tcp_backlog_size, freebind. Nothing to make consistent there yet. connectionInspectionTimeout from #9315 maps to listener_filters_timeout and will need the same treatment once it lands.

That leaves the two listener filters, and they both break in a different way than the value fields:

proxy_protocol. patchProxyProtocolFilter returns early if the filter is already present, so it is additive. Whoever enables it wins and nobody else on that socket can turn it off. This is the TODO from #3337. It fails in the opposite direction from the three value fields, so folding it into the same merge would be wrong.

tls_inspector fingerprints. Same early-return shape, and I do not think this one has been reported anywhere. addXdsTLSInspectorFilter bails out when the filter already exists and throws away the fingerprints it was handed. addServerNamesMatch passes the listener's JA3/JA4 config, addXdsTCPFilterChain passes nil. So if a listener that does not want fingerprinting installs the inspector first, another listener's enableJA3Fingerprinting on the same socket is dropped silently.

On ipv4_compat: it comes from getEnvoyIPFamily(gateway.envoyProxy), so it is the same for every listener behind one proxy. I do not think it can diverge, but I have not traced every mergeGateways path and would rather someone who knows those better confirm it.

So there is a real gap, but it is two listener filters rather than a long tail of settings, and they need different handling and different status wording than the value fields.

@zhaohuabing happy to go either way: widen this PR to cover the filters as well, or land the three value fields here and do the filters as their own PR. Which do you prefer?

@kadircanyildirm-crypto

kadircanyildirm-crypto commented Aug 5, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look, and sorry — that is fair feedback. AI does speed my work up a lot, but you are right that it shows in the writing. I will be more careful with it from here and keep these in my own words.

@HusseinKabbout

Copy link
Copy Markdown
Contributor

Thanks for taking a look, and sorry — that is fair feedback. AI does speed my work up a lot, but you are right that it shows in the writing. I will be more careful with it from here and keep these in my own words.

Maybe my comment about AI sounded harsher than intended. What I meant is that since the PR description, PR comments and proposed changes in the PR sounded all like AI, it contains a lot of information that should be validated and can be tiresome to read (wall of text).

@kadircanyildirm-crypto

Copy link
Copy Markdown
Author

No worries, that is a fair read. I will keep them short.

@kadircanyildirm-crypto
kadircanyildirm-crypto force-pushed the fix/shared-socket-listener-settings branch from b49afd4 to 7935a7c Compare August 13, 2026 09:11
@kadircanyildirm-crypto

Copy link
Copy Markdown
Author

@guydc thanks, pushed the doc work you asked for.

The four affected fields — tcpKeepalive, connection.bufferLimit, connection.maxAcceptPerSocketEvent and timeout.tcp.connectionInspectionTimeout — now say in their API docs that they belong to the shared listener socket and that the first listener wins when more than one sets them. The ClientTrafficPolicy task page has a new section listing them together with the same rule.

I also folded connectionInspectionTimeout into the fix. It landed on main after this PR was opened (#9315) and is written on the same socket, so it had the same problem.

The filter chain case @HusseinKabbout raised is a layer down and is still open; I left it on #9652 rather than growing this PR.

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.45455% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.27%. Comparing base (f5fb24b) to head (7935a7c).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
internal/xds/translator/listener.go 94.44% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9673      +/-   ##
==========================================
+ Coverage   76.22%   76.27%   +0.04%     
==========================================
  Files         261      261              
  Lines       44146    44228      +82     
==========================================
+ Hits        33650    33733      +83     
+ Misses       8266     8265       -1     
  Partials     2230     2230              

☔ View full report in Codecov by Harness.
📢 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.

@kadircanyildirm-crypto

Copy link
Copy Markdown
Author

/retest

@zhaohuabing

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 7935a7ce9b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Signed-off-by hint: the field names and layout do not change, only the
values on the sockets that were previously getting a default.

Signed-off-by: kadircanyildirm-crypto <kadir.can.yildirm@gmail.com>
Resolve the connection inspection timeout across listeners on the same
socket as well, and call out in the API docs and the ClientTrafficPolicy
task page that these four fields belong to the shared listener socket.

Signed-off-by: Kadir Can Yildirim <252162627+kadircanyildirm-crypto@users.noreply.github.com>
Signed-off-by: Kadir Can Yildirim <252162627+kadircanyildirm-crypto@users.noreply.github.com>
main no longer emits initialFetchTimeout: 0s in the RDS config source, so
the expected listeners output for the shared socket settings test case
had to be regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4riP5modRF9PLRwTWKeUN
Signed-off-by: Kadir Can Yildirim <252162627+kadircanyildirm-crypto@users.noreply.github.com>
@kadircanyildirm-crypto
kadircanyildirm-crypto force-pushed the fix/shared-socket-listener-settings branch from 7935a7c to 4cc118f Compare September 5, 2026 16:37
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.

3 participants