Repository navigation
net: redial --connect addresses that drop, as in Core - #908
Conversation
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>
There was a problem hiding this comment.
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
Problem
A
--connecttarget 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--connecthas no peers until it is restarted. The startup dial also covers at most three targets (follow_n), so a fourth--connectaddress is never dialled at all.Only
--connecthostnames are retried.run_p2ppassesconnect_dnstoset_connect_hosts, and the 2 s redial pass walksmanual_hostsandconnect_hostsonly. Core'sThreadOpenConnectionsloops over every-connectaddress for as long as the node runs and reopens any that is not connected.Noted under "Not changed" in #876.
Fix
PeerHub::set_connect_addrsholds theNetAddrtargets.redial_remembered_withdials each one that has no live session, asmanual, after the hostnames. A hostname and an address that meet at one endpoint share one dial per pass.run_p2pregisters 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_p2pskips tip mode and shuts down, so there is no running node to redial for.)connect_netreturns, 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 andaddnode add.prepare_outbound_sessionnow holds aDialInFlightguard from beforeconnect_netuntil it returns on any path, and the redial pass skips a target with a dial in flight.connect_nethad 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_sessionnow bounds the connect at 8 s throughconnect_timeout_for(I2P 90 s), the same values IBD and the tip-follow dial already use.Tests
node_run_p2p_shortasserted that a--connectpeer stays gone afterdisconnectnode(it waited for an emptygetpeerinfo). It now asserts that the seeder sees that session close (matched by itsaddrbind), that the peer comes back asmanualunder a new id, and that the seeder sees the new inbound.getpeerinfo, afterdisconnectnodeand after the seeder's unsolicitedtx, now wait for the old session ids to end, so the redial cannot race them. The seconddisconnectnodetargets the oldnodeid. An address that was never connected still returns-29.addnode onetrybeat is removed. Once the--connectpeer comes back on its own, amanualrow no longer shows thatonetrydialled.onetryandaddsharedial_resolved, whichredial_same_endpoint_in_addnode_and_connect_dials_oncestill drives throughadd.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 undercfg(test)).redial_same_endpoint_in_addnode_and_connect_dials_onceadds an IP and an onion--connectaddress. Both dial asmanual, 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_addrscall 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_shortpassed three runs in a row locally.Ran locally:
cargo fmt --all -- --check,./scripts/ast-grep.sh,cargo clippy --workspace --all-targets -- -D warnings, the fullrbitcoin-netlib tests, therbitcoin-noderun::tests, andnode_run_p2p_short.🤖 Generated with Claude Code