Skip to content

fix(l1): announce local IP when --p2p.addr is unspecified#6713

Open
edg-l wants to merge 1 commit into
mainfrom
fix/p2p-announce-local-ip-on-unspecified-bind
Open

fix(l1): announce local IP when --p2p.addr is unspecified#6713
edg-l wants to merge 1 commit into
mainfrom
fix/p2p-announce-local-ip-on-unspecified-bind

Conversation

@edg-l
Copy link
Copy Markdown
Contributor

@edg-l edg-l commented May 22, 2026

Summary

When ethrex is launched with --p2p.addr=0.0.0.0 (or ::) and no --nat.extip, the announce/enode/ENR address ended up as the bind address verbatim, i.e. enode://…@0.0.0.0:30303. Peers cannot dial back to that, so the node sits at 0 inbound peers. Spotted on bal-devnet-7 (the deployment passes --p2p.addr=0.0.0.0 without --nat.extip).

This patch:

  • Factors the address-resolution logic out of get_local_p2p_node into a small pure function resolve_p2p_endpoints, so it can be unit-tested.
  • When --p2p.addr is given and resolves to an unspecified address, the announced address now falls back to the auto-detected local IP (the same local_ip() / local_ipv6() call the no-flag branch already used). --nat.extip still overrides; specific bind addresses are unchanged.
  • If no local IP can be detected, emits a loud warning rather than silently advertising the unspecified address.

This is the minimal fix. A follow-up could implement devp2p endpoint prediction (learning the external IP from the to field on incoming PINGs/PONGs, à la geth's UDPEndpointStatement), which would remove the need for operators to set --nat.extip at all on cloud hosts. Out of scope for this PR.

Test plan

  • cargo fmt -p ethrex
  • cargo clippy -p ethrex --all-targets -- -D warnings
  • cargo test -p ethrex --lib initializers:: — 6 new unit tests pass
  • Deploy onto a bal-devnet ethrex node with --p2p.addr=0.0.0.0 and confirm admin_nodeInfo reports the host IP in the enode/ENR and inbound peers > 0.

When `--p2p.addr=0.0.0.0` (or `::`) was passed without `--nat.extip`,
ethrex used the bind address as the externally-announced address,
ending up with `enode://...@0.0.0.0:30303` in the ENR. Peers can't dial
back to that, so the node had 0 inbound peers.

Now the announce address falls back to the auto-detected local IP when
the bind is unspecified; `--nat.extip` still overrides. If no local IP
is detectable, log a loud warning instead of silently advertising the
unspecified address.
@edg-l edg-l requested a review from a team as a code owner May 22, 2026 10:59
@github-actions
Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Known Issues

Tests intentionally excluded from CI. Source of truth for the Known
Issues
section the L1 workflow appends to each ef-tests job summary
and posts as a sticky PR comment.

EF Tests — Stateless coverage narrowed to EIP-8025 optional-proofs

make -C tooling/ef_tests/blockchain test calls test-stateless-zkevm
instead of test-stateless. The zkevm@v0.3.3 fixtures are filled against
bal@v5.6.1, out of sync with current bal spec; the broad target trips ~549
fixtures. Re-broaden once the zkevm bundle is regenerated.

Why and resolution path

PR #6527 broadened
test-stateless to extract the entire for_amsterdam/ tree from the
zkevm bundle and run all of it under --features stateless; combined with
this branch's bal-devnet-7 semantics that scope produces ~549
GasUsedMismatch / ReceiptsRootMismatch /
BlockAccessListHashMismatch failures.

test-stateless-zkevm filters cargo to the eip8025_optional_proofs
suite, which still validates the stateless harness without the bal-version
mismatch.

Re-broaden by switching test: back to test-stateless in
tooling/ef_tests/blockchain/Makefile once the zkevm bundle is regenerated
against the current bal spec.

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Here is my review of PR #6713:


PR Review: fix(l1): announce local IP when --p2p.addr is unspecified

Overview

This PR fixes a real connectivity bug: nodes launched with --p2p.addr=0.0.0.0 (no --nat.extip) would advertise 0.0.0.0 in their ENR/enode, making them unreachable for inbound connections. The fix introduces resolve_p2p_endpoints, a pure helper that encapsulates address-resolution logic and can be unit-tested in isolation.

The approach is correct and the refactor is clean. A few things worth addressing:


Correctness

IPv6 bind → IPv4 fallback is semantically ambiguous

In cmd/ethrex/initializers.rs, the unspecified-address branch at ~line 408:

let external = if bind.is_ipv6() {
    local_v6.or(local_v4)
} else {
    local_v4.or(local_v6)
};

If --p2p.addr=:: and the host has no global IPv6 address (only IPv4), the node binds on :: but announces an IPv4 address in its ENR. A peer receiving this ENR would try to dial the IPv4 address, but the node is listening on :: (dual-stack). This usually works on Linux with dual-stack sockets, but it is surprising and not explicitly documented. The warning message also doesn't mention this cross-family fallback, so operators won't know it happened.

Unconditional local_ip() / local_ipv6() calls

Previously these were only called in the (None, None) branch. Now both are called unconditionally at the call site in get_local_p2p_node:

local_ip().ok(),
local_ipv6().ok(),

This is a startup-only cost, so it's not a serious concern, but it does change when the OS network interface probing happens. A node started with --p2p.addr=1.2.3.4 --nat.extip=5.6.7.8 (no local IP needed at all) now performs two interface lookups it will never use. A lazy alternative — calling these only inside the (Some(addr), None) / (None, None) branches — would preserve the original semantics without complicating the test interface (you could still inject them via closures). Low priority.


Test Coverage

Missing IPv6 unspecified test cases

The test suite has no test for --p2p.addr=::. The code takes a distinct branch (bind.is_ipv6()) for this case. Three scenarios are missing:

  • :: bind with a local v6 address available → should announce v6.
  • :: bind with no v6 but a v4 available → the cross-family fallback case described above.
  • :: bind with no local IPs → should fall back to :: and warn.

These would also serve as regression protection for the IPv6-unspecified path.


Minor / Inherited Issues

.expect() and assert! on user-supplied input

Several panics exist for bad CLI input that predate this PR (e.g., extip.parse().expect("Failed to parse --nat.extip address"), the address-family assert!). They are not introduced here, but they are preserved in the refactored function. These will produce a Rust panic backtrace rather than a clean user-facing error message. This is out of scope for this fix but worth a follow-up to replace with proper error returns.


What's Good

  • Extracting resolve_p2p_endpoints as a pure function is the right call — it's easy to read, easy to test, and removes the implicit dependency on local_ip() / local_ipv6() from the logic itself.
  • The 6 existing tests cover the primary cases clearly and without unnecessary setup.
  • The warn! for the undetectable-IP case is the correct response rather than silently advertising a broken address.
  • The doc comment on resolve_p2p_endpoints accurately describes all three precedence rules.
  • The fix itself is minimal and does not change behavior for nodes using specific bind addresses or --nat.extip.

Summary

The core fix is correct and the refactor is solid. The main actionable gaps are:

  1. Add tests for the --p2p.addr=:: (IPv6 unspecified) paths, especially the cross-family fallback to v4.
  2. Consider documenting (at minimum, in the warn! message) that an IPv4 address was used as the external address when binding on ::.

Automated review by Claude (Anthropic) · sonnet · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

The PR refactors P2P endpoint resolution to prevent nodes from advertising 0.0.0.0 or :: in their ENR, which would make them unreachable for inbound connections. This is a solid usability improvement.

Code Quality & Correctness

  1. Logic Correctness (Lines 372-430): The precedence rules in resolve_p2p_endpoints are implemented correctly:

    • --nat.extip always wins for the external address.
    • When --p2p.addr is unspecified, the code correctly falls back to auto-detected local IPs.
    • The family-matching assertion for --nat.extip (lines 383-389) prevents misconfigurations.
  2. Address Family Fallback (Lines 405-409): The logic prefers the matching IP family (v6 for v6, v4 for v4) but falls back to the other family if unavailable. This is reasonable defensive programming, though consider if advertising IPv6 when bound to 0.0.0.0 (or vice versa) could cause connectivity issues in strict firewall environments.

  3. Panic vs Error Handling: The function still uses .expect() for CLI argument parsing (e.g., lines 379, 380). While consistent with the existing codebase style, be aware that invalid inputs will crash the node at startup rather than returning a clean error. This is acceptable for initialization code but worth noting.

Testing

The unit tests cover the main scenarios well. Consider adding coverage for:

  • IPv6 unspecified address (::) to verify the local_v6.or(local_v4) fallback.
  • IPv6 unspecified with only IPv4 local available to verify cross-family fallback behavior.

Example test to add:

#[test]
fn p2p_addr_unspecified_v6_prefers_v6() {
    let local_v6 = ip("fe80::1");
    let local_v4 = ip("10.0.0.5");
    let (bind, ext) = resolve_p2p_endpoints(Some("::"), None, Some(local_v4), Some(local_v6));
    assert_eq!(bind, ip("::"));
    assert_eq!(ext, local_v6); // Should prefer IPv6 when bind is IPv6
}

Documentation & Style

  • The doc comment (lines 362-372) clearly explains the precedence rules.
  • The extraction of resolve_p2p_endpoints as a pure-ish function (aside from logging) improves testability significantly.
  • Log messages at lines 414-416 and 421-424 appropriately inform operators of the automatic address selection.

Security & Performance

  • Security: No vulnerabilities introduced. Preventing advertisement of unspecified addresses is a minor security/usability win (prevents unreachable node configurations).
  • Performance: No concerns; this is single-execution initialization code.

Verdict: Approve with minor suggestion to expand IPv6 test coverage.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. Medium: cmd/ethrex/initializers.rs can now advertise the wrong address family when --p2p.addr is unspecified. With --p2p.addr ::, the fallback uses local_v6.or(local_v4), and with --p2p.addr 0.0.0.0 it uses local_v4.or(local_v6). That means the ENR can end up advertising IPv4 while the node binds IPv6-only sockets, or vice versa. The bind side still uses bind_addr directly for TCP/UDP sockets in crates/networking/p2p/types.rs, crates/networking/p2p/network.rs, and crates/networking/p2p/network.rs, so peers may dial an endpoint the node is not actually listening on. The fallback should stay same-family only; otherwise keep the unspecified address and warn.

  2. Low: The new tests in cmd/ethrex/initializers.rs do not cover the cross-family regression above. Add cases for Some("::"), None, Some(v4), None and Some("0.0.0.0"), None, None, Some(v6) so this behavior is pinned down.

No other correctness or security issues stood out in this patch.

I couldn’t run cargo test -p ethrex --lib initializers::tests here because rustup failed to create temp files under /home/runner/.rustup due the read-only environment.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions
Copy link
Copy Markdown

Lines of code report

Total lines added: 90
Total lines removed: 0
Total lines changed: 90

Detailed view
+-----------------------------------+-------+------+
| File                              | Lines | Diff |
+-----------------------------------+-------+------+
| ethrex/cmd/ethrex/initializers.rs | 745   | +90  |
+-----------------------------------+-------+------+

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 22, 2026

Greptile Summary

Fixes the long-standing issue where launching with --p2p.addr=0.0.0.0 (or ::) without --nat.extip caused the node to advertise the unspecified address in its ENR, making it unreachable for inbound peers. The fix factors address resolution into a new pure-ish resolve_p2p_endpoints function backed by 6 unit tests.

  • Bind address and external/announced address are now resolved in a single, documented function with clear precedence rules; get_local_p2p_node delegates to it after detecting the local IPs.
  • When --p2p.addr is an unspecified address and no --nat.extip is provided, the announced address falls back to the auto-detected local IP of the matching family (with an info! log), or keeps the unspecified address with a loud warn! if no local IP is found.

Confidence Score: 3/5

The core fix is correct and well-tested for the common cases, but the cross-family fallback path can silently produce a broken bind/announce pair without any log warning.

The primary bug (advertising 0.0.0.0 in the ENR) is correctly addressed and the refactor is clean, but the cross-family fallback inside the unspecified-bind branch doesn't guard against returning a mixed-family pair and emits no warning when it does so. The extip arm already has an explicit assertion for this exact situation, making the inconsistency visible in the diff.

cmd/ethrex/initializers.rs — the cross-family fallback in resolve_p2p_endpoints around lines 403–416

Important Files Changed

Filename Overview
cmd/ethrex/initializers.rs Extracts P2P address resolution into a testable resolve_p2p_endpoints function and fixes the 0.0.0.0/:: announce bug. The auto-detect fallback path can silently return a cross-family (bind, external) pair when the detected local IP family doesn't match the bind family.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["resolve_p2p_endpoints(p2p_addr, nat_extip, local_v4, local_v6)"] --> B{nat_extip set?}
    B -- Yes --> C["external = nat_extip\nbind = p2p_addr (or UNSPECIFIED of same family)"]
    B -- No --> D{p2p_addr set?}
    D -- Yes --> E{bind.is_unspecified?}
    E -- No --> F["return (bind, bind)"]
    E -- Yes --> G{local IP of matching family?}
    G -- Found --> H["INFO: announce auto-detected IP\nreturn (bind, local_ip)"]
    G -- Not found, cross-family available --> I["⚠️ Silent mismatch:\nreturn (bind_v4, local_v6) or (bind_v6, local_v4)"]
    G -- None at all --> J["WARN: announce unspecified\nreturn (bind, bind)"]
    D -- No --> K["return (local_v4 or local_v6, same)"]
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
cmd/ethrex/initializers.rs:403-416
**Silent cross-family address mismatch in unspecified bind fallback**

When `--p2p.addr=0.0.0.0` is given and `local_v4` is `None` but `local_v6` is available, the code returns `(IpAddr::V4(0.0.0.0), IpAddr::V6(fe80::1))` — an IPv4 bind address paired with an IPv6 external address. The node would bind only on IPv4 sockets but advertise an IPv6 ENR, so peers would attempt IPv6 connections and find nothing listening. The symmetric case (`--p2p.addr=::` with only IPv4 local) produces the inverse mismatch. Neither case emits a warning (unlike the zero-addresses case on line 418). The `(_, Some(extip))` arm already guards this with an explicit `assert!` on family equality — the same guard (or at minimum a `warn!`) is needed here before returning the cross-family pair.

Reviews (1): Last reviewed commit: "fix(l1): announce local IP when --p2p.ad..." | Re-trigger Greptile

Comment on lines +403 to +416
if bind.is_unspecified() {
let external = if bind.is_ipv6() {
local_v6.or(local_v4)
} else {
local_v4.or(local_v6)
};
match external {
Some(ext) => {
info!(
announced = %ext,
bind = %bind,
"--p2p.addr is unspecified; announcing auto-detected local IP. Set --nat.extip to override."
);
(bind, ext)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Silent cross-family address mismatch in unspecified bind fallback

When --p2p.addr=0.0.0.0 is given and local_v4 is None but local_v6 is available, the code returns (IpAddr::V4(0.0.0.0), IpAddr::V6(fe80::1)) — an IPv4 bind address paired with an IPv6 external address. The node would bind only on IPv4 sockets but advertise an IPv6 ENR, so peers would attempt IPv6 connections and find nothing listening. The symmetric case (--p2p.addr=:: with only IPv4 local) produces the inverse mismatch. Neither case emits a warning (unlike the zero-addresses case on line 418). The (_, Some(extip)) arm already guards this with an explicit assert! on family equality — the same guard (or at minimum a warn!) is needed here before returning the cross-family pair.

Prompt To Fix With AI
This is a comment left during a code review.
Path: cmd/ethrex/initializers.rs
Line: 403-416

Comment:
**Silent cross-family address mismatch in unspecified bind fallback**

When `--p2p.addr=0.0.0.0` is given and `local_v4` is `None` but `local_v6` is available, the code returns `(IpAddr::V4(0.0.0.0), IpAddr::V6(fe80::1))` — an IPv4 bind address paired with an IPv6 external address. The node would bind only on IPv4 sockets but advertise an IPv6 ENR, so peers would attempt IPv6 connections and find nothing listening. The symmetric case (`--p2p.addr=::` with only IPv4 local) produces the inverse mismatch. Neither case emits a warning (unlike the zero-addresses case on line 418). The `(_, Some(extip))` arm already guards this with an explicit `assert!` on family equality — the same guard (or at minimum a `warn!`) is needed here before returning the cross-family pair.

How can I resolve this? If you propose a fix, please make it concise.

let external = if bind.is_ipv6() {
local_v6.or(local_v4)
} else {
local_v4.or(local_v6)
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.

Cross-family fallback may advertise an unreachable address. When bind is 0.0.0.0 and local_v4 is None but local_v6 is Some, this falls back to the IPv6 address — but the v4 socket can't accept inbound v6 connections, so peers reaching the ENR endpoint will fail to connect. Same in the v6 branch (line 405) when only local_v4 is available.

In practice this only matters on single-stack hosts where the other stack's local-IP probe somehow succeeds (rare), so it's not a regression vs. main. But the warn-then-fallback path below might be the better default when only the wrong-family IP is found: keep bind as the announced address and emit the warning, since at least operators won't think peering is healthy when it isn't. Consider:

let external = if bind.is_ipv6() { local_v6 } else { local_v4 };

(drop the cross-family .or(...))

Non-blocking; corner of a corner. Flagging because the test matrix doesn't cover the v6 path of this branch either — see body.

bind = %bind,
"--p2p.addr is unspecified and no local IP could be detected; \
announcing the unspecified address. Inbound peer connections will fail. \
Set --nat.extip to fix this."
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.

nit: the message says "Set --nat.extip to fix this" but the branch is only reached when local_ip() and local_ipv6() both failed — which usually means the host genuinely has no routable IP detectable. An operator hitting this path doesn't necessarily know what extip to pass either. --p2p.addr=<explicit-ip> is the more general alternative.

Suggest:

"--p2p.addr is unspecified and no local IP could be detected; \
 announcing the unspecified address. Inbound peer connections will fail. \
 Set --nat.extip or use --p2p.addr=<ip> to fix."

Low priority — just operator UX.

let (bind, ext) = resolve_p2p_endpoints(None, Some("203.0.113.5"), None, None);
assert_eq!(bind, ip("0.0.0.0"));
assert_eq!(ext, ip("203.0.113.5"));
}
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.

Worth adding a test for the IPv6 unspecified path to mirror p2p_addr_unspecified_v4_announces_local_ip:

#[test]
fn p2p_addr_unspecified_v6_announces_local_ipv6() {
    let local6 = ip("fe80::1");
    let (bind, ext) = resolve_p2p_endpoints(Some("::"), None, None, Some(local6));
    assert_eq!(bind, ip("::"));
    assert_eq!(ext, local6);
}

The v6 arm of the if bind.is_ipv6() { local_v6.or(local_v4) } switch is currently uncovered. Also missing: a #[should_panic] test for the --p2p.addr / --nat.extip family-mismatch assertion. Both are cheap.

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

Labels

L1 Ethereum client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants