fix(daemon): mitigate onion message flooding DoS attack#1840
Conversation
Add three defenses against an ongoing DoS attack where an attacker connects directly to makers' onion services and floods them with orderbook requests, causing disk fill from log spam, CPU exhaustion, and OOM kills: 1. Per-connection message rate limiting in OnionLineProtocol: Drop connections exceeding 45 messages per 15-second window. 2. Handshake requirement for JM messages: Silently reject JM messages from non-directory peers that haven't completed a handshake, preventing unauthenticated peers from triggering expensive orderbook response operations. 3. Downgrade log level for 'no directory peer' warning: Change from WARNING to DEBUG for the message logged when a privmsg fails because the recipient's directory peer is not found, reducing console log noise during attacks.
Add 11 unit tests covering the new DoS protections: - TestOnionLineProtocolRateLimiting (6 tests): messages under limit processed, over limit triggers disconnect, counter resets after interval, invalid messages still counted, counter initialization, exact boundary behavior. - TestReceiveMsgHandshakeCheck (5 tests): JM messages rejected from non-handshaked peers, accepted from handshaked peers, accepted from directory peers regardless of status, control messages still processed without handshake, privmsg also rejected from non-handshaked peers.
|
@AdamISZ @kristapsk @PulpCattel This problem is bringing down most makers at the moment. |
|
Interesting about the orderbook part. Note that a very small feature was added early on by @chris-belcher to limit : not allow more that one General rate limiting certainly makes a ton of sense, I vaguely remember thinking about doing that. I'm curious about the 'don't treat a peer as a directory unless.. x'; is it the case that directory peers have privileges in terms of what they can send (I must admit I've forgotten how the protocol works; is it because the directories are sending us peer lists or something? Isn't orderbook a command sent from the taker peer to the directory node?) |
Right, Chris's check: joinmarket-clientserver/src/jmdaemon/message_channel.py Lines 887 to 889 in 5252017 (which came from JoinMarket-Org/joinmarket#298) blocks multiple !orderbook commands packed into a single message. But the attack we observed was sending many separate messages, each with a single !orderbook. That check doesn't help there, since it's one !orderbook per message, one message per line, many lines per second. In the base code joinmarket-clientserver/src/jmdaemon/onionmc.py Lines 172 to 180 in 5252017 passes everything straight through with no rate limit. And joinmarket-clientserver/src/jmdaemon/onionmc.py Lines 982 to 989 in 5252017 parses the from_nick directly from the message payload without validating it against the peer's handshake nick, so a single connection could even vary the from_nick across requests (though for !orderbook specifically that doesn't change the cost much, since the maker generates the same response regardless). And that's why it's important to ban per connection instead of per nick! Each !orderbook triggers the full on_orderbook_requested path at joinmarket-clientserver/src/jmdaemon/daemon_protocol.py Lines 759 to 772 in 5252017 which builds and sends the complete offerlist (including fidelity bond proof if enabled). And probably is the cause behind the memory "leak" that leads to OOM crashes. About directory peer privileges, you're right that it relates to message relay. The directory is not sending its own !orderbook, it's forwarding taker requests to makers. So it's not as trivial to limit. The flow is:
So from a maker's perspective, the directory connection legitimately carries !orderbook messages from many different takers. That's why the handshake gate this PR adds exempts directory peers. Applying a per-connection rate limit or handshake requirement to the directory connection would break normal operation during busy periods. Rate limiting directory-forwarded messages would need to be per-nick rather than per-connection, which is a bigger change (and out of scope for this stopgap). JM-NG implements this at the directory level and at the directory clients as well, to not trust directories more than necessary. The distinction in this PR is: for direct (non-directory) inbound connections, there's no reason to accept JM messages before the handshake completes, and there's no reason a single direct peer should send 45+ messages in 15 seconds. Those are the two checks this patch adds. |
|
Yes. I believe I understand all the logic, and it makes sense. The historical progression is something like: per-message rate limits were enforced by the IRC servers so we were only really worried (if at all) about intra-message edge cases like the !orderbook > 1 mentioned above (and only then because of its magnifying effect; individual line lengths were also of course limited in the IRC version). Then when the onion message channel stuff was added by me, my attitude was 'let's see if this even works' at first; it did for a while, then got hammered by the non-JM specific onion service DOS of the 2021-2023 period (I don't remember the exact time frame) and onionmc wasn't working too well. I should have put some rate limiting in right at the start; the system is clearly much too weak without it. And then we get to your further developed point: intrinsically directory nodes are different and can't be subject to general/normal rate limiting rules (should they not still have some rate limiting rules, though, even if different?). Which means we don't just trust anyone to be a d-node; this does leave open the question of (this was part of the original high level thinking): can directory nodes gossip other directory nodes? Think of the analogy with seeding peers in bitcoin IBD bootstrap. (edit: for anyone reading who doesn't see why that was part of the original thinking, consider the censorship resistance angle; one thing we're really trying hard to avoid here is giving directory nodes the power to censor makers). It's fine if not, just a question. |
|
I think we talked about the censorship resistance/gossiping somewhere but I don't remember. Open to ideas! Short version of how joinmarket-ng does it:
Rate limiting in jm-ng is layered, with different scopes:
Conceptually:
And of course all of this needs something to make new connections expensive, otherwise rate limiting doesn't really work! And that is Tor's PoW defense: joinmarket-ng/joinmarket-ng#108 Which is very cool because it only kicks in when needed, and is transparent to the user and jm-ng itself. |
|
Right. Indeed the Tor pow is a big part. It's just that, somewhere in the stack, the user has to feel at least somewhat comfortable that a random maker out there can communicate their offer to them. The more limited/controlled the message layer is, the less they can be sure of that. |
|
On that topic, I would like to explore what can be done with Nostr relays. Since there are there anyway and there's lots of them. |
|
Thought I should add my 2 sats worth on this attack. Rate limiting is a part of the solution but certain behaviours suggest non-standard usage. In my patch, my approach is simple: disconnect any non-directory peers that send a pubmsg. Normal taker behaviour does not involve sending a pubmsg directly to makers. Pubmsgs are meant to be public and should only be sent in the public IRC channel or through a directory. As @AdamISZ mentioned, the IRC servers already rate-limit messages from clients so implementing the same on directories would be effective. Following this logic, accepting pubmsgs only from directories mitigates this attack, since it prevents attackers from sending these messages directly to peers; privmsgs sent directly are still allowed. I've observed this attack uses either different takers or the same taker with multiple connections, making it easy to bypass per-connection rate limits. On the privilege concern: directories being the only peers allowed to forward pubmsgs is acceptable because makers connect to directories, not vice versa. This means attackers cannot exploit this channel, as it would require makers to connect to them. There is a potential risk of directory censorship, but makers mitigate this by connecting to multiple directories. |
Sooner or later, makers will be able to receive a command directly. If not !orderbook then !fill, which is unavoidable. Or do the attack through a directory. Opening Tor connections takes a bit of time and resources, and that seems enough to slow down the current attack to manageable for most hardware. Would be nice to agree on the protocol specification about direct pubmsg though. For compatibility. |
Problem
An ongoing DoS attack targets JoinMarket makers by connecting directly to makers' onion services and flooding
!orderbookrequests.Each request can trigger expensive orderbook response handling and failed reply routing, causing:
What this patch does
This is a small stopgap mitigation in
src/jmdaemon/onionmc.py:"Failed to send privmsg because no directory peer is connected."from warning to debug.Directory classification / spoofing notes
dn-handshakeis only accepted from peers already marked as directory.{"directory": true}in a handshake does not grant directory privileges.Limitations
!orderbookthrough trusted directory relay paths; this bypasses per-connection direct-connection limiting.Testing
test/jmdaemon/test_onionmc_dos.pywith 11 tests:Applying patch manually
Don't trust, verify!