Blind Relay Abuse Guard
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.
| Control | State |
|---|---|
| Blind payload forwarding | Implemented |
| Signed freshness and replay suppression | Implemented |
| Previous-hop rate limiting and quarantine | Implemented |
| Privacy-safe runtime evidence | Implemented |
| Nodeboard operator visibility | Implemented |
| Plaintext or payload inspection | Prohibited |
| User, route, or social-graph analytics | Prohibited |
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.
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 constant | Current default |
|---|---|
MAX_BLIND_RELAY_SEEN_ROUTES | 8192 route IDs |
BLIND_RELAY_ROUTE_REPLAY_WINDOW_SECS | 600 seconds |
BLIND_RELAY_PREVIOUS_HOP_RATE_LIMIT | 120 requests / 60 seconds |
BLIND_RELAY_PREVIOUS_HOP_FAILURE_THRESHOLD | 12 scored failures / 300 seconds |
BLIND_RELAY_PREVIOUS_HOP_QUARANTINE_SECS | 300 seconds |
MAX_BLIND_RELAY_PREVIOUS_HOP_BUCKETS | 4096 buckets |
BLIND_RELAY_MAX_ENVELOPE_AGE_SECS | 600 seconds |
BLIND_RELAY_MAX_FUTURE_SKEW_SECS | 120 seconds |
BLIND_RELAY_DELIVERY_RECEIPT_MAX_AGE_SECS | 120 seconds |
MAX_BLIND_RELAY_FORWARD_ATTEMPTS | 3 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.
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.
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.
system_stats.discovery_status.peer_store.runtime.blind_relay
system_stats.discovery_status.peer_store.peer_health_summary
| Counter group | Fields |
|---|---|
| Intake and disposition | received, terminal, forwarded, rejected |
| Validation and protection | invalid_signature, envelope_too_large, ttl_exhausted, no_route, invalid_endpoint, loop_detected, replay_dropped, timestamp_rejected, rate_limited, quarantined, quarantine_started |
| Transport and retry | backpressure_dropped, forward_failed, retry_attempted, retry_succeeded, retry_exhausted |
| Synthetic evidence | probe_attempted, probe_succeeded, probe_failed, two_hop_probe_attempted, two_hop_probe_succeeded, two_hop_probe_failed |
| Real delivery and freshness | verified_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.
status | Meaning |
|---|---|
idle | No relay or probe evidence yet. |
observing | Some evidence exists but readiness is not established. |
stale | Previously successful evidence is no longer fresh. |
ready | Fresh accepted work or qualifying proof exists without active transport attention. |
protecting | Abuse protection counters are active while the relay remains operational. |
degraded | Forwarding or probe failures need investigation. |
attention | Backpressure 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.
- Confirm discovery descriptors and bootstrap recovery are fresh.
- Compare
accepted_total,accepted_percent, and last accepted age before declaring the runtime ready. - Distinguish
real_relay_readyfrom synthetic probe readiness; only the former proves an authenticated client-originated terminal receipt. - 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.
| Layer | Repository path | Role |
|---|---|---|
| Rust relay API | crates/aeronyx-server/src/api/chat_peer.rs | Authenticates envelopes and applies loop, replay, freshness, rate, quarantine, retry, and terminal receipt rules. |
| Rust PeerStore | crates/aeronyx-server/src/services/peer_store.rs | Stores bounded aggregate counters, peer health, readiness, and proof classifications. |
| Rust health API | crates/aeronyx-server/src/api/vpn_health.rs | Publishes local privacy-safe health JSON. |
| Rust reporter | crates/aeronyx-server/src/management/reporter.rs | Carries aggregate status in node heartbeat metadata. |
| Backend observability | privacy_network/api/vpn_observability.py | Returns owner-scoped system metadata to the operator console. |
| Nodeboard types | types/index.ts | Defines blind-relay and peer-health response types. |
| Nodeboard detail and i18n | app/dashboard/nodes/[id]/page.tsx and lib/i18n/index.ts | Renders 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.
- Keep
payload_b64opaque in every relay path. - Add only aggregate counters or bounded reason buckets.
- Never join counters with route IDs, endpoints, users, receivers, or message metadata.
- Keep synthetic probe totals out of encrypted message, packet, and byte totals.
- Update Rust tests, Nodeboard types, and all language versions of this page whenever semantics change.