Blind Relay Abuse Guard

AeroNyxJune 20, 20266 min read154 views

How AeroNyx nodes contain replay, loops, rate abuse, and failing peers while preserving blind encrypted relay and privacy-safe operator evidence.

Blind Relay Abuse Guard is the safety boundary for AeroNyx decentralized encrypted forwarding. It limits abusive or unstable relay behavior without parsing ciphertext, building durable route histories, or turning node operators into traffic observers.

Status and scope

The guard is implemented in Rust and its aggregate results are exposed through node health metadata and Nodeboard. Its job is containment and truthful evidence: it reports whether opaque relay work is accepted, protected, degraded, or stale, never what the encrypted work contains.

ControlState
Blind payload forwardingImplemented
Signed freshness and replay suppressionImplemented
Previous-hop rate limiting and quarantineImplemented
Privacy-safe runtime evidenceImplemented
Nodeboard operator visibilityImplemented
Plaintext or payload inspectionProhibited
User, route, or social-graph analyticsProhibited

The blind-node invariant

A relay may authenticate routing metadata, apply bounded policy, forward an opaque envelope, and return a signed terminal receipt. It must never inspect or infer the payload or expose metadata that can reconstruct a route, a user identity, or a social relationship.

  • plaintext messages, packet payloads, media contents, or MemChain plaintext
  • DNS contents, destinations, domains, URLs, or browsing history
  • route IDs, complete paths, endpoint URLs, or client public IPs
  • full public keys, receiver identities, message IDs, or social-graph edges
  • private keys, voucher secrets, wallet-level traffic, or decryption material

Admission pipeline

Every request passes through bounded checks before it can consume forwarding capacity. The order below is conceptual; all decisions use signed routing metadata and local aggregate state, not decrypted content.

text
verify signed previous_hop and envelope
apply in-flight backpressure
check timestamp freshness
check route replay cache
apply previous-hop rate/quarantine decision
validate TTL, loop safety, next-hop descriptor, and endpoint
forward opaque ciphertext or terminate into pending store

Replay and freshness protection

Replay suppression is local, short-lived, and capacity bounded. Signed envelope timestamps reject stale or implausibly future-dated frames. The values below are current runtime defaults in main, not permanent protocol promises; changing them requires tests and documentation updates.

Runtime constantCurrent default
MAX_BLIND_RELAY_SEEN_ROUTES8192 route IDs
BLIND_RELAY_ROUTE_REPLAY_WINDOW_SECS600 seconds
BLIND_RELAY_PREVIOUS_HOP_RATE_LIMIT120 requests / 60 seconds
BLIND_RELAY_PREVIOUS_HOP_FAILURE_THRESHOLD12 scored failures / 300 seconds
BLIND_RELAY_PREVIOUS_HOP_QUARANTINE_SECS300 seconds
MAX_BLIND_RELAY_PREVIOUS_HOP_BUCKETS4096 buckets
BLIND_RELAY_MAX_ENVELOPE_AGE_SECS600 seconds
BLIND_RELAY_MAX_FUTURE_SKEW_SECS120 seconds
BLIND_RELAY_DELIVERY_RECEIPT_MAX_AGE_SECS120 seconds
MAX_BLIND_RELAY_FORWARD_ATTEMPTS3 attempts

Previous-hop rate limiting and quarantine

Rate limiting is keyed by the signed previous-hop identity. More than 120 requests in the 60-second window starts a five-minute local quarantine. A separate failure score starts the same quarantine after 12 scored validation failures inside five minutes.

text
invalid_previous_hop | invalid_signature | self_loop | route_loop | ttl_exhausted

Only adversarial validation reasons contribute to the failure score. Transport timeouts, lost acknowledgements, and a duplicate route retry do not poison a healthy previous hop. The bucket store is bounded and idle state expires, so the guard cannot become a durable communication graph.

Idempotency and retry semantics

A repeated route ID inside the replay window is an idempotent success response: the node records one aggregate replay drop but does not deliver or forward the envelope again. Transient next-hop failures use at most three attempts with bounded jitter; permanent failures are not retried.

text
duplicate route_id -> accepted=true, reason=duplicate_route, no second delivery
transient next-hop failure -> bounded retry with deterministic jitter
permanent validation failure -> no retry

Aggregate runtime counters

Rust exposes coarse cumulative counters and freshness timestamps under the two paths below. Counters are node-scoped operating evidence, not message logs, billable user analytics, or proof that a particular conversation occurred.

text
system_stats.discovery_status.peer_store.runtime.blind_relay
system_stats.discovery_status.peer_store.peer_health_summary
Counter groupFields
Intake and dispositionreceived, terminal, forwarded, rejected
Validation and protectioninvalid_signature, envelope_too_large, ttl_exhausted, no_route, invalid_endpoint, loop_detected, replay_dropped, timestamp_rejected, rate_limited, quarantined, quarantine_started
Transport and retrybackpressure_dropped, forward_failed, retry_attempted, retry_succeeded, retry_exhausted
Synthetic evidenceprobe_attempted, probe_succeeded, probe_failed, two_hop_probe_attempted, two_hop_probe_succeeded, two_hop_probe_failed
Real delivery and freshnessverified_client_onion_deliveries, last_verified_client_onion_delivery_at, last_accepted_at, last_event_at

Evidence quality semantics

The quality summary separates accepted opaque work, synthetic reachability probes, synthetic two-hop control proofs, and terminal-signed client delivery receipts. real_relay_ready is reserved for a fresh authenticated client-originated delivery receipt from the expected terminal. Synthetic evidence must never be presented as App/user traffic.

statusMeaning
idleNo relay or probe evidence yet.
observingSome evidence exists but readiness is not established.
stalePreviously successful evidence is no longer fresh.
readyFresh accepted work or qualifying proof exists without active transport attention.
protectingAbuse protection counters are active while the relay remains operational.
degradedForwarding or probe failures need investigation.
attentionBackpressure or exhausted retries need immediate operator attention.

proof_scope further distinguishes client_message_delivery, relay_acceptance, message_delivery, control_plane, single_hop_control_plane, attempted, and none. Historical totals remain cumulative, while readiness booleans require fresh evidence and account for active transport failures.

Privacy-safe peer health

peer_health_summary uses a shortened node identifier and coarse health buckets so operators can isolate a failing or quarantined peer without seeing traffic relationships. It is a control-plane diagnostic surface with an explicit privacy boundary.

Allowed:

  • short node_id_prefix
  • coarse health and descriptor state
  • gossip and route-success freshness buckets
  • aggregate route success and failure counts
  • aggregate loop, replay, rate-limit, and quarantine counts
  • quarantine time remaining and bounded reason buckets

Not allowed:

  • full node public keys
  • route IDs or endpoint lists
  • encrypted blobs or payload hashes
  • message IDs or receiver identities
  • client IPs, destinations, or DNS data
  • social-graph edges or who communicates with whom

Operator workflow

Open Nodeboard, choose the node, then use Discovery and Security / Relay Protection. Interpret changes over time and correlate only with node health, reachability, queue pressure, and signed proof freshness.

  1. Confirm discovery descriptors and bootstrap recovery are fresh.
  2. Compare accepted_total, accepted_percent, and last accepted age before declaring the runtime ready.
  3. Distinguish real_relay_ready from synthetic probe readiness; only the former proves an authenticated client-originated terminal receipt.
  4. If protection, degraded, or attention appears, inspect aggregate reason buckets and transport health without requesting user-level logs.

Source map

Documentation uses repository-relative paths so the specification stays valid when infrastructure moves between hosts. Backend and Nodeboard live in separate repositories but consume only owner-scoped, privacy-safe node metadata.

LayerRepository pathRole
Rust relay APIcrates/aeronyx-server/src/api/chat_peer.rsAuthenticates envelopes and applies loop, replay, freshness, rate, quarantine, retry, and terminal receipt rules.
Rust PeerStorecrates/aeronyx-server/src/services/peer_store.rsStores bounded aggregate counters, peer health, readiness, and proof classifications.
Rust health APIcrates/aeronyx-server/src/api/vpn_health.rsPublishes local privacy-safe health JSON.
Rust reportercrates/aeronyx-server/src/management/reporter.rsCarries aggregate status in node heartbeat metadata.
Backend observabilityprivacy_network/api/vpn_observability.pyReturns owner-scoped system metadata to the operator console.
Nodeboard typestypes/index.tsDefines blind-relay and peer-health response types.
Nodeboard detail and i18napp/dashboard/nodes/[id]/page.tsx and lib/i18n/index.tsRenders Security / Relay Protection with localized privacy-boundary copy.

Foundation for multi-hop routing

Multi-hop routing needs replay resistance, loop containment, bounded retries, peer quarantine, and evidence that cannot be confused with user traffic. This guard provides that base for layered encryption and future route diversity without weakening the blind-node invariant.

Developer rules

Treat every new field as a privacy review. A useful operator metric must answer a node-level reliability question without identifying the payload, sender, receiver, path, endpoint, or conversation.

  1. Keep payload_b64 opaque in every relay path.
  2. Add only aggregate counters or bounded reason buckets.
  3. Never join counters with route IDs, endpoints, users, receivers, or message metadata.
  4. Keep synthetic probe totals out of encrypted message, packet, and byte totals.
  5. Update Rust tests, Nodeboard types, and all language versions of this page whenever semantics change.

Node discovery and verifiable encrypted relay delivery