net: peer resolution, getpeerinfo, locators and the outbound cap (#668, #691) - #718
Open
bkeroack wants to merge 3 commits into
Open
net: peer resolution, getpeerinfo, locators and the outbound cap (#668, #691)#718bkeroack wants to merge 3 commits into
bkeroack wants to merge 3 commits into
Conversation
bkeroack
force-pushed
the
fix/668-691-net-parity
branch
from
September 9, 2026 20:21
95a8ede to
d0a0379
Compare
Contributor
Author
|
Rebased onto the updated 🤖 Generated with Claude Code |
bkeroack
force-pushed
the
fix/664-compat-notifications-timeouts
branch
from
September 9, 2026 22:31
b4493af to
97b9fff
Compare
bkeroack
force-pushed
the
fix/668-691-net-parity
branch
from
September 9, 2026 22:31
d0a0379 to
0a37777
Compare
bkeroack
force-pushed
the
fix/664-compat-notifications-timeouts
branch
from
September 9, 2026 22:50
97b9fff to
40f5653
Compare
bkeroack
force-pushed
the
fix/668-691-net-parity
branch
from
September 9, 2026 22:50
0a37777 to
a752679
Compare
#691) Resolution. `-addnode` / `-connect` / `addnode` all went through `PeerAddr::parse_with_default_port`, which resolved hostnames with the blocking `std::net::ToSocketAddrs` from inside async handlers, used the local resolver even under `-proxy`, and ignored `-dns=0` outright. A new async `resolve_peer_target` is the single entry point: literal IPs and `.onion` targets never touch a resolver, a clearnet hostname is refused under `-proxy` (the leak `resolve_operator_seeds` already refuses for `-seednode`) and under `-dns=0`. `parse_with_default_port` is now pure. Handing the name to the proxy to resolve, as Core does, is not implemented. `getpeerinfo`. `network` folds every unroutable address into `not_publicly_routable` via a new `net::is_routable` (Core's `IsRoutable`), which also replaces the loopback-only proxy bypass in `dial_direct`. `addr_relay_enabled` is the latch Core's `SetupAddressRelay` sets rather than a function of the direction. `servicesnames` walks bits 0..64 in order. `last_block` / `last_transaction` move only on acceptance, so the block channel now carries the sending peer's stats. The dead `relay_txes` field is gone, and `getconnectioncount` counts the set `getpeerinfo` lists. Locators. `locator_fork_height` is Core's `FindForkInGlobalIndex`: only entries on the active chain count, and an unmatched locator falls back to genesis so the reply starts at height 1. `getblocks` breaks before pushing `hashStop`; `getheaders` honours `hashStop` at all. `remove_peer_addr` clears `manual_addrs` / `manual_onion_hosts`, so a removed added-node stops being dialled as manual and stops bypassing `-connect` gating. Outbound cap. `check_outbound_limit_for` takes a second, total bound — Core's `semOutbound`, `min(full_relay + block_relay + feeler, maxconnections)` — under the same lock as the per-type reservation. The two types with no individual limit, addr-fetch and feeler, returned early before, so `addconnection` could open them without bound. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL
Both notes were inherited and both were wrong. `feature_proxy.py`'s
recorded blocker — an outbound peer dialled through -proxy never
appearing in getpeerinfo — is gone; it now cannot start at all, because
its `setup_nodes` passes `-i2psam`, which satd refuses by name. It also
wants `-cjdnsreachable` and unix-socket proxies, so it stays `skip`
under `feature-missing`.
`rpc_net.py` likewise gets past the proxy stage now and fails asserting
that `help("getpeerinfo")` contains Core's network list; satd's
`help <command>` returns the method name, not per-method help text.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL
…in parallel `is_routable` and `getpeerinfo.network` judged `::ffff:a.b.c.d` as an IPv6 address, so an RFC 1918 peer that arrived on a dual-stack `[::]` listener was reported as a routable `ipv6` peer. Core's `CNetAddr` unwraps the mapped form at construction; both sites now canonicalise first. Core's `IsLocal` is 0.0.0.0/8 as well as 127.0.0.0/8, not the single unspecified address. `dial_added_peers` resolved and dialled the added entries in one spawned loop, so N unreachable `-addnode`/`-connect` entries on SIGHUP held the N-th behind (N-1) connect timeouts — a regression from the spawn-per-address shape it replaced. One task per address again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL
bkeroack
force-pushed
the
fix/664-compat-notifications-timeouts
branch
from
September 10, 2026 00:42
40f5653 to
6faab7c
Compare
bkeroack
force-pushed
the
fix/668-691-net-parity
branch
from
September 10, 2026 00:42
a752679 to
6ab12a7
Compare
Contributor
Author
|
Independent review pass — one fix landed on this branch.
Verified against Core v31.1 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #668. Closes #691.
Seventh in the 0.5.2 close-out stack. Based on
fix/664-compat-notifications-timeouts(#717) — merge that first.Name resolution honours
-proxyand-dns, and stops blocking the runtime-addnode,-connectand theaddnodeRPC all went throughPeerAddr::parse_with_default_port, which did three things it must not:std::net::ToSocketAddrsfrominside async handlers, so a slow resolver parked a tokio worker;
-proxy, handingwhoever watches that resolver the peers a proxied node exists to hide —
the same leak
resolve_operator_seedsalready refuses for-seednode;-dns=0, which was parsed and then consulted only for DNS seeding.net::dns::resolve_peer_targetis now the single async entry point for everyoperator-supplied peer target, and
parse_with_default_portis a pure parser.Literal IPs and
.oniontargets never touch a resolver and work in everymode; a clearnet hostname is refused under
-proxyand under-dns=0, with amessage saying which.
Not done here: handing the hostname to the proxy for it to resolve (Core's
by-name SOCKS5
ConnectThroughProxy).PeerAddrhas no clearnet-by-namevariant and the dial/dedupe bookkeeping is keyed on either a socket or an
onion host, so that is its own change. The leak is closed by refusal in the
meantime, which is the behaviour satd already documents for
-seednode.getpeerinfonetworkCNetAddr::GetNetwork— every unroutable address isnot_publicly_routable, via a newnet::is_routableaddr_relay_enableddirection == OutboundSetupAddressRelaysets, so an inbound peer exchanging addrs reportstrueservicesnamesUNKNOWN[2^n]fallbacklast_block/last_transactionrelay_txes(field)last_block/last_transactionare eviction inputs. Stamped on receipt, apeer could keep its own eviction protection alive with blocks the node
already had or transactions it rejects — the peer eviction exists to shed.
Carrying that meant the block-processing channel now carries the sending
peer's stats handle alongside the block.
net::is_routablealso replaces the loopback-only proxy bypass indial_direct. That bypass happened to match Core (GetNetwork()returnsNET_UNROUTABLEfor loopback, andGetProxy(NET_UNROUTABLE)is false) butonly for loopback; RFC1918 and link-local peers were being sent through the
proxy where Core dials them directly.
getconnectioncountnow counts the same setgetpeerinfolists.getblocks/getheadersanswer from Core's fork pointlocator_fork_heightisCChain::FindForkInGlobalIndex:reply start at
stale_height + 1on the active chain, which the peercannot connect to anything it holds);
1, instead of re-announcing the genesis block;
getblocksbreaks before pushinghashStop, instead of announcing therequester the block it named as already held;
getheadershonourshashStopat all — it was answering every requestwith up to 2000 headers.
addnode <peer> removeleft the peer manualremove_peer_addrcleared the reconnect list but notmanual_addrs/manual_onion_hosts, so a removed added-node kept being dialled as a manualconnection and kept bypassing
-connectgating.Total outbound cap (#691)
Core bounds automatic outbound connections twice: per type, and with the
semOutboundcounting semaphore sizedmin(m_max_automatic_outbound, m_max_automatic_connections). satd had onlythe per-type limits — and
addr-fetchandfeeler, the two types Core capsonly through that semaphore, returned "capacity fine" unconditionally, so
addconnectioncould open them until the process ran out of sockets. Thetotal check now runs under the same lock that reserves the per-type slot.
MANUALstays exempt, as in Core.Tests
New, each perturbation-proven (delete the guard, a named test fails):
net::dns—dns_disabled_refuses_a_hostname_but_not_a_literal,a_hostname_is_not_resolved_locally_under_a_proxy,a_hostname_still_resolves_with_no_proxy_and_dns_on,only_real_names_are_classified_as_needing_a_lookupnet::peer—unroutable_peers_are_not_reported_as_ipv4_or_ipv6,servicesnames_is_emitted_in_bit_order,addr_relay_enabled_reports_the_latch_not_the_directionnet::manager—getblocks_answers_from_cores_fork_point,getheaders_honours_the_stop_hash,removing_an_added_node_drops_its_manual_statusa_peers_last_block_and_last_tx_only_move_on_acceptance,addconnection_stops_at_the_total_outbound_capacityCore-functional inventory
Two rows re-measured, both still
skipbut for different reasons thanrecorded:
feature_proxy.py— the proxy-dial failure it was filed under is gone; itnow cannot start, because its
setup_nodespasses-i2psam(I2P is out ofscope for satd). It also needs
-cjdnsreachableand unix-socket proxies.rpc_net.py— likewise past the proxy stage; it now fails asserting thathelp("getpeerinfo")contains Core's network list. satd'shelp <command>returns the method name, not per-method help text.
🤖 Generated with Claude Code
https://claude.ai/code/session_011H5HUWaezLTYJCZDpgaioL