Skip to content

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

Open
kadircanyildirm-crypto wants to merge 1 commit 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 1 commit 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 b49afd4
🔍 Latest deploy log https://app.netlify.com/projects/cerulean-figolla-1f9435/deploys/6a72fdc103788b000846f6dc
😎 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 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.

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>
@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.

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.

2 participants