Skip to content

feat(protocol): a member answers only the peers its application admitted - #689

Closed
bahdotsh wants to merge 1 commit into
feat/app-visibility-gatefrom
feat/app-inbound-gate
Closed

bahdotsh wants to merge 1 commit into
feat/app-visibility-gatefrom
feat/app-inbound-gate

Conversation

@bahdotsh

@bahdotsh bahdotsh commented Nov 4, 2026

Copy link
Copy Markdown
Member

Step 7b of the shared network plan (ADR 0037). Stacked on #688.

One predicate: admitted

admitted(peer) is true when the peer is any one of:

  • visible;
  • in the named set;
  • holding a 1:1 session with this node;
  • on the roster of a group this node holds.

With no publisher key, every peer is admitted. app_sees_peer (the emission gate from #688) now asks this same predicate, as the ADR says.

The named set

  • What it holds: addresses the app itself acted toward. That covers send_message, forward_message, send_media, connection requests and their answers, presence, typing and read receipts, establish_secure_session, manual_mls_create_session and invite_to_group.
  • What writes it: only those calls. A frame never writes it.
  • Storage: a new sealed protocol-state category, named_peers, because it is a contact list. The value is the Unix second the address was last named.
  • Bound: 4096 addresses. The least recently named address without a session is evicted first.
  • Backfill: on every launch, every peer that holds a session is added, so turning membership on cuts no one off.

The inbound gate

A frame addressed to this node from a sender that is not admitted goes down the blocked-sender path:

  • it is not processed;
  • it gets no answer of any kind;
  • no event is emitted.

The sender sees what it would see for an address nobody holds. ACKs are also refused at both ACK send points (send_ack_with_status and send_undecryptable_ack).

These pass whatever the sender:

  • __MEMBER__;
  • relay server answers;
  • custody receipts;
  • the group data plane, which opens only under a group this node holds;
  • discovery gossip, which gets its own rules in 7c.

Key packages, 1:1 Welcomes and group Welcomes pass if they carry a valid proof.

The end-to-end proof

  • What carries it: every key package, 1:1 Welcome and group Welcome sent by a node that holds a certificate. It is attached in sign_control_message, so every send path gets it.
  • Deviation from the ADR sketch: the proof travels in the frame metadata (__member_proof, defined once in offline-protocol-sealed), not in a payload field.
    • The bound body is then the content exactly as it travels, so nothing has to be re-serialised to hash it.
    • The payload structs have about 57 struct literals across the crates, including the leaf's; a payload field would have touched all of them.
    • Metadata is outside the control signature, but that costs nothing. The proof signs the recipient, kind and body itself, so stripping it fails closed and moving it onto another frame fails to verify.
  • The spec section is updated to match.

Tests

  • Seven new tests:
    • a stranger's key package gets no frame back of any kind;
    • the same stranger, once named, is answered;
    • an unconfigured node answers as before;
    • two members who never met converge on the proof alone;
    • a proof recorded on its way to alice and pasted onto bob's frame to dave is refused;
    • a node with the key but no certificate proves nothing;
    • the named set survives a restore, and an explicit call writes it.
  • Mutation: with the inbound gate forced open, two of these tests fail.
  • Suites: offline-protocol lib 2211 pass, offline-protocol-uniffi 318 pass, offline-protocol-sealed 102 pass.
  • Clippy: clean over the workspace.

Deliberately not in this PR

  • Service discovery under membership (sealed bodies, proofs on query and response, answering the originator) is 7c, the next PR.
  • Subscription renewal and data sync on discovery stay ungated. Both act on relations the app made itself, and they did not go through the named-set hooks. The ADR lists them under the gate, so step 8's guard enumeration should decide whether they move behind admitted.

Step 7b of the shared network plan (ADR 0037): the inbound gate.

One predicate, admitted, now decides both what the application is told
and what this node answers. A peer is admitted when it is visible, in
the named set, holds a 1:1 session with this node, or is on the roster
of a group this node holds; with no publisher key every peer is.

- The named set: the addresses the application itself sent to, paired
  with, requested a session or connection with, or invited. Written
  only by those calls, never by anything a frame carries; persisted as
  a new sealed protocol-state category (`named_peers`, a contact list);
  at most 4096, the least recently named address with no session goes
  first; backfilled on every launch from every session, so turning
  membership on cuts off no contact.
- The inbound gate: a frame addressed to this node from a sender that
  is not admitted is treated exactly as a blocked sender's, with no
  processing, no answer of any kind and no event, so the sender sees
  what it sees for an address nobody holds. Passed whatever the sender:
  `__MEMBER__`, relay server answers, custody receipts, the group data
  plane (it opens only under a held group) and discovery gossip (its
  own rules come next). Acknowledgements are refused at both ACK send
  points too.
- The end-to-end proof: every key package, 1:1 Welcome and group
  Welcome a node with a certificate sends carries one, attached at
  signing, so two members several hops apart converge with no naming.
  It travels in the frame metadata (`__member_proof`) rather than in
  the payload, so the bound body is the content exactly as it travels
  and nothing is re-serialised to hash it; the proof signs recipient,
  kind and body itself, so the metadata being outside the control
  signature costs nothing.

Tests: a stranger's key package gets nothing back; the same stranger,
once named, is answered; an unconfigured node answers as before; two
members who never met converge on the proof; a proof made for one
member does not admit its sender at another; a node with the key and
no certificate proves nothing; the named set survives a restore and an
explicit call writes it. Disabling the gate fails two of them.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant