Skip to content

feat(nym-proxy-ffi): observe the mixnet's three legs by capture and probe (ADR 0007) - #1323

Open
zancas wants to merge 6 commits into
devfrom
mixnet_send_assertions
Open

feat(nym-proxy-ffi): observe the mixnet's three legs by capture and probe (ADR 0007)#1323
zancas wants to merge 6 commits into
devfrom
mixnet_send_assertions

Conversation

@zancas

@zancas zancas commented Aug 25, 2026

Copy link
Copy Markdown
Member

This change implements the observation surface of ADR 0007. Mixnet Mode routes a send across three legs. The app observes none of them today. This change makes the legs observable in debug wallet builds.

What the change adds

The diagnostics cargo feature compiles four things into zingo-nym-proxy-ffi:

  • A tracing capture layer. It matches events from nym_gateway_client and nym_socks5_client_core. Those are the two targets where nym drops the leg signals today.
  • A polled drain. Events land in a capped drop-oldest queue, and drain_diagnostics surrenders the queue in order with the count of any events lost to overflow. The host implements no callback, so no host code runs on the mixnet client's threads.
  • The bootstrap narrative. Under the feature, plain start carries the typed BootstrapEvents from zingo-netutils into the same queue.
  • Two probes on the handle. probe_sentinel classifies one Sentinel round trip into ExitProven, ExitRefused, GatewayLinkDead, or Indeterminate. probe_destination completes one TLS handshake against a caller-supplied destination through the tunnel and adds DestinationProven, HandshakeRefused, and TunnelFailed. The handshake verifies against the compiled-in Mozilla bundle per ADR 0006, and sends no application protocol.

A refusal verdict shows the exit's own captured words. Silence that no capture explains reports as indeterminate.

The two ratified ADRs ride along as their own commits: ADR 0005 (the component rename ruling) and ADR 0007 (this surface). CONTEXT.md gains the resolved vocabulary, including the rename of "correspondent" to "destination".

What the change does not do

  • No build system enables the feature yet. Release and debug binaries are byte-equivalent to dev. The per-variant build enablement (Dockerfile, build scripts, gradle, workbench, CI) is a follow-on pull request, the same deferral shape ADR 0005 uses for the rename.
  • No UI. The hidden diagnostics screen and its bridge module land separately, per the backend/UI separation rule.
  • No iOS wiring. The Rust surface is platform-neutral; the #if DEBUG bridge follows.

Sequencing

The zingo-netutils pin moves to the head of zingolib PR 2742 (nym_bootstrap_events), which defines BootstrapEvent. This pull request retargets the pin to the next zingolib tag once PR 2742 merges. It must not merge before that retarget.

Verification

  • cargo check, cargo clippy --all-targets, and cargo nextest run pass with default features (21 tests) and with --features diagnostics (27 tests).
  • cargo ndk --target aarch64-linux-android check --features diagnostics passes, which covers the composed logcat-plus-capture subscriber.

A note for review

The first cut used a streaming callback interface. The maintainer ruled it out because UniFFI callbacks hand the crate a boxed trait object. The drain replaced it, and every type on the new surface is static. ProxyDeathObserver is untouched.

🤖 Generated with Claude Code

zancas and others added 6 commits August 24, 2026 22:00
The maintainer ruled on 2026-08-19 that the crate becomes mixnet-proxy
and that the word shim is retired from the repository's vocabulary. The
rename itself is deferred to its own pull request because it reaches
every build system. This commit records the decision, the rejected
alternatives, and the open questions about the wallet component's name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mixnet Mode routes a send across three legs, and the app observes none
of them, because nym exposes no connection state and drops the exit's
typed ConnectionError on the floor. ADR 0007 rules the missing
observations into the proxy FFI: a tracing capture layer over nym's two
log targets, a two-arm probe (the Sentinel round trip for legs one and
two, one TLS handshake against a caller-supplied destination for all
three), and a typed BootstrapEvent surface in zingo-netutils, with
every race reporting, reconnects included. The whole surface compiles
behind a cargo feature that only debug wallet builds enable.

CONTEXT.md gains the vocabulary the ruling resolved: Leg, Exit Node,
Destination (renaming correspondent), Sentinel, Capture, Probe, and
Verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The diagnostics feature compiles the observation surface ADR 0007
rules: a tracing capture layer matching nym_gateway_client and
nym_socks5_client_core, one streaming MixnetDiagnosticsObserver fed by
a dedicated forwarder thread, a start_diagnosed constructor that
carries the typed bootstrap narrative across the FFI, and the two
probes. probe_sentinel classifies one Sentinel round trip into
ExitProven, ExitRefused, GatewayLinkDead, or Indeterminate, reading
the exit's own captured words. probe_destination completes one TLS
handshake against a caller-supplied destination through the tunnel,
verifying against the patched Mozilla-bundle verifier per ADR 0006,
and adds HandshakeRefused, TunnelFailed, and DestinationProven.

The plain constructor now rides the same start seam as the observed
one, so the two cannot drift. The zingo-netutils pin moves to the
nym_bootstrap_events head for BootstrapEvent and retargets to the next
zingolib tag once PR 2742 merges. Release builds are byte-equivalent:
the feature is off by default and no build system enables it yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The maintainer ruled against the streaming callback interface: UniFFI
hands the crate a boxed trait object for every callback interface, and
the only consumer is a debug screen that polls its platform bridge
anyway. ADR 0007 now specifies a capped drop-oldest queue behind one
drain_diagnostics export, with the overflow loss reported, and records
the callback as a rejected option.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MixnetDiagnosticsObserver, its forwarder thread, and the
start_diagnosed constructor are gone. Events now land in a capped
drop-oldest queue, and the host polls drain_diagnostics, which
surrenders the queue in order together with the count of events lost
to overflow. Every type on the surface is static, no host code runs on
the mixnet client's threads, and plain start carries the bootstrap
narrative whenever the diagnostics feature is compiled.

An overflow falsifier pins the drop-oldest contract and the loss
report. Both feature graphs pass clippy and their tests (27 with
diagnostics, 21 without), and the aarch64-linux-android check passes
with the feature on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The maintainer ruled that BootstrapEvent::PullFailed carries the
pull's whole typed NetOpFailure, and zingolib's nym_bootstrap_events
branch now implements it (651bcb445). ADR 0007's netutils paragraph
said the failure stays prose; this amendment records the ruling that
replaced it. The FFI mapping adapts when the pin retargets to the next
zingolib tag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant