Skip to content

fix(server)!: reject a wildcard bind with no advertised address - #3923

Open
chengxilo wants to merge 2 commits into
apache:masterfrom
chengxilo:fix-unreachable-roster-candidates
Open

fix(server)!: reject a wildcard bind with no advertised address#3923
chengxilo wants to merge 2 commits into
apache:masterfrom
chengxilo:fix-unreachable-roster-candidates

Conversation

@chengxilo

@chengxilo chengxilo commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR address?

Closes #3890

Rationale

A wildcard bind says which interfaces a node accepts on, not where a client reaches it, so publishing it as an address hands clients a target they cannot dial.

What changed?

node.advertised_address supplies it, and the server now refuses to start when server is configured with wildcard bind while leaves it unset.

Breaking:

deployments binding 0.0.0.0 without a roster must declare an address. The Helm chart can derives it from the Service DNS name and the shipped compose files name their service; anything else needs IGGY_NODE_ADVERTISED_ADDRESS. A cluster.nodes ip must now be a literal IP, and no declared address may be the unspecified one.

Some detail regarding new behavior:

When would it boot?

Standalone (cluster.enabled = false)

tcp.address node.advertised_address Boot Metadata publishes
127.0.0.1:8090 unset 127.0.0.1 (derived from the bind)
127.0.0.1:8090 broker.example.com broker.example.com (declared wins)
0.0.0.0:8090 unset rejected
0.0.0.0:8090 broker.example.com broker.example.com
any 0.0.0.0 / :: rejected
any broker:8090 (carries a port) rejected

Cluster (cluster.enabled = true)

tcp.address only picks the bind interface here — ports come from the roster — so a wildcard is perfectly
normal.

Roster field Value Boot
nodes[].ip 172.28.0.101
nodes[].ip 0.0.0.0 / :: rejected
nodes[].ip iggy-server (any hostname) rejected, points at advertised_address
advertised_address unset ✅ → publishes ip
advertised_address broker.example.com ✅ → publishes it
advertised_address 0.0.0.0 / :: rejected
selector address 0.0.0.0 / :: rejected, error names the CIDR
node.advertised_address set ✅ but ignored, warns at startup

Which address a client is told (advertised_for):

selector matching the client's source IP (longest prefix) → advertised_address → ip

Both modes

tcp.address Boot
:8090 (empty host) rejected — Rust's SocketAddr has no such spelling
localhost:8090 (hostname) rejected — a literal IP is required
anything else that does not parse rejected, one message naming the fix

Edge cases

Situation Behavior
cluster.enabled = false with [[cluster.nodes]] left behind Roster is neither resolved nor validated
boots even if an ip is a hostname
A selector whose CIDR does not parse ❌ rejected (previously dropped in silence)

That last row is a side fix: a malformed selector used to be swallowed, surfacing only as "clients on one network
get the catch-all address" with nothing to debug.

Local Execution

  • Passed
  • Pre-commit hooks ran

AI Usage

  1. Claude Code (Opus)
  2. Diagnosis and implementation
  3. Reviewed line by line with my best effort. BUT I am not very familiar with the server side code, so I am not sure if these changes introduce any side-effect that I didn't notice. According to the changed part, I think it looks ok.
  4. Yes

@github-actions

Copy link
Copy Markdown

Thanks for the PR. It is labeled S-waiting-on-review and queued for review.

Slash commands (own line, regular comment) move it around the queue:

  • /ready - back to S-waiting-on-review after addressing feedback
  • /author - flip to S-waiting-on-author while you finish changes
  • /request-review @user-or-team - request a reviewer

See CONTRIBUTING.md for details.

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Aug 19, 2026
Closes apache#3890

A server whose TCP listener binds a wildcard reported that wildcard as its own
client-facing address in GetClusterMetadata. SDKs collect roster addresses into
their reconnect candidates, so 0.0.0.0:8090 became a dial target that retry
would eventually pick and never reach.

A bind address answers which interfaces a node accepts on. It is not an answer
to where a client reaches it, and for the unspecified address the two have no
relation. With a roster each node already answers the second question through
cluster.nodes.advertised_address; without one the server had no way to be told
it at all. node.advertised_address supplies it, named to match its roster
counterpart, and the server now refuses to start when a wildcard bind leaves
the question unanswered. A concrete bind address still needs no declaration: it
already names an interface a client can reach.

Only what an operator declares is validated. A bind address is never held to
being routable, which is the mistake that made Kafka reject wildcard binds that
had always been valid (KAFKA-18281). The declared values are held to it in both
modes, so a roster ip, advertised address or per-network selector that names
the unspecified address stops the boot rather than the cluster: peers dialing
0.0.0.0 reach their own host, which is how a cluster comes up with every node
believing it is alone while its containers report healthy.

Resolution is now infallible past construction. A roster node is built through
TryFrom, so an address that does not parse fails there instead of leaving every
consumer to carry a fallback, and the fallbacks are gone: metadata no longer
publishes a raw unparsed string, forwarding no longer treats a missing replica
ip as "no target", and a selector whose CIDR does not parse is no longer
dropped in silence. The two listeners also resolve the self address once
between them, rather than each deriving it from its own bind address and
disagreeing whenever http.address and tcp.address differ.

BREAKING CHANGE: a server that binds a wildcard address without a roster now
refuses to start until node.advertised_address names where clients reach it.
Deployments that bind 0.0.0.0 must declare one: the Helm chart derives it from
the Service DNS name and the shipped compose files name their service, but an
external deployment needs the hostname or load balancer address its clients
dial. A cluster.nodes ip must now be a literal IP, and no declared address may
be the unspecified one.
@chengxilo
chengxilo force-pushed the fix-unreachable-roster-candidates branch from 60f1345 to 2b6fc1a Compare August 19, 2026 05:11
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.25092% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.82%. Comparing base (ab257a0) to head (7f78459).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
core/configs/src/server_config/cluster.rs 86.04% 9 Missing and 3 partials ⚠️
core/server/src/bootstrap.rs 73.33% 2 Missing and 2 partials ⚠️
core/configs/src/server_config/validators.rs 93.75% 2 Missing and 1 partial ⚠️
core/configs/src/server_config/node.rs 97.14% 0 Missing and 1 partial ⚠️
core/server/src/http.rs 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master    #3923       +/-   ##
=============================================
- Coverage     83.82%   68.82%   -15.01%     
  Complexity     1358     1358               
=============================================
  Files          1212     1212               
  Lines        166148   151623    -14525     
  Branches     133622   119224    -14398     
=============================================
- Hits         139272   104352    -34920     
- Misses        23253    43712    +20459     
+ Partials       3623     3559       -64     
Components Coverage Δ
Rust Core 67.85% <92.25%> (-16.77%) ⬇️
Java SDK 66.67% <ø> (ø)
C# SDK 40.53% <ø> (-35.55%) ⬇️
Python SDK 90.00% <ø> (ø)
PHP SDK 84.48% <ø> (ø)
Node SDK 95.94% <ø> (+0.09%) ⬆️
Go SDK 68.32% <ø> (ø)
Files with missing lines Coverage Δ
core/configs/src/server_config/defaults.rs 100.00% <100.00%> (ø)
core/configs/src/server_config/server.rs 85.48% <ø> (ø)
core/server/src/cluster_meta.rs 99.50% <100.00%> (+0.89%) ⬆️
core/server/src/dispatch.rs 89.39% <100.00%> (-0.07%) ⬇️
core/server/src/http/error.rs 86.96% <100.00%> (+0.48%) ⬆️
core/configs/src/server_config/node.rs 97.14% <97.14%> (ø)
core/server/src/http.rs 92.73% <66.66%> (-0.38%) ⬇️
core/configs/src/server_config/validators.rs 91.94% <93.75%> (+0.18%) ⬆️
core/server/src/bootstrap.rs 80.70% <73.33%> (-0.19%) ⬇️
core/configs/src/server_config/cluster.rs 98.16% <86.04%> (-0.54%) ⬇️

... and 333 files with indirect coverage changes

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review PR is waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server publishes its wildcard bind address as a client-facing address

1 participant