Repository navigation
p2p: don't ban local, inbound-onion or manual peers for misbehaviour (Core parity) - #944
Merged
Merged
Conversation
…ds per peer - v1 frames are capped at Core's MAX_PROTOCOL_MESSAGE_LENGTH (4,000,000 bytes) instead of 32 MiB. The v1 and v2 receive buffers grow as payload bytes arrive, at most 256 KiB ahead of them (V1Transport::readData, V2Transport MAX_RESERVE_AHEAD), and a large v1 buffer is released after its message. - inv/getdata over MAX_INV_SZ (50,000), headers over MAX_HEADERS_RESULTS (2000) and addr/addrv2 over MAX_ADDR_TO_SEND (1000) are misbehaviour, as in Core; a notfound over 5016 entries is ignored. - A peer's queue counts its bytes. getdata is served as Core's ProcessGetData does: entries are kept in a per-peer backlog and served until the queue passes 1,000,000 bytes (-maxsendbuffer's default), and the rest as it drains. The peer's next message is not read while a getdata of its is being served or its queue is over the limit (ProcessMessages). -maxuploadtarget is charged only for blocks queued. - The write loop watches the disconnect signal during a write, and drops a peer whose socket takes no bytes for 20 minutes (TIMEOUT_INTERVAL, "socket sending timeout"). Sent bytes are counted as the socket takes them. - A version whose user agent is over MAX_SUBVERSION_LENGTH is dropped, as Core's LIMITED_STRING read does; the user agent and unknown message types are sanitized with Core's SanitizeString before they are stored or logged. - getcfilters/getcfheaders/getcfcheckpt requests that Core's PrepareBlockFilterRequest refuses now disconnect instead of being ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The write loop reads nothing more from a peer until its getdata has been served, so the manager's 500 ms drain tick bounded a peer fetching blocks to about one getdata, or one send buffer's worth of a large one, per tick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deal with a peer whose misbehaviour reaches the threshold as Core's MaybeDiscourageAndDisconnect does (net_processing.cpp), in its order: a noban peer and a manual connection are not punished; a peer on a local address (CNetAddr::IsLocal: 127.0.0.0/8, 0.0.0.0/8, ::1) is disconnected without punishing the address; anyone else is disconnected and its address banned for -bantime, as before (Core discourages it). satd banned every peer but noban ones. Every inbound onion peer arrives from the address Tor dialled from, usually 127.0.0.1, as does every unwhitelisted local client, so one such peer's misbehaviour refused all of them until the ban expired, across restarts. A manual peer was banned and then skipped by the redial loop. Peers accepted on the onion listener are now marked (Core's m_inbound_onion), and getpeerinfo reports them on the onion network, as Core's ConnectedThroughNetwork does. An outbound onion peer is banned by its onion host, the address Core punishes, rather than by the 0.0.0.0 placeholder socket every onion peer shares. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
serve_compact_block reported a MSG_CMPCT_BLOCK entry as answered even when its cmpctblock could not be queued, so the entry left the getdata backlog with nothing sent. It now says whether the message was queued, and an entry that was not stays first, as every other entry does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…/discourage-not-ban-local-onion-manual
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s from Core decides the disconnect-only rung on the address alone (pnode.addr.IsLocal(), net_processing.cpp), and Tor on the same host dials from loopback. With Tor on another host (-bind=<lan address>=onion) every onion peer arrives from that host's address. Core discourages it, and still admits it while inbound slots are free (net.cpp); satd's ban would refuse every onion peer for -bantime. An inbound onion peer is now disconnected without a ban whatever address it arrives from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merged with fix/p2p-message-limits' tip (02bdb2d) as the base, since this branch already carried it and release/0.6 now holds its squashed equivalent. No conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 13, 2026
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.
Problem
When a peer's misbehaviour score reached 100,
add_ban_score(node/src/net/manager.rs) exempted onlynobanpeers. Every other peer was disconnected and its IP banned for-bantime(24 h by default), and the ban was written tobanlist.json.Bitcoin Core handles a misbehaving peer in
MaybeDiscourageAndDisconnect(src/net_processing.cpp:5083-5120), in this order:nobanpeer is not punished ("Not punishing noban peer",:5094-5098);:5100-5104);CNetAddr::IsLocal,src/netaddress.cpp:398-412: IPv4 127.0.0.0/8 or 0.0.0.0/8, IPv6::1) is disconnected, but its address is not discouraged, "since that would discourage all peers on the same local address" (:5106-5113). Its log line names the peer "inbound onion" or "local";:5115-5119).satd skipped steps 2 and 3, which had these effects:
is_addr_bannedbefore a peer is admitted) refused every onion peer, and every local client withoutnoban, until the ban expired. The ban was kept across restarts.addnode,-addnode,-connect) was banned like any other, and the redial loop then skipped the banned address.getpeerinfoalso reported an inbound onion peer asnot_publicly_routable(its loopback address). Core reports it asonion:ConnectedThroughNetworkreturnsNET_ONIONform_inbound_onion(src/net.cpp:602-605,:619;src/rpc/net.cpp:235).The out-of-range
getblocktxnthat scores 100 matches Core:SendBlockTransactionscallsMisbehavingfor "getblocktxn with out-of-bounds tx indices" (src/net_processing.cpp:2598-2605). That trigger is unchanged. Only what happens to the peer afterwards changes.Fix
PeerManager::misbehaviour_actionapplies Core's ladder when the score reaches the threshold:noban→ keep; manual → keep; local address (net::is_local, Core'sIsLocal, with an IPv4-mapped address judged as the IPv4 address it carries) or an inbound onion peer → disconnect without a ban; otherwise → disconnect and ban for-bantimeas before.add_ban_scoreacts on the result. When a peer is kept, its score resets to zero and a warning is logged, as Core clearsm_should_discourageand logsNot punishing … peer N!.PeerInfo::inbound_onion(Core'sm_inbound_onion) is set for peers accepted on the-bind=…=onionlistener. It names the kind of peer in the disconnect log line, andgetpeerinforeports these peers withnetwork: "onion".0.0.0.0placeholder that every onion peer shares. That placeholderIsLocalwould count as local, and banning it said nothing about the peer. Core judges and punishes the peer by its onion address, which is not local, so satd now bans the onion host (BanTarget::Onion).Tests
Unit tests,
net::manager::peerpunish_tests(new filenode/src/net/manager_peerpunish_tests.rs):misbehaviour_follows_cores_ladder: one case per rung. Ordinary inbound, outbound and private-network (192.168.x) peers are banned (the must-succeed half).noban, manual, and manual-on-loopback peers are kept. 127.0.0.1, 127.5.6.7,::1,::ffff:127.0.0.1, an inbound onion peer (on loopback, and forwarded from a LAN address) and an outbound peer on loopback are disconnected without a ban.a_misbehaving_outbound_onion_peer_is_banned_by_its_onion_hostkept_peers_stay_however_often_they_misbehave: repeated misbehaviour fromnobanand manual peers, plus an ordinary peer kept at 99 and banned at 100.an_out_of_range_getblocktxn_from_an_inbound_onion_peer_bans_nothing: throughhandle_message. The ordinary peer is still banned.getpeerinfo_reports_an_inbound_onion_peer_as_oniona_peer_accepted_on_the_onion_listener_is_marked_inbound_onion: a real accept, with and without the onion flag.Integration tests (
satd/tests/p2p_punish.rs, new target, run against a live regtest node):a_misbehaving_onion_or_local_peer_is_disconnected_without_a_ban: the node listens on127.0.0.1:Pand on127.0.0.1:O=onion. For each listener, a peer sends an out-of-rangegetblocktxnand is disconnected,listbannedstays empty, and a second peer on the same listener is admitted and served.getpeerinfo.networkisonionon the onion listener.a_misbehaving_manual_peer_stays_connected: afteraddnode … onetryto a raw listener, the same message leaves the manual peer connected and served, and nothing is banned.Existing tests edited in place:
compact_blocks::blocktxn_with_wrong_count_is_penalisedandgetblocktxn_out_of_range_index_is_penalisedstill require the disconnect. They now require that nothing is banned, because their raw peer is on 127.0.0.1. One comment inp2p_limits.rswas updated for the same reason.Fails without the fix: yes.
inbound_onionfield added so they compile, and each failed on its own assertion. Examples:misbehaviour_follows_cores_ladder"a manual peer: left: Banned, right: Kept";kept_peers_stay_however_often_they_misbehave"manual: left: Banned, right: Kept";an_out_of_range_getblocktxn_from_an_inbound_onion_peer_bans_nothing"and nothing is banned";a_misbehaving_outbound_onion_peer_is_banned_by_its_onion_host"its onion host is banned";getpeerinfo_reports_an_inbound_onion_peer_as_onion"not_publicly_routable" vs "onion";a_peer_accepted_on_the_onion_listener_is_marked_inbound_onion"[false]" vs "[true]".a_misbehaving_manual_peer_stays_connectedwith "the manual peer is still connected and served", anda_misbehaving_onion_or_local_peer_is_disconnected_without_a_banwith "onion: getpeerinfo.network".left: Banned, right: Disconnected.inbound_onionassignment, and theinbound_onionterm ingetpeerinfo.network. All six unit tests failed on their assertions.misbehaviour_follows_cores_ladderandkept_peers_stay_however_often_they_misbehavefailed on "manual".an_out_of_range_getblocktxn_from_an_inbound_onion_peer_bans_nothingfailed on "and nothing is banned", and the other three failed as listed above. With only the local rung removed,a_misbehaving_onion_or_local_peer_is_disconnected_without_a_banfailed on "onion: nothing is banned".Notes
src/net.cpp:1810-1816), and does not list them inlistbanned. satd keeps its persistent-bantimeban for the last rung. Changing that would change a default and whatlistbannedandbanlist.jsoncontain, so it is out of scope here.Misbehavingas reaching the threshold. Only what happens at the threshold changes here. Anobanpeer now accumulates a score like any other, logsNot punishing noban peer N!at warn level when the score reaches 100 (as Core logs), and its score resets. Before, every increment was skipped with a debug line.pnode.addr.IsLocal(),src/net_processing.cpp:5106), andm_inbound_oniononly picks the word in its log line. With Tor on the same host the two agree. With Tor on another host (-bind=<lan address>=onion), every onion peer arrives from that host's address. Core discourages that address, and a discouraged address is still admitted while inbound slots are free. satd's equivalent would be a-bantimeban that refuses every onion peer. So satd disconnects an inbound onion peer without a ban whatever address it arrives from (is_local(addr) || inbound_onion), as the rung is described for this change. Removing|| info.inbound_onioninmisbehaviour_actionrestores Core's exact test.setban <host>.onion, and of the0.0.0.0/32ban this used to add for an outbound onion peer. It is unchanged here and left for the dial-path work. Core skips a banned or discouraged address only for automatic dials (OpenNetworkConnection,src/net.cpp:3023-3027).DisconnectNode(pnode.addr)also disconnects every other peer on the same address (src/net.cpp:3839-3842). satd still disconnects only the peer that misbehaved, and the ban refuses the others when they next connect. Unchanged here.Release notes
satd now treats a misbehaving peer the way Bitcoin Core does. Peers with the
nobanpermission and manual connections (addnode,-addnode,-connect) are never disconnected or banned for misbehaviour. A peer on a local address, and every peer that connects through the node's Tor hidden service, is disconnected but its address is not banned. Before this change, one such peer's misbehaviour could ban 127.0.0.1 for-bantime(24 hours by default, kept across restarts), which refused all onion and local P2P clients withoutnobanfor that time.getpeerinfonow reports peers that came in through the hidden service withnetwork: "onion".🤖 Generated with Claude Code