← Back to Research
Research · July 22, 2026

One prior-knowledge h2c connection multiplexes N eth_getLogs past an L4 per-connection cap on go-ethereum's JSON-RPC port

Simon Morley·7 min read

go-ethereum's JSON-RPC HTTP port terminates cleartext HTTP/2 (h2c) by prior knowledge — a client can send the raw HTTP/2 preface with no TLS and no Upgrade handshake and the node speaks h2 — and HTTP/2 multiplexes many requests as independent streams over one TCP connection. An operator who fronts that port with an L4 / TCP-passthrough edge and rate-limits connections per source IP (nginx stream limit_conn, or a cloud L4 load balancer) counts a single connection while being blind to the requests multiplexed inside it, so one h2c connection carrying N eth_getLogs requests pulls N amplified responses through the single permitted connection — an amplification-multiplexing bypass of the connection cap.

Why this matters

Rate-limiting an exposed RPC endpoint by connections per IP is a common, cheap edge control — it is what an L4 load balancer or an nginx stream block gives you without terminating TLS or parsing HTTP. The bypass matters because HTTP/2's whole point is to carry many requests on one connection, and an L4 edge cannot see them: it forwards the TCP byte stream — the h2 preface and every multiplexed stream inside — opaquely to the origin, and its only lever is the count of connections. So the naïve assumption — "we cap connections per IP, so one IP can't flood us" — does not hold once the origin speaks h2c: one permitted connection becomes an arbitrary number of concurrent requests. The control is not useless (it does stop N separate connections), it is simply enforcing at the wrong layer for a multiplexed transport. Note the honest boundary: this defeats a connection cap, not an L7 per-request rate limit — an edge that terminates h2 and counts requests still sees and limits each stream.

How the attack works

  • Entry point: go-ethereum's JSON-RPC HTTP transport (--http) on its port (:8545 in the lab), reached through an L4/TCP-passthrough edge that forwards the connection opaquely.
  • Preconditions: the RPC port is exposed behind an L4 edge whose only control is a per-connection cap (e.g. nginx stream limit_conn); the origin accepts prior-knowledge h2c (no TLS, no Upgrade needed). No authentication is required.
  • Processing path: geth's JSON-RPC HTTP server (node/rpcstack.go, rpc/http.go) is served by Go's net/http, whose h2c-capable handler accepts a prior-knowledge HTTP/2 preface on the cleartext listener. HTTP/2 (RFC 9113) then multiplexes concurrent streams over the one connection up to SETTINGS_MAX_CONCURRENT_STREAMS. The attacker opens one connection and issues N eth_getLogs calls as N streams; the L4 edge forwards the bytes and counts one connection.
  • Boundary crossed: a connection-count control (L4 limit_conn), not an L7 per-request rate limit. Each eth_getLogs inside is itself a response-amplifying read (the subject of a separate finding, NR-2026-023), so the multiplex delivers N amplified responses for one connection slot.
  • Resulting behaviour: the connection cap never triggers on the multiplexed tunnel while the origin does N requests' worth of scan, serialise and egress — measured at 20 streams / ~39 MB on a single connection (below).

Affected systems

  • Severity: MEDIUM — availability / bandwidth-and-CPU cost only; no funds, no consensus break, no authentication bypass. The harm is per-node egress + serialisation load on an exposed RPC endpoint whose only edge control counts connections.
  • Chain: Ethereum.
  • Implementation: go-ethereum, tested at 1.17.4, with the JSON-RPC HTTP transport exposed behind an L4/TCP-passthrough edge.
  • Required edge: an L4 / TCP-passthrough front whose only control is a per-connection cap — measured with nginx stream limit_conn 1 per source IP; a cloud L4 load balancer with per-connection limits is the same shape.
  • Not affected: an edge that terminates HTTP/2 and rate-limits per request (L7) — in testing, an h2-terminating edge with a per-request limit rejected 17 of 20 streams (503), so the multiplex does not defeat request-level limiting. A loopback-bound RPC, or a port not exposed on a routable interface, is also out of scope. The behaviour is a property of the RPC server terminating cleartext h2c plus HTTP/2 multiplexing, not a version-specific regression; no affected version range is established beyond the tested 1.17.4.

Evidence

  • Disclosed by NullRabbit: advisory NR-2026-053 ("h2c stream-multiplexing bypasses an L4 connection cap on the JSON-RPC port", published 2026-07-15) is the canonical record; it cites geth's node/rpcstack.go / rpc/http.go and carries the measurements below. No CVE is assigned.
  • Publicly documented: go-ethereum's JSON-RPC HTTP server is served by Go's net/http, whose h2c handler accepts a prior-knowledge HTTP/2 preface on the cleartext listener; HTTP/2 stream multiplexing is RFC 9113. The bypass is the interaction of that with a connection-granularity edge control, not a documented feature.
  • Independently reproduced by NullRabbit (lab): a reproducer (primitive eth_geth_h2c_multiplex_connlimit_bypass, family rate_limiter_bypass, source_class: original) runs a self-owned go-ethereum 1.17.4 --dev node behind an nginx stream L4 edge (limit_conn 1 per source IP). The captured packet trace ships in the public dataset NullRabbit/nr-bundles-public (corpus crp_12a57e6811944e4a); it contains the two successful multiplexed h2c connections — one direct to the origin, one through the L4 edge — each carrying all 20 streams on a single connection.
  • Measured (self-owned lab): one prior-knowledge h2c connection multiplexing 20 eth_getLogs streams completed 20/20, pulling tens of megabytes of response through the single connection — an independent read of the shipped pcap measures ~38–39 MB of TCP payload per multiplexed connection (~1.9 MB/stream), while the advisory reports the response volume as ~43.9 MB. The same run rejected 16 of 20 separate connections from one IP (limit_conn), the advisory/manifest control confirming the cap is real and is bypassed only by the multiplexed tunnel (that rejection set is a reported result, not part of this packet capture). Through an L7 (h2-terminating) edge with a per-request rate limit, 17 of 20 streams were rejected (503, per the advisory). These figures are from a self-owned lab node, not a production measurement.

Operator actions

  • Rate-limit at L7 (per request), not only L4 (per connection). A terminating reverse proxy that counts requests sees each multiplexed stream; a connection cap does not.
  • Bound HTTP/2 multiplexing at the edge or origin with a low SETTINGS_MAX_CONCURRENT_STREAMS, and cap the eth_getLogs result size / block range as well (the NR-2026-023 mitigation) so each permitted stream is bounded.
  • Do not expose the JSON-RPC port raw behind an L4 / TCP passthrough — keep it on loopback or behind an authenticating, request-aware gateway.
  • Exposure check: if your RPC edge is an L4 load balancer or nginx stream with a per-connection cap, confirm whether the origin answers a prior-knowledge h2c preface and whether a single connection can carry multiple concurrent eth_getLogs streams. These are configuration-level mitigations; no vendor patch is asserted here.

Canonical references

Related attacks

The per-stream payload here is eth_getLogs, whose own response amplification is a separate NullRabbit finding — NR-2026-023 (eth_getlogs_response_amp, NRDAX-T0329), which NR-2026-053 explicitly cross-references: the multiplex tunnel is what lets N of those amplified reads ride one connection the L4 edge counts as one. Separately, a companion go-ethereum finding, eth_geth_h2h1_empty_authority_vhost_bypass (advisory NR-2026-054, NRDAX-T0110), also turns on an HTTP/2 edge-vs-origin disagreement — there over Host/:authority identity (an h2→h1 downgrade that bypasses geth's --http.vhosts allowlist), here over connection-vs-stream accounting. That companion is a different family (auth_bypass, not this finding's rate_limiter_bypass), and it is its record — not NR-2026-053 — that places it on NullRabbit's rigs/h2-diff downgrade-differential set; NR-2026-053 does not itself reference it. No relationship is claimed beyond the advisory's own NR-2026-023 cross-reference and this same-surface companion link.

Track attacks affecting go-ethereum and Ethereum RPC infrastructure.

Analysis plus reproducer. No weaponised proof-of-concept code.

researchethereumgo-ethereumhttp2h2crate_limiter_bypassrpcinfrastructure-security
Publication policy:Why we publish these findings →

Related Posts