feat(reachability): gate relay publication with canary quorum - #136
feat(reachability): gate relay publication with canary quorum#136mickvandijke wants to merge 11 commits into
Conversation
fe26eef to
5f4c745
Compare
Add a relay canary request/response protocol so newly acquired MASQUE relay addresses are cold-dialed by independent close-group witnesses before they enter DHT self-record gossip. Keep legacy ADD_ADDRESS relay hints out of DHT records, retain canary-rejected relayers across ordinary acquisition failures, and return typed request timeouts so unreachable relay probes count as failed witness attempts. SemVer: minor
Align the relay canary docs with the randomized non-close witness selection, version the internal canary request/response topic, and add driver-level tests for canary rejection retention across acquisition failures. Add structured rollout fields for witness availability and ineligible responses, and link the transport cleanup follow-up for rejected MASQUE allocations. SemVer: patch
…elayer knowledge Witnesses previously refused to probe a relay address unless they already held a Direct address for the named relayer (relay_canary_addr_matches_relayer_record). Witnesses are chosen as non-close peers while the relayer is drawn from the target's close group, so at scale a random witness almost never knows the relayer: canaries returned RelayerUnknown, quorum fell to InsufficientWitnesses, and relays were never published — leaving NAT'd nodes unreachable. The relayer-knowledge check was only an anti-amplification rail, not part of verification (the cold dial plus identity check needs just the relay address and the target identity). Replace it with a per-source token-bucket rate limiter (one dial per 10s per source, reusing crate::rate_limit::Engine, LRU-bounded), which also subsumes the former per-source in-flight concurrency guard. Throttled sources receive WitnessRateLimited (Ineligible), so they never count as a probe failure and cannot trigger a false relay rejection. SemVer: patch Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The canary exclusion set was preserved on RelayAcquisitionOutcome::Failed but cleared on every other outcome. If the only close Direct candidate is an excluded relayer, acquisition fails every round and that relayer is never retried, leaving the node permanently relay-less. Clear the set on AcquisitionFailed too, matching the InsufficientWitnesses policy: exclusions now accumulate only across a contiguous run of Rejected verdicts and reset on any other outcome. Backoff rate-limits retries and a still-unreachable relay is simply re-excluded the next round. SemVer: patch Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Do not count request-level relay canary errors as failed relay probes. In mixed-version networks an older witness can authenticate and ignore /rr/relay-canary-v1, producing a timeout without ever evaluating the relay. Keep explicit canary-capable DialFailed, IdentityExchangeFailed, and IdentityMismatch responses as eligible relay failures. SemVer: patch
Carry the authoritative accepted connection into LinkTransport, keep provisional and published relay allocations behind one identity-scoped lifecycle, and remove packet-by-packet relay tracing from the hot path.\n\nCompletes WithAutonomi/saorsa-core#138 and supports WithAutonomi/saorsa-core#136.\n\nSemVer: breaking
Bind relay publication to an opaque prepared allocation, require majority maintenance evidence, withdraw an established relay immediately when that quorum rejects it, and keep DHT address publication synchronized with current peers.\n\nConsume saorsa-transport's fix/pr136-provisional-relay branch so PR WithAutonomi#136 is built against the matching provisional-relay lifecycle.\n\nSemVer: patch
5f4c745 to
ae2dbda
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a canary-gated MASQUE relay publication flow in the reachability subsystem, ensuring relay allocations are only published to the DHT after independent third-party verification and periodically revalidated thereafter.
Changes:
- Adds a versioned relay canary request/response protocol and integrates witness selection + quorum verification into relay acquisition/maintenance.
- Refactors proactive relay lifecycle to a prepare → publish/abort model using saorsa-transport’s opaque
PreparedRelayhandle. - Improves address-set publication behavior by tracking acknowledgements and retrying only pending/new K-closest peers.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/transport/saorsa_transport_adapter.rs | Adapts proactive relay lifecycle to prepare/publish/abort and updates relay health API. |
| src/transport_handle.rs | Threads PreparedRelay through setup/publish/abort, and adds TLS-authenticated canary dial plumbing. |
| src/reachability/session.rs | Filters acquisition candidates using a per-round exclusion set and logs prepared relay allocation. |
| src/reachability/mod.rs | Introduces the new canary module and updates module-level documentation. |
| src/reachability/driver.rs | Implements the canary-gated state machine, periodic revalidation, and publication retry targeting. |
| src/reachability/canary.rs | New: relay canary protocol types, witness selection, validation, and probe/quorum logic. |
| src/reachability/acquisition.rs | Switches acquisition output from SocketAddr to PreparedRelay to bind lifecycle to a specific allocation. |
| src/network.rs | Stops merging relay-ish transport hints into DHT routing state; refactors reconnect path to use typed candidates. |
| src/identity/node_identity.rs | Adds derivation of PeerId from transport-authenticated ML-DSA SPKI and tests. |
| src/dht_network_manager.rs | Adds canary request handling + per-source rate limiting and makes address publication concurrent/ack-tracking. |
| src/adaptive/dht.rs | Exposes an internal ensure_peer_channel wrapper for reconnect callers. |
| Cargo.toml | Switches saorsa-transport dependency to a git reference (branch). |
| Cargo.lock | Pins saorsa-transport to the referenced git commit. |
| if let Err(error) = self | ||
| .transport | ||
| .publish_proactive_relay_session(relay.allocation) | ||
| .await |
| let allocated = self.prepare_proactive_relay(relay_addr).await?; | ||
| if let Err(error) = self.publish_proactive_relay(allocated).await { | ||
| let _ = self.abort_proactive_relay(allocated).await; | ||
| return Err(error); | ||
| } | ||
| Ok(allocated.public_addr()) |
|
|
||
| # Networking | ||
| saorsa-transport = "0.35.2" | ||
| saorsa-transport = { git = "https://github.com/WithAutonomi/saorsa-transport.git", branch = "fix/pr136-provisional-relay" } |
|
Agree with the reviews above, both on the canary teardown and on the witness quorum. Nothing to add there. A few smaller things I did not see raised, none of them blocking:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/dht_network_manager.rs:4638
- Invalid relay-canary requests are currently rejected without sending any response. Because the requester treats missing responses as success for mixed-version compatibility, a rejected request can be miscounted as an assumed-success witness result (and could be abused by modified clients). Consider replying with an explicit failure result instead of silently dropping.
if let Err(reason) = validate_relay_canary_request(&source_peer, &request) {
debug!(
peer = %source_peer.to_hex(),
reason = %reason.summary(),
"Rejecting relay canary request"
src/dht_network_manager.rs:5217
- When the isolated relay-canary semaphore is exhausted, the code drops the request without replying. With the current requester logic, that timeout is interpreted as a legacy "no protocol" response (assumed success), which can incorrectly admit/retain a relay under load. Prefer sending an explicit rate-limited response when capacity is exhausted.
let Ok(permit) = semaphore.try_acquire_owned() else {
warn!(
peer = %source_peer.to_hex(),
"Dropping relay canary request: isolated probe budget exhausted"
);
src/reachability/canary.rs:45
- The PR description/compatibility notes mention a
relay-canary-v1topic and that mixed-version request timeouts are treated as ineligible evidence. This implementation usesrelay-canary-v2and explicitly treats missing canary-protocol responses as positive (assumed-success) evidence. Please align the PR description and/or protocol semantics so reviewers/operators know which behavior is intended.
/// Request/response protocol name used with `TransportHandle::send_request`.
pub(crate) const RELAY_CANARY_PROTOCOL: &str = "relay-canary-v2";
/// Wire topic emitted by the request/response wrapper for canary requests.
pub(crate) const RELAY_CANARY_WIRE_TOPIC: &str = "/rr/relay-canary-v2";
# Conflicts: # Cargo.lock # Cargo.toml
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (4)
src/transport_handle.rs:1501
- This doc comment still points to ADR-014, but ADR-016 is introduced as the accepted design and ADR-014 is marked superseded in this PR. Update the ADR reference so readers land on the current reachability design.
/// This is the caller-driven entry point for ADR-014 relay acquisition.
src/transport/saorsa_transport_adapter.rs:2293
connect_happy_eyeballs_authenticateddocuments that the returned address is always normalized, but in the racing (tokio::select!) path the returnedDialedPeer.remote_addris not normalized (and the fallbackreturn Ok(peer)paths also bypass normalization). This can reintroduce IPv4-mapped/unnormalized addresses into callers.
tokio::select! {
res6 = v6_fut => match res6 {
Ok(peer) => Ok(peer),
Err(_) => {
for addr in v4_targets {
src/reachability/canary.rs:45
- PR description says the change adds the versioned internal
relay-canary-v1request/response topic, but the implementation defines and usesrelay-canary-v2(and/rr/relay-canary-v2). Please align the on-wire topic name with the PR description (or update the PR description/ADR accordingly) so mixed-version behavior is predictable.
/// Request/response protocol name used with `TransportHandle::send_request`.
pub(crate) const RELAY_CANARY_PROTOCOL: &str = "relay-canary-v2";
/// Wire topic emitted by the request/response wrapper for canary requests.
pub(crate) const RELAY_CANARY_WIRE_TOPIC: &str = "/rr/relay-canary-v2";
src/transport_handle.rs:1484
- These doc comments still reference ADR-014, but this PR introduces ADR-016 and marks ADR-014 as superseded. Update the reference so readers can find the current design rationale.
This issue also appears on line 1501 of the same file.
/// Returns `true` if no relay was established or the relay is healthy.
/// Returns `false` if a relay was established but the QUIC connection
/// has closed. Used by the relayer monitor (ADR-014 item 6).
pub async fn is_relay_healthy(&self) -> bool {
| let manager_clone = Arc::clone(&self_arc); | ||
| let semaphore = Arc::clone(&self_arc.relay_canary_semaphore); | ||
| let Ok(permit) = semaphore.try_acquire_owned() else { | ||
| warn!( | ||
| peer = %source_peer.to_hex(), | ||
| "Dropping relay canary request: isolated probe budget exhausted" | ||
| ); | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/reachability/canary.rs:56
- The PR description says mixed-version request timeouts should be treated as ineligible evidence, but the canary logic documents and implements
NoProtocolResponse/timeout as an assumed positive result (see comment andRelayCanaryProbeDisposition::AssumedSuccess). Please reconcile this (either update the PR description/ADR text, or adjust the code to treat timeouts as ineligible rather than success).
/// Positive witness results needed before a relay is publishable.
///
/// Publication is unanimous across the selected independent witnesses. During
/// the mixed-version rollout, a request that was delivered but received no
/// canary-protocol response counts as a positive result so legacy nodes do not
/// make relay availability worse than before the canary gate.
const RELAY_CANARY_ADMISSION_SUCCESSES: usize = RELAY_CANARY_WITNESS_TARGET;
src/reachability/driver.rs:494
publish_typed_set_with_policymay attempt to publish an emptytypedaddress set (whenforceis true). In the current DHT implementation, an emptyPublishAddressSetis ignored (DhtCoreEngine::filter_publish_address_setreturnsNoneon empty input), but the network handler still ACKs it, so this "withdraw stale relay" publish will appear successful while leaving prior addresses intact on peers.
let typed = self_addresses.into_typed_vec();
if typed.is_empty() {
info!(
"driver: publishing empty authoritative address set to withdraw stale relay state"
);
}
src/reachability/canary.rs:45
- The PR description says the internal canary topic is
relay-canary-v1, but the implementation defines/usesrelay-canary-v2(and wire topic/rr/relay-canary-v2). Please align the PR description and implementation so mixed-version expectations and any external references match the actual protocol name.
This issue also appears on line 50 of the same file.
/// Request/response protocol name used with `TransportHandle::send_request`.
pub(crate) const RELAY_CANARY_PROTOCOL: &str = "relay-canary-v2";
/// Wire topic emitted by the request/response wrapper for canary requests.
pub(crate) const RELAY_CANARY_WIRE_TOPIC: &str = "/rr/relay-canary-v2";
Linear issue
https://linear.app/autonominetwork/issue/V2-812/gate-proactive-relay-publication-with-independent-canary-verification Tracking issue: #138. Transport PR: WithAutonomi/saorsa-transport#131.
Risk tier
Compatibility
relay-canary-v1request/response topic; mixed-version request timeouts are treated as ineligible evidence rather than relay failures.PreparedRelaylifecycle; no new external crate is added.Semver impact
Summary
Gate proactive MASQUE relay publication behind independent canary evidence and continuously revalidate established relays. A relay allocation is provisional until admission quorum succeeds; a completed maintenance majority rejection withdraws it immediately.
Changes
https://github.com/WithAutonomi/saorsa-transport.git, branchfix/pr136-provisional-relay;Cargo.lockpins transport commit4a2ceb96from PR Revert PR #121 #131.Test evidence
cargo test --lib --quiet: 499 passed, 0 failed.cargo clippy --all-targets --all-features -- -D warnings: clean.cargo test --lib: 1,488 passed, 3 ignored, 0 failed.cargo check --workspace --quiet: clean.pr136-final-1h-0729testnet: 200 nodes, 30% NAT, one continuous 50 MiB uploader; 200/200 node services active, zero restarts, zero established-relay rejection/withdrawal, RelayLost, unhealthy-tunnel, publish/abort/teardown failures, and 11,169 successful maintenance revalidations.4a2ceb96removes it and adds a real-QUIC regression. The requested follow-up fleet was canceled before observation at the requester's direction.New dependency
None. The existing saorsa-transport dependency temporarily follows PR #131's Git branch until that PR is released.
ADR
https://github.com/WithAutonomi/saorsa-core/blob/feat/relay-canary-gate-rc-2026.6.2/docs/adr/ADR-014-proactive-relay-first-nat-traversal.md
Mitigation / rollback
Revert
ae2dbdaand the six original canary commits, then restore the released saorsa-transport dependency.