What syscall-layer tooling cannot see in P2P infrastructure: a technique-by-technique analysis
More than $100 billion in digital assets is staked across decentralized networks. The operators who run that infrastructure protect it with traditional syscall-layer security tooling: Falco, commercial EDR, generic host agents. This is a technique-by-technique analysis of why that tooling is structurally blind to an entire class of availability attacks against the nodes themselves.
The shape is always the same. A network denial-of-service attack against a validator or RPC node lands, and nothing fires. It is not a tuning problem, and it is not a rule you forgot to enable. These attacks live above the layer the tools observe, so the tools cannot see them however they are configured.
The analysis below uses Falco as the worked example. Falco is the standard open-source syscall-layer runtime security engine, and its detection surface is public and documented, so its limits can be shown rather than asserted. The same limits apply to any tool that observes at the system-call boundary, including commercial EDR and generic host agents, because they are properties of that boundary and not of how well Falco is written. The five techniques are reproduced denial-of-service findings from the NullRabbit corpus of P2P and RPC infrastructure attacks. Each one links to its NRDAX registry entry and, where published, a full write-up and advisory.
The question
Host runtime security watches the system-call boundary. When a process opens a file, spawns a child, connects a socket, or reads a buffer, the kernel emits an event and the engine checks its rules against it. For what these tools were built to catch, that is a good place to watch: a shell spawned inside a container, an unexpected execve, a write to /etc/shadow, an outbound connection to somewhere it should not go.
A network denial-of-service attack against a blockchain node is not that kind of event. Its harm is CPU spent on a cryptographic handshake, memory consumed by concurrent protocol streams, or bandwidth burned serialising an oversized response. What drives it is the rate, the volume, and the frame-level detail of traffic arriving on a port the node deliberately exposes. The question is whether any of that reaches the syscall boundary in a form a rule can act on. Technique by technique, it does not.
The detection surface
Be exact about what a syscall-layer engine sees for network activity. The list is short:
evt.type, the system call itself:accept,connect,close,read,write,sendto,recvfrom.- The
fd.*five-tuple for a socket:fd.sip,fd.sport,fd.cip,fd.cport,fd.l4proto. Who connected to whom, on what ports, over TCP or UDP. evt.res, the return value of areadorwrite, meaning the number of bytes moved by that one call, or a negative errno.- Process and container identity,
proc.*andcontainer.*, to attribute the event to a workload.
That is the whole surface for network behaviour. Everything below asks what these fields can reconstruct about an attack whose mechanism runs above them, and what they cannot.
Three structural limits
Three properties of this vantage point carry the argument. They are not gaps you can patch. They follow from observing at the system-call boundary.
No counting, and no time windows
Falco evaluates each event on its own. A rule condition is a boolean expression over the fields of one syscall. There is no operator for "N events in T seconds", no per-source counter, no sliding window in the condition language. You can write a rule that fires on one accept. You cannot write one that fires on the fiftieth accept from a source IP in the last second, because the rule has no memory of the previous forty-nine.
Rate and volume detection has to move downstream to a metrics backend, say Prometheus with an alerting rule. At that point it is metrics alerting, not a syscall rule, and it has been flattened to a coarse count that no longer carries the per-source, per-connection attribution needed to name a technique. Almost every attack here is defined by rate or volume, so this one limit rules most of them out before any protocol detail matters.
No CPU field, and no reliable live memory
The fields describe the syscall and the process that made it. They do not describe what that process is consuming. There is no process-CPU field and no reliable per-event memory-growth (RSS) signal. You can see that a socket was accepted. You cannot see that servicing it cost 40 ms of elliptic-curve arithmetic, or that resident memory just grew by 200 MB.
For the two techniques whose harm is CPU burn or memory growth, the harm is exactly the quantity the fields cannot express. The attack is invisible not because it hides, but because the instrument has no dial for it.
No protocol parsing, and the bytes are encrypted anyway
Falco captures at most the first snaplen bytes of a read or write buffer, 80 by default, and it does not parse HTTP/2 framing, gRPC, or a Noise or TLS handshake. Two separate walls stand here, and either one is enough.
First, no parser. Even with unlimited capture, nothing in the engine can tell you "this exchange was HEADERS followed immediately by RST_STREAM", or "this connection carried five thousand concurrent streams". Those are frame-layer facts, and the frames are never decoded.
Second, ciphertext. For Noise, TLS, and HTTP/2 over TLS, which is how modern P2P and RPC transports run, the captured bytes are encrypted. The content that would separate an attack from ordinary traffic is unreadable in principle, not just unparsed. Raising snaplen to capture the whole payload gives you more ciphertext, not more signal.
The technique-by-technique table
Five reproduced techniques against the surface above. "Detectable?" asks whether the attack can be written as a technique-attributable rule at the syscall layer, one that names this attack rather than flagging a generic symptom.
| Technique | Detectable at the syscall layer? | The concrete signal, or the specific reason there is none | Honest false-positive assessment |
|---|---|---|---|
| Pre-auth handshake CPU burn (NRDAX-T0205) | No | The cost is userspace crypto: several X25519 operations plus a signature verify per handshake, run before the peer is authenticated or rate-limited. No syscall is emitted per Diffie-Hellman step, so the work never appears. The only trace is an accept followed by a quick close on the P2P port. Telling an attacker from a legitimate peer means counting that churn over a window, which the rule language cannot do. | Every legitimate peer connection is byte-for-byte identical at the syscall layer. Any threshold fires constantly or never, and the burn itself is never seen. |
| HTTP/2 rapid reset (NRDAX-T0112) | No | The whole attack is a HEADERS-then-RST_STREAM pattern in HTTP/2 frames, carried inside one long-lived TCP connection. Streams are virtual and produce no per-stream syscalls, and the frames are unparsed and, over TLS, ciphertext. There is nothing at the syscall layer to key on. | One steady connection with ordinary read and write activity. No signal, so no rule, and no true positives either. |
| gRPC / HTTP/2 multiplexing OOM (NRDAX-T0097) | No (OOM symptom only) | The cause is the number of concurrent HTTP/2 streams multiplexed onto one connection, a frame-layer quantity that is invisible to syscalls. The only syscall-adjacent trace is the eventual OOM-kill, a SIGKILL or cgroup event. That is post-mortem, carries no attribution, and there is no live-RSS field to catch the growth before it happens. | An OOM-kill fires for any memory bug or honest load spike. No technique attribution at all. |
| Mempool / ingress-pool pending-eviction flood (NRDAX-T0156, NRDAX-T0246) | No | Pool sizing, eviction ordering, and quota keying are application logic: transaction parsing, pool ordering, per-key accounting. None of it surfaces as a distinct syscall. The half-open variant is an accept with no following read, but there is no cumulative-count or rate field to turn that into a signal. | Slow or idle but legitimate peers look identical. Separating them needs aggregation the rule language does not have. |
| Unbounded response amplification / oversized transfer (NRDAX-T0329) | Partial (size symptom only) | The one real field is evt.res. On read or recvfrom (ingress) or write or sendto (egress) it gives the byte count of a single transfer, so a large value flags a large message. But applications move data in roughly 16 KB chunks, spreading one large logical message across many syscalls, and any byte threshold overlaps legitimate large blocks, snapshots, and RPC responses. It is a size symptom, not a technique. | Heavy false positives from ordinary large transfers. Useful as a volume symptom for a metrics pipeline, not as an attributable rule. |
NullRabbit enforces at the layer where each signal lives. The rate-, connection-, and volume-driven vectors are dropped or rate-limited by source at the packet layer with eBPF/XDP, before userspace allocates. The two that hide inside TLS, rapid reset and multiplexing OOM, surface in the client's own stream metrics, not on the wire. Every technique above is mapped live in the NRDAX registry.
Reading the table. Zero of the five can be written as technique-attributable syscall rules. The one "partial", size via evt.res, cannot carry an NRDAX-T attribution: it fires on a busy block or a large snapshot exactly as it fires on an attack. Building any of these as a host rule ships false-positive noise that names nothing.
The "why not" column repeats one of three reasons. The quantity that discriminates is either a rate the language cannot count (handshake burn, pending-eviction flood), a resource cost with no field (CPU burn, OOM), or a frame-level fact that is unparsed and encrypted (rapid reset, stream multiplexing). A better rule closes none of them.
Do the plugins change it?
Falco's plugin framework adds event sources and field enrichment. Check each one against the gap, since "just add a plugin" is the obvious objection.
k8sauditsources Kubernetes API-server audit events. That is control-plane activity, who called the Kubernetes API, not the node's P2P or RPC wire. It never sees a peer handshake or an RPC call arriving at the client.containerandk8smetaenrich existing syscall events with container and pod metadata. They add labels to what is already captured. They add no new view of the wire.- No production Falco plugin parses HTTP/2 frames or a cryptographic handshake. There is no h2-frame source and no Noise-handshake source to subscribe to. The frame-level and crypto-cost facts stay unobserved whatever plugins are loaded.
Falcosidekickis an output router. It forwards alerts to Slack, object storage, and similar sinks. It sits downstream of detection and detects nothing itself.
The ecosystem enriches and routes. It does not move the observation point off the syscall boundary, which is where the limits are.
Where the signal actually lives
Every signal that discriminates these five techniques sits in one of three places, all above or beside the syscall boundary:
- Wire-frame semantics: HEADERS-then-RST_STREAM, the count of concurrent stream IDs on a connection, half-open handshakes per source IP, the shape of a Noise message. The syscall layer sees these only as opaque, usually encrypted, bytes.
- Userspace crypto cost: the CPU spent on the pre-auth half of a handshake, which no syscall records.
- Event rate and volume: connections, streams, or bytes per source over a window, which the rule language cannot count.
Two vantage points can recover these. It matters which signal each one actually reaches.
The first is packet and transport-layer inspection with eBPF/XDP, in front of the userspace daemon. The unencrypted transport metadata is visible before the application reads it, and counting is native instead of absent: per-source connection and handshake-attempt rates, half-open counts, packet timing and volume, TCP-level reset patterns, and, for cleartext framing such as h2c, the frame pattern itself. This is where the rate and metadata signals live. It does not read application frames that sit inside TLS. Encrypted HTTP/2 stays encrypted at the packet layer too.
The second is the application's own metrics. For the frame-level quantities that are encrypted on the wire, meaning concurrent stream count, mempool and ingress-pool occupancy, per-request response size, and HTTP/2 stream-reset counters, the client is usually already exporting the number. It is the harm made legible, if you collect it and alert on it. A node that emits its active-stream gauge tells you about the multiplexing-OOM technique that no packet capture of a TLS session can.
This is why NullRabbit puts enforcement at the packet layer instead of the host. For the vectors driven by rate, connections, or volume, an eBPF/XDP layer sees the per-source metadata before the application does and can drop or rate-limit by source before userspace allocates the memory, CPU, or connection slots the attack is trying to burn. It does not decrypt TLS to do this. It starves the vector of the resources it depends on. The two that live inside TLS, rapid reset and multiplexing OOM, are the exception: the packet layer cannot read those frames, so their signal is the client's own stream metrics instead. In every case the observation point sits above the syscall boundary, which is where a host engine watches.
Evidence
Each technique below links to its canonical NRDAX registry entry and, where published, the full NullRabbit write-up and advisory. Capture bundles for these primitives live in the public dataset NullRabbit/nr-bundles-public, so the behaviour is reproducible rather than asserted.
- Pre-handshake crypto CPU burn. NRDAX-T0205. Reproduced against CometBFT and Cosmos (advisory NR-2026-008; write-up: How CometBFT's SecretConnection handshake spends CPU before authenticating the peer), and against Casper and Conflux (advisory NR-2026-039).
- HTTP/2 rapid-reset stream exhaustion. NRDAX-T0112. Reproduced against Walrus (advisory NR-2026-038). The same pattern is present in Substrate and Polkadot HTTP/2 servers.
- gRPC and HTTP/2 multiplexing OOM. NRDAX-T0097. Reproduced against an IOTA node's public gRPC server (advisory NR-2026-003; write-up: How unbounded HTTP/2 concurrent streams let one TCP connection OOM-kill an IOTA node's public gRPC server).
- Mempool and ingress-pool pending-eviction flood. NRDAX-T0156. The adjacent quota-keying variant, rate-limit key confusion (NRDAX-T0246), was reproduced against the Internet Computer's ingress pool (advisory NR-2026-036).
- Unbounded RPC response amplification. NRDAX-T0329. Reproduced across Solana (
getProgramAccounts; advisories NR-2026-001, NR-2026-040), Sui (advisory NR-2026-004), Zcash and Zebra (advisory NR-2026-061), and go-ethereum (eth_getLogs; advisory NR-2026-023; write-up: How a wide-range eth_getLogs query turns a few hundred bytes into an unbounded response).
Scope of the claim
This is an analysis of a detection boundary, not a verdict on a tool. Falco is a strong engine at the layer it was built for, and nothing here says otherwise. It is the example because its surface is open enough to reason about precisely. The claim is narrow: for network denial-of-service against P2P and RPC infrastructure, the signal that separates an attack from ordinary load lives in wire-frame semantics, userspace crypto cost, and per-source rate and volume, and none of those is expressible at the system-call boundary. If your host tooling did not catch one of these, that is why, and no rule will change it.
Related Posts
How a single crafted gossipsub PRUNE backoff value panics rust-libp2p nodes via an Instant overflow (CVE-2026-34219)
rust-libp2p's gossipsub implementation stores a peer-supplied PRUNE backoff duration and later adds a further duration to it during heartbeat processing using unchecked arithmetic; a single crafted PRUNE with a backoff value near u64::MAX overflows that addition and panics the process.
How CometBFT's SecretConnection handshake spends CPU before authenticating the peer
CometBFT completes the full SecretConnection STS handshake — an X25519 Diffie–Hellman exchange and an Ed25519 signature verification — for any connecting peer before checking whether that peer's node-ID is allowlisted, so an unauthenticated remote source can spend the node's asymmetric-crypto budget at will.
How an unauthenticated TLS half-open flood pins rippled's memory and crashes it under low file-descriptor limits
rippled's inbound TLS listeners on the peer port (51235) and the RPC-HTTPS port (5006) accept a partial TLS handshake with no deadline and no per-IP half-open cap, so a single source IP can pin server memory indefinitely and, at a default file-descriptor limit, crash the process.
