nervosnetwork / nervosnetwork/ckb

QUIC listen address is not discovered for third-party outbound connections

Open
#5,310 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.