nervosnetwork / nervosnetwork/ckb
QUIC listen address is not discovered for third-party outbound connections
- Dominant language
- Rust
- Stars
- 1.2k
- Forks
- 266
- Avg merge
- 10d 5h
- Merged PRs (30d)
- 4
Description
## Summary
While validating #5295, a three-node local integration scenario showed that explicit QUIC dialing works, but a QUIC-only node's reusable listen address is not discovered and used by a third node through the normal Identify/Discovery/outbound path.
## Environment
- PR: #5295
- Tested head: `0d3477aecde0485bdb46dbae650ce15082e9708f`
- Binary: `ckb 0.209.0 (0d3477a 2026-08-21)`
- Test framework: `nervosnetwork/ckb-py-integration-test`
- Acceptance note: https://github.com/nervosnetwork/acceptance-internal/issues/1573#issuecomment-5367986037
## Scenario
Use three local dev nodes:
- Node A: TCP + QUIC listen
- `/ip4/0.0.0.0/tcp/`
- `/ip4/0.0.0.0/udp//quic-v1`
- Node B: QUIC-only listen
- `/ip4/0.0.0.0/udp//quic-v1`
- Node C: TCP-only listen
- `/ip4/0.0.0.0/tcp/`
- The PR supports outbound QUIC dialing even when the local node does not listen on QUIC.
Steps:
1. Start all three nodes and mine one block on A to avoid unstable initial dev/IBD behavior.
2. Connect B to A by explicitly calling `add_node(A_peer_id, A_quic_addr)`.
3. Connect C to A by explicitly calling `add_node(A_peer_id, A_tcp_addr)`.
4. Do not call `add_node` from C to B.
5. Wait for A to learn B's QUIC address through Identify/Discovery.
6. Wait for C to learn B through A and establish an outbound QUIC connection to B.
## Expected Behavior
C should eventually discover B's reusable QUIC listen address and connect to B over QUIC, for example:
`/ip4/127.0.0.1/udp//quic-v1/p2p/`
## Actual Behavior
- Explicit QUIC dialing still works: B can connect to A using A's QUIC address.
- C can connect to A over TCP.
- A's `get_peers` eventually shows a `/quic-v1` address for B, but the observed address is an inbound QUIC session address with an ephemeral UDP source port, for example:
`/ip4/127.0.0.1/udp/63523/quic-v1/p2p/`
- This is not B's configured QUIC listen port.
- C waits 90 seconds and remains connected only to A; it does not discover/connect to B over QUIC.
## Notes / Possible Cause
The current code appears to have two interacting behaviors:
- `NetworkState::from_config` filters `listen_addresses` / `public_addresses` through reachability checks. Local test listen addresses such as `/ip4/0.0.0.0/udp//quic-v1` do not become public addresses for Identify advertisement.
- `IdentifyCallback::local_listen_addrs` can include `observed_addrs` when public/listen addresses are not available. For outbound QUIC dials, the observed address can be the client's temporary UDP source port, which is not a reusable listen address.
This means a peer may learn a `/quic-v1` address for the remote node, but not the actual reusable QUIC listen address needed for third-party discovery and dialing.
## Suggested Regression Test
Add a three-node integration test:
1. B QUIC-only connects to A by QUIC.
2. C connects only to A by TCP.
3. Assert A learns B's reusable `/udp//quic-v1` address.
4. Assert C discovers B and connects to B using a `/quic-v1` address without direct `add_node(C, B)`.
Contributor guide
Research direction
Start by tracing NetworkState::from_config and IdentifyCallback::local_listen_addrs, focusing on how listen, public, and observed addresses are selected for Identify and Discovery. Use the three-node scenario in nervosnetwork/ckb-py-integration-test as the regression test; done means A learns B's reusable QUIC listen address and C discovers and connects to B without a direct add_node call.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100