Skip to content

net: redial --connect addresses that drop, as in Core - #908

Merged
reardencode merged 3 commits into
reardencode:masterfrom
bkeroack:net/connect-redial-literals
Oct 4, 2026
Merged

reardencode merged 3 commits into
reardencode:masterfrom
bkeroack:net/connect-redial-literals

Conversation

@bkeroack

@bkeroack bkeroack commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Problem

A --connect target that parses as an address (IP, onion, I2P, CJDNS) is dialled once, by the startup tip-follow dial, and never again. When that session drops, or that first dial fails, a node that only uses --connect has no peers until it is restarted. The startup dial also covers at most three targets (follow_n), so a fourth --connect address is never dialled at all.

Only --connect hostnames are retried. run_p2p passes connect_dns to set_connect_hosts, and the 2 s redial pass walks manual_hosts and connect_hosts only. Core's ThreadOpenConnections loops over every -connect address for as long as the node runs and reopens any that is not connected.

Noted under "Not changed" in #876.

Fix

  • PeerHub::set_connect_addrs holds the NetAddr targets. redial_remembered_with dials each one that has no live session, as manual, after the hostnames. A hostname and an address that meet at one endpoint share one dial per pass.
  • run_p2p registers them right after the startup follow dials, inside the tip-follow block, so catch-up is unchanged: no redialled session runs next to the catch-up sessions. Hostnames still register before catch-up, as today. (When catch-up ends incomplete, run_p2p skips tip mode and shuts down, so there is no running node to redial for.)
  • The redial pass no longer stacks dials. It skipped a target only once the provisional session was registered, which happens after connect_net returns, so a connect that hangs (a slow SOCKS circuit, a host that drops SYNs) got a new dial every 2 s, and each that connected became its own session. That already applied to hostnames and addnode add. prepare_outbound_session now holds a DialInFlight guard from before connect_net until it returns on any path, and the redial pass skips a target with a dial in flight.
  • connect_net had no timeout on the dial-task path. The SOCKS reply and I2P SAM waits can block for ever once the proxy has accepted, which with the guard would keep a target out of the redial pass for good. prepare_outbound_session now bounds the connect at 8 s through connect_timeout_for (I2P 90 s), the same values IBD and the tip-follow dial already use.

Tests

  • node_run_p2p_short asserted that a --connect peer stays gone after disconnectnode (it waited for an empty getpeerinfo). It now asserts that the seeder sees that session close (matched by its addrbind), that the peer comes back as manual under a new id, and that the seeder sees the new inbound.
  • The two beats that waited for an empty getpeerinfo, after disconnectnode and after the seeder's unsolicited tx, now wait for the old session ids to end, so the redial cannot race them. The second disconnectnode targets the old nodeid. An address that was never connected still returns -29.
  • The addnode onetry beat is removed. Once the --connect peer comes back on its own, a manual row no longer shows that onetry dialled. onetry and add share dial_resolved, which redial_same_endpoint_in_addnode_and_connect_dials_once still drives through add.
  • New service::tests::redial_skips_a_connect_target_still_dialling: a SOCKS proxy that accepts and never replies holds the first dial. Three more redial passes leave the proxy with one connection, and once the connect times out the next pass dials again (the bound is 2 s under cfg(test)).
  • redial_same_endpoint_in_addnode_and_connect_dials_once adds an IP and an onion --connect address. Both dial as manual, the IP shares the dial of the hostname at the same endpoint, and an address with a live session is not dialled again.

Each new assertion fails when its guard is reverted: dropping the address targets from the redial pass fails the unit test, dropping the set_connect_addrs call fails the journey on "--connect peer was not redialled after disconnectnode", dropping either the in-flight skip or the guard fails the SOCKS test on 4 proxy connections instead of 1, and raising the connect bound to an hour fails it on "a connect that never finishes must time out and be redialled". node_run_p2p_short passed three runs in a row locally.

Ran locally: cargo fmt --all -- --check, ./scripts/ast-grep.sh, cargo clippy --workspace --all-targets -- -D warnings, the full rbitcoin-net lib tests, the rbitcoin-node run:: tests, and node_run_p2p_short.

🤖 Generated with Claude Code

bkeroack and others added 3 commits October 4, 2026 15:42
Only --connect hostnames went into the 2 s redial pass
(set_connect_hosts takes connect_dns). A target that parses as a
NetAddr (IP, onion, I2P, CJDNS) got one tip-follow dial at startup and
nothing after it: when that session dropped, or failed the first time,
a --connect-only node had no peers until it was restarted. Startup also
dials at most three targets, so a fourth --connect address was never
dialled. Core keeps reconnecting every -connect address.

PeerHub::set_connect_addrs holds the NetAddr targets, and
redial_remembered_with dials each one with no live session as manual,
sharing one dial per endpoint with the hostnames. run_p2p registers them
after the startup follow dials, so catch-up is unchanged.

node_run_p2p_short asserted that a disconnected --connect peer stays
gone. It now asserts the peer comes back as manual under a new id. The
beats that waited for an empty getpeerinfo now wait for the old session
ids to end, which the redial cannot race. The addnode onetry beat is
dropped: with the peer redialled it no longer showed anything.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The redial pass skipped a target only when is_target_live saw its
session, and the provisional session is registered after
connect_net returns. A connect that hangs (a slow SOCKS circuit, a host
that drops SYNs) got a new DialRequest every 2 s, and each attempt that
finally connected became its own session. This already applied to
--connect hostnames and addnode add; redialling --connect addresses
widened it.

prepare_outbound_session now holds a DialInFlight guard from before
connect_net until it returns, on every path, and the redial pass skips a
target with a dial in flight.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
connect_net has no timeout, and the SOCKS reply and I2P SAM waits can
block for ever once the proxy has accepted. With the in-flight guard
that would hold a target out of the redial pass for good. The dial task
path now bounds the connect at 8 s (connect_timeout_for gives I2P 90 s),
the value IBD and the tip-follow dials already use. The SOCKS stall test
now also waits for the timeout and a second dial; under cfg(test) the
bound is 2 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@rearden-grok rearden-grok Bot left a comment

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.

Summary

Literal --connect targets (IP, onion, I2P, CJDNS) are now remembered and redialled as manual on the existing 2 s pass once tip-follow starts, including addresses the startup dial skipped past follow_n. That matches the part of Core's ThreadOpenConnections this PR claims: every -connect entry is retried for the life of the tip-follow process, and a target that is already connected is not opened again. Core blocks in ConnectNode on one thread, so it needs no separate in-flight set; this node dials concurrently, and the new DialInFlight guard plus the 8 s connect bound (90 s for I2P via connect_timeout_for) closes the hole where a hung SOCKS or SYN-drop dial stacked a session every pass. Registering literals only after the startup follow dials, not during catch-up, is an intentional narrower window than Core and matches the changelog. The disconnect/redial assert belongs in node_run_p2p_short and is correctly placed there; dropping the addnode onetry beat is justified because a redialled --connect peer is indistinguishable from onetry on that same address. The new SOCKS stall test cannot fold into that journey (--proxy would wrap the seeder dial) or into any other catalog journey, and the dial-channel dedup (shared hostname/address endpoint, onion, live skip) is not visible on a real session without a backdoor, so both units should stay.

Issue counts by severity

  • bugs: 0
  • suggestions: 0
  • nits: 0

@reardencode
reardencode merged commit 7ad170a into reardencode:master Oct 4, 2026
18 checks passed
@bkeroack
bkeroack deleted the net/connect-redial-literals branch October 6, 2026 23:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants