Skip to content

fix(relay): the operator's onion-relay opt-in leaks through a shared rate-limit bucket #512

Description

@MichaelTaylor3d

Task

A stranger can determine whether this operator enabled onion relaying, by draining a rate-limit
bucket and watching a different RPC method change shape. Close the side channel, or decide
deliberately that the opt-in is not a secret and record that.

Why the equal-frames guard does not cover this

relay_capsule's five refusal sites all return one payload-free RelayStatus::Refused, and
dig-node#356's new guard pins the refusal FRAME equal to a plain miss. That closes the frame
channel. It does not close this one, because the leak is not in the frame — it is in a state
change the refusal leaves behind.

The mechanism

  1. relay_capsule gate 3 (module_relay.rs:124) calls allow_proxy_fetch(requestor).
  2. allow_proxy_fetch (download.rs:1643) CONSUMES a token, not merely inspects one.
  3. That bucket is shared with miss_outcome's second leg (download.rs:2721), where exhaustion
    degrades a later read from fetch-through to redirect.

So the probe is:

  • Spam dig.getModuleInfo with proxy: true to drain the bucket. Burst is 4.0
    (rate_limit.rs:179).
  • Then issue dig.getContent and watch which shape comes back.
  • Bytes means relaying is OFF — gate 2 refused before gate 3, so nothing was ever consumed.
  • Redirect means relaying is ON — gate 3 was reached and the tokens were spent.

The operator's opt-in is inferable from ~5 cheap requests, and nothing in the relay path looks
anomalous while it happens.

Why gate ORDER is the root cause

Gate 2 (operator opt-in, :120) returns BEFORE gate 3 (:124) ever runs. That ordering is what makes
token consumption a proxy for the opt-in: the presence or absence of a side effect answers a question
the frames were carefully built not to answer.

A related, smaller ordering defect sits beside it: :124 also runs before the capsule-warmer check at
:127, so on a build with no warmer a requestor's allowance is spent on a relay that could never have
happened. Same class, lower severity.

The fork, so whoever takes this does not have to invent it

  • (a) Do not consume on a path that cannot relay. Check-then-consume, or refund when a later gate
    refuses. Removes the signal; costs a small amount of limiter accuracy under abuse.
  • (b) Separate the buckets so the proxy-class allowance is not observable through miss_outcome.
    Removes the cross-method coupling that makes the probe cheap; more moving parts.
  • (c) Decide the opt-in is not a secret and say so in module_relay.rs's module doc — deleting
    the confidentiality claim rather than leaving a claim the code does not honour. Legitimate, and
    strictly better than the current state, where the doc implies a property the system does not have.

(a) is the recommendation. It is the smallest change and it addresses the root — a refusal should
not leave evidence of how far it got.

Evidence bar

A guard here must distinguish "the bucket was charged" from "the bucket was not", so it needs two
runs that differ ONLY in the operator opt-in, with the post-state asserted unequal. Asserting the
refusal frames are equal is already done by #356's guard and is not sufficient — that is the
whole point of this ticket.

Provenance

Found by the pre-merge security audit on PR #504 (the dig-node#356 NC-12 doc-claim audit), which
was asked whether the equal-frames guard actually closed the oracle and demonstrated that it does
not. Not fixed there: #504 is a measurement PR, and a security change grown inside it would make it
ungateable.

The correctness of #504's guard is unaffected — it closes the frame channel it claims to close,
and its doc now names this channel explicitly rather than implying the oracle is shut.

Audit: #356
Parent: https://github.com/DIG-Network/dig_ecosystem/issues/3128

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:businessa person can DO something new, or money moves, or a shipped surface stops lying to them

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions