net/upnp: LAN attacker can poison external-IP advertisement via UPnP-IGD response race (Step 3 post-Codex; see #19) #10
Labels
No labels
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
SubGeniusFinance/Offerings-to-Cthulhu#10
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Background
PR #9 (merge
24196aa) closed the worst external-IP-discovery trust surface — the cleartext-HTTP callout to amazonaws.com / ipify / icanhazip. One smaller surface remains for any OFFd compiled withUSE_UPNP=1: the UPnP-IGD address-discovery path insrc/net.cpp(around theMapPort/ThreadMapPortmachinery, miniupnpc backend).That path trusts whatever device responds first to the SSDP
M-SEARCHmulticast at239.255.255.250:1900. ItsGetExternalIPAddressSOAP response is fed directly into:…which seeds
mapLocalHostat score 3 (post-PR-#9 enum numbering) and gossips the address to peers viaaddrandversionmessages.There is no authentication of the UPnP-IGD responder, no certificate pinning, no out-of-band sanity check on the returned IP. It is trust-on-first-respond.
Threat model
Attacker capability: code execution on any device on the OFFd operator's LAN. They do NOT need:
Concrete attacker positions in the wild:
Attack chain
Attacker runs a UPnP-IGD impersonator on the LAN. Off-the-shelf tooling exists (
miranda-upnpand similar); a from-scratch implementation is ~200 lines of Python. The impersonator listens on SSDP multicast and serves a forged IGD device-description XML.OFFd boots. miniupnpc multicasts
M-SEARCH * HTTP/1.1. Both the attacker's daemon and the real router see it. Attacker is at microsecond latency on the same broadcast domain — they win the race the vast majority of the time. (Fallback if they don't: send unsolicited SSDPNOTIFYbetween OFFd boots.)miniupnpc accepts the attacker's
LOCATION:URL, fetches the fake device-description XML, and locks onto the attacker's SOAP endpoint for the rest of the OFFd process lifetime.miniupnpc calls
UPNP_GetExternalIPAddress. Attacker returns any IP they want:OFFd records
AddLocal(<attacker-chosen-ip>, LOCAL_UPNP). Score 3 entersmapLocalHost.For the next ~24 hours, OFFd advertises that IP as itself in
addrmessages to every connected peer and inversionhandshakes to inbound peers. Honest peers gossip it onward.What the attacker gains
In order of badness:
a. Eclipse-attack staging. Attacker advertises their own malicious-OFFd IP as the victim's address. Honest peers dialing the victim end up at the attacker, get a curated chain view, are eclipsed at the moment of first contact. This is the same protocol-level primitive the 2018 >80%-hash attacker used (see project-off-attacker-and-checkpoint). PR #9 closed this for the HTTP path; UPnP remains.
b. Pool-operator denial / griefing. Advertise a wrong IP for a victim mining-pool's wallet node. Miners can't reach the pool; pool operator gets falsely-attributed outage reports. Reputation hit.
c. Reputation attack on an uninvolved third party. Pick
<attacker-chosen-ip>= some residential ISP user. OFFd makes them look like a node. They get inbound P2P traffic and may trigger ISP abuse-detection.d. Treasury/bridge confusion during a contested operation. If a future Treasury-signing OFFd is ever misconfigured behind a UPnP NAT, the attacker can make peers disagree about which IP is canonical during a fork-coordination moment. Doesn't compromise the wallet key, only the discovery layer.
Realistic impact for OFF today
Cluster nodes: low. Operator-controlled cluster nodes already configure
-externalip=(LOCAL_MANUAL, higher score thanLOCAL_UPNP) — the UPnP-injected address loses the score comparison and is never advertised. Nodes hosted on VPSes with static public IPs have no gateway router to SSDP at all. There is no LAN-resident-attacker scenario for VPS-hosted nodes; the hypervisor / VLAN boundary is a much larger pre-requisite than "be on the same wifi."Third-party hobbyist operators: medium. A user running OFFd at home behind a residential router without
-externalip=is the target. As the network grows post-Restoration, this demographic will exist. They are the population this exploit is for.Proposed mitigation (phased)
v2.0.x release notes (immediate, zero code). Document
-upnp=0 -externalip=<your-real-ip>as the recommended config for any public-facing OFFd. Explain why (this issue).v2.1.0 default flip (one-line code change). Change the default of
-upnpfromtruetofalseinsrc/init.cpp(HelpMessage/SoftSetBoolArgpath). Bitcoin Core itself made this flip in bitcoin/bitcoin#20410 (2020, merged into 22.0) on the same threat-model reasoning. Operators who explicitly want UPnP can still pass-upnp=1.v2.2.x or later (depends/ work). Compile-time
USE_UPNP=0in release builds; drop miniupnpc fromdepends/entirely. Would benefit from a Skifdni-style depends/ PR. Eliminates the surface for everyone, including operators who would have ignored the warning in step 1.Trade-offs
-upnp=1explicitly or configure port-forwarding manually. Same tradeoff Bitcoin Core accepted in 2020.-externalip=precedence — these are operator-protection changes, not cluster-protection changes.Related
24196aa) — closed the analogous HTTP-discovery path. This issue is what's left after that.Not a Restoration blocker
This is a hardening item, not a fork-window item. No consensus impact. Step 1 (docs) can ship anytime; steps 2 and 3 fit naturally into the next v2.x point releases.
Phase 2 landed — #14 merged at
63626e76. Runtime default for-upnpflipped fromUSE_UPNP(compile-time) tofalse. UPnP is now opt-in.What this changes:
Offeringsdwith no-upnparg inOfferings.conf): UPnP is off at startup. No M-SEARCH on the LAN, no IGD SOAP traffic, no attacker race window.Offerings-qt):fUseUPnPdefaults to false in QSettings.upnp=1inOfferings.conf, or wallets withfUseUPnP=truealready saved in QSettings): behavior unchanged — the persisted opt-in carries forward. They have to deliberately untoggle to get the new posture, which is the correct migration behavior. Worth calling out in v2.1.0 release notes so existing operators know to revisit the setting.Phases recap:
USE_UPNP=0+ drop miniupnpc fromdepends/): still outstanding. Skifdni-style PR — irreversible without re-adding the depends recipe, so deferring until the chain is stable post-Codex. Not a Restoration blocker.Leaving this issue open for Phase 3 tracking. The attack surface is now down to opt-in only — a meaningful reduction, but a clearer floor would be removing UPnP support from release binaries entirely.
Default unbound. Iä Iä.
Closing as covered.
Step 1 (release-notes recommendation): shipped in v2.0.4 / v2.0.5 release notes.
Step 2 (runtime default flip,
-upnp=0): shipped in v2.0.6 via PR #14 (commit63626e7). Daemons built from v2.0.6 onward no longer enable UPnP without explicit-upnp=1.Step 3 (drop miniupnpc from depends/ + finish Windows CI alignment): tracked in #19, decision: post-Codex (after block 1,050,667).
Closing this issue since the parent threat-model description is captured in #19's body and there's nothing left in the parent that doesn't have a home elsewhere. Reopen if Step 3 surfaces additional concerns that don't fit in #19.