Skip to content

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

Open
chengxilo wants to merge 15 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 15 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

This PR block #3650, see #3890 for detail.

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 93.51032% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.27%. Comparing base (37d7eee) to head (547fa59).

Files with missing lines Patch % Lines
core/configs/src/server_config/cluster.rs 90.24% 9 Missing and 3 partials ⚠️
core/server/src/bootstrap.rs 87.09% 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/cluster_meta.rs 98.80% 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     84.03%   71.27%   -12.76%     
  Complexity     1358     1358               
=============================================
  Files          1215     1215               
  Lines        170820   156068    -14752     
  Branches     138576   123826    -14750     
=============================================
- Hits         143547   111242    -32305     
- Misses        23411    40992    +17581     
+ Partials       3862     3834       -28     
Components Coverage Δ
Rust Core 68.86% <93.51%> (-16.07%) ⬇️
Java SDK 66.67% <ø> (ø)
C# SDK 74.96% <ø> (ø)
Python SDK 90.10% <ø> (ø)
PHP SDK 85.65% <ø> (ø)
Node SDK 93.96% <ø> (-1.85%) ⬇️
Go SDK 68.29% <ø> (ø)
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/dispatch.rs 89.43% <100.00%> (+<0.01%) ⬆️
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/cluster_meta.rs 99.53% <98.80%> (+0.93%) ⬆️
core/server/src/http.rs 92.75% <66.66%> (-0.38%) ⬇️
core/configs/src/server_config/validators.rs 92.23% <93.75%> (+0.15%) ⬆️
core/server/src/bootstrap.rs 79.56% <87.09%> (-0.01%) ⬇️
core/configs/src/server_config/cluster.rs 98.22% <90.24%> (-0.49%) ⬇️

... and 254 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.

@mmodzelewski mmodzelewski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The core logic is sound, there are a few things worth fixing before merge, though. Besides the comments in-line, here are the ones that don't have a direct reference in the changed code:

  • docker-compose.yml (root) — builds the Dockerfile (wildcard binds at Dockerfile:45-48), has no environment: block, and cluster is off by default, so the README-documented docker compose up quickstart now refuses boot. Needs IGGY_NODE_ADVERTISED_ADDRESS=localhost like the eight other compose files patched in this PR.
  • foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java:92 — the testcontainer sets IGGY_TCP_ADDRESS=0.0.0.0:8090 with no advertised var; it will refuse boot once apache/iggy:edge ships this change. The C# fixture got the fix (VsrCluster.cs:294); the Java one was missed. Dormant today only because CI takes the USE_EXTERNAL_SERVER path.
  • core/server/src/args.rs:57 — the --help example IGGY_TCP_ADDRESS=0.0.0.0:8090 is now boot-refusing when copy-pasted standalone; the READMEs were updated but the help text was missed.

Comment thread core/configs/src/server_config/server.rs
if let Some(http_addr) = topology.http_listen_addr {
// One host for all four transports, resolved from the client-facing
// TCP bind so both listeners publish the same node address.
let self_advertised = self_advertised_address(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This changes the source of the HTTP metadata self address: before this PR, http.rs published the HTTP listener's own bound address (self_ip: bound_addr.ip().to_string()); now it derives from the TCP bind. With the shipped default tcp.address = 127.0.0.1:8090 and HTTP bound to a concrete external interface, the published address silently flips from the external IP to 127.0.0.1 - no boot refusal, no warning. Consider warning on (or validating) the interface mismatch between the TCP and HTTP binds.

Comment thread core/configs/src/server_config/cluster.rs
};
let replica_ip = config.ip.parse().ok();
Self {
impl TryFrom<ClusterNodeConfig> for ResolvedClusterNode {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TryFrom checks only that the sources parse; it doesn't reject unspecified addresses the way the validator does for roster ip, catch-all, and selectors. The "always dialable" invariant therefore holds only because validation runs before conversion - a direct TryFrom caller can still build a node advertising 0.0.0.0. Mirroring the unspecified rejection here (on the canonical form) would make the type self-defending.

Comment thread core/server/src/cluster_meta.rs
Comment thread helm/charts/iggy/templates/deployment.yaml
@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants