Skip to content

iroh-lan improvements#5

Merged
rustonbsd merged 15 commits into
mainfrom
iroh-lan-improvements
May 28, 2026
Merged

iroh-lan improvements#5
rustonbsd merged 15 commits into
mainfrom
iroh-lan-improvements

Conversation

@rustonbsd

@rustonbsd rustonbsd commented Mar 23, 2026

Copy link
Copy Markdown
Owner

learnings and stability improvements that came out of network stress testing of iroh-lan #PR10

  • Cargo / versioning

    • Crate version set to 0.2.0-pre.1.
    • iroh bumped 0.97 → 0.98; optional iroh-gossip 0.97 → 0.98; rand 0.9 → 0.10.
    • Added lru = "0.17".
    • tokio features made explicit: ["macros", "rt-multi-thread", "sync", "time"].
    • tracing-subscriber/env-filter and anyhow kept as optional; gossip-example feature extended to enable tokio/signal.
    • Features: default = [], gossip-example = ["iroh-gossip","anyhow","tracing-subscriber","tokio/signal"].
  • README & examples

    • README installation pinned to iroh = "0.98" and iroh-auth = "0.2.0-pre.1".
    • Endpoint construction in examples uses Endpoint::builder(iroh::endpoint::presets::N0).
    • Examples now await authenticator endpoint setup (auth.set_endpoint(&endpoint).await) before building routers.
    • Mermaid notes simplified (removed leading emoji).
  • Crate layout / large refactor

    • Replaced large inline implementation in src/lib.rs with modular layout:
      • New modules: src/auth.rs, src/protocol.rs, src/secret.rs, src/error.rs, src/tests.rs.
      • lib.rs now only wires modules, re-exports, and file-local constants.
    • Public re-exports preserved: Authenticator, AuthenticatorError, IntoSecret, ALPN.
  • Public API & traits

    • New trait IntoSecret with impls for SecretSlice, String, &str, Vec, &[u8], &[u8; N], Box<[u8]>.
    • Authenticator: new public struct with:
      • pub const ALPN (&[u8] = b"/iroh/auth/0.1").
      • pub fn new<S: IntoSecret>(secret: S) -> Self.
      • pub async fn set_endpoint(&self, endpoint: &Endpoint).
      • pub async fn is_authenticated(&self, id: &PublicKey) -> bool.
      • Test-only helpers: list_authenticated, list_blocked.
    • AuthState enum added: {Unauthenticated, InFlight, Authenticated, Blocked} with Display impl.
  • Timeouts, constants & caches

    • AUTH_TIMEOUT file-local constant set to 30s.
    • TRANSMISSION_TIMEOUT file-local constant set to 5000 ms.
    • LRU_CACHE_SIZE constant = 10_000; per-EndpointId auth state stored in an LruCache<EndpointId, WatchableRemote>.
  • Authentication implementation (src/auth.rs)

    • Implements SPAKE2-based mutual auth:
      • auth_accept (incoming) and auth_open (outgoing) perform SPAKE2, HKDF-SHA512 key expansion (accept/open contexts), constant-time verification, bounded reads/writes.
      • end_of_auth drains/finishes streams with timeout semantics.
    • Authenticator holds secret (SecretSlice), optional Endpoint (Arc<Mutex<Option>>), and auth_state (Arc<Mutex<LruCache<...>>>).
    • WatchableRemote wrapper around n0_watcher::Watchable with identity-based Eq/Hash.
    • perform_auth / background worker behavior: repeatedly attempts outgoing auth until AUTH_TIMEOUT, promotes to Authenticated on success, marks Blocked on block-worthy failures, retries non-block failures with 1s backoff.
  • Protocol integration (src/protocol.rs)

    • Authenticator implements iroh::protocol::ProtocolHandler::accept and iroh::endpoint::EndpointHooks (after_handshake, before_connect).
    • after_handshake:
      • Accepts auth ALPN directly.
      • For application ALPNs, consults LRU cache watcher: rejects when no InFlight entry, waits up to AUTH_TIMEOUT for in-flight completion, maps to 401/403.
    • before_connect:
      • Accepts when already authenticated or when initiating an auth ALPN connection.
      • Registers in-flight entries for outgoing auth and spawns background auth workers where appropriate.
    • Helpers (pub(crate)): register_in_flight, release_in_flight, get_auth_state to manage the LRU-backed watchers; LRU evictions are logged.
  • Errors

    • New src/error.rs defines:
      • pub enum AuthenticatorError: AddFailed, AcceptFailed(String), OpenFailed(String), AcceptFailedAndBlock(String, EndpointId), OpenFailedAndBlock(String, EndpointId), EndpointNotSet.
      • pub enum InFlightError: PromotionNotAllowed(String).
      • Display and std::error::Error impls for both enums.
    • Network/timeouts and protocol failures map to specific AuthenticatorError variants (including "and block" variants carrying EndpointId).
  • Tests (new)

    • New #[cfg(test)] module in src/tests.rs:
      • Unit: SPAKE2 token/key agreement test.
      • Integration: test_auth_success, test_auth_failure, test_auth_parallel (parallel stress with configurable ALPNs).
      • Helpers build real iroh Endpoints/Router, use Authenticator hooks, poll auth state with bounded timeouts (timeout = AUTH_TIMEOUT * 2).
      • DummyProtocol provided to satisfy router acceptance.
  • Examples adjustments

    • examples/basic.rs and examples/gossip.rs updated to await auth.set_endpoint(&endpoint).await.
    • gossip example uses Endpoint::builder(presets::N0), integrates gossip-example feature and tokio/signal handler.
  • Lines & churn

    • lib.rs drastically reduced; major deletions from inline implementation and large additions across new modules:
      • auth.rs (~+560 lines), protocol.rs (+328), tests.rs (+~300), secret.rs (+49), error.rs (+48).
    • Cargo.toml small diffs: +17/-9 lines.
  • Behavioral / migration notes

    • Internals and concurrency semantics changed: per-EndpointId LRU-backed watcher model, background outbound auth workers, async set_endpoint required before initiating outgoing auth.
    • Previously-public AUTH_TIMEOUT is now file-local; TRANSMISSION_TIMEOUT and LRU_CACHE_SIZE are internal constants consumers need not rely on.
    • Consumers must ensure to call auth.set_endpoint(&endpoint).await after binding endpoint and before relying on outgoing auth.
  • TODOs / outstanding verification (author notes)

    • Add test covering simultaneous incoming and outgoing auth connections between the same peer.
    • Run reliability tests in iroh-lan under network degradation to validate stability improvements.

@rustonbsd

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Modularizes the previous inline authentication into four modules (auth, protocol, error, secret); adds an LRU-backed per-peer auth cache, SPAKE2 mutual auth (accept/open), HKDF-derived keys, protocol hooks enforcing auth on connect/accept, tests, and updates to Cargo/README dependencies and example awaits.

Changes

Cohort / File(s) Summary
Manifest & Docs
Cargo.toml, README.md
Bumped crate version to 0.2.0-pre.1, updated iroh/iroh-gossip/rand, added lru, enabled explicit tokio features, extended gossip-example feature; README updated endpoint preset usage and removed certain emoji from Mermaid notes.
Crate root / re-exports
src/lib.rs
Replaced large inline auth code with module declarations and pub use re-exports for auth, error, protocol, secret; moved types/constants into submodules; made AUTH_TIMEOUT and new TRANSMISSION_TIMEOUT/LRU_CACHE_SIZE file-local; replaced inline tests with mod tests;.
Authenticator implementation
src/auth.rs
New Authenticator with shared secret storage, optional async Endpoint, LRU-backed per-EndpointId Watchable states, AuthState enum, SPAKE2-based auth_accept/auth_open, HKDF-SHA512 key expansion, constant-time verification, stream drain/finalization, and perform_auth retry/timeout worker; public API includes new, set_endpoint, is_authenticated, test helpers.
Protocol integration & helpers
src/protocol.rs
New ProtocolHandler and EndpointHooks implementations tying auth into accept, after_handshake, and before_connect; maps auth outcomes to AcceptError/handshake decisions; adds register_in_flight, release_in_flight, and get_auth_state helpers managing the LRU cache and watcher lifecycle with eviction logging.
Error types
src/error.rs
New AuthenticatorError and InFlightError enums with Display and std::error::Error impls, covering accept/open failures, block semantics, endpoint-not-set, and add/promotion errors.
Secret conversions
src/secret.rs
New IntoSecret trait and implementations for SecretSlice<u8>, String, &str, Vec<u8>, &[u8], fixed-size arrays, and Box<[u8]>.
Tests
src/tests.rs
New test module exercising SPAKE2 token/key agreement, end-to-end auth success/failure, parallel auth runs, protocol wiring with a DummyProtocol, and IntoSecret conversions; uses Tokio multi-thread runtime and bounded timeouts.
Examples
examples/basic.rs, examples/gossip.rs
Examples updated to construct endpoints via iroh::endpoint::presets::N0 and to await auth.set_endpoint(&endpoint) before building/spawning routers.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Auth as Authenticator
    participant Endpoint as iroh::Endpoint
    participant Remote as RemotePeer
    participant Worker as AuthWorker

    rect rgba(200, 230, 255, 0.5)
    Note over Client,Auth: Connect-time registration and gated handshake
    end

    Client->>Auth: before_connect(remote_id, alpn)
    Auth->>Auth: register_in_flight(remote_id)
    Auth-->>Endpoint: spawn_auth_worker(remote_id) (if endpoint set)
    Auth->>Client: provisional accept/reject

    Client->>Remote: perform handshake
    Remote->>Auth: accept(connection) -> auth_accept(connection)
    Auth->>Worker: background auth_open -> SPAKE2 exchange
    Worker->>Remote: open stream, exchange derived keys
    Worker->>Auth: update auth_state -> Authenticated / Blocked / Unauthenticated

    Endpoint->>Auth: after_handshake(conn_info)
    Auth->>Auth: get_auth_state(remote_id) / wait watcher (AUTH_TIMEOUT)
    Auth-->>Endpoint: accept or reject handshake
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'iroh-lan improvements' is vague and does not accurately represent the substantial refactoring performed. Replace with a more specific title reflecting the main change, such as 'Refactor: Extract authentication logic into separate modules and add LRU state caching'.
Docstring Coverage ⚠️ Warning Docstring coverage is 35.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib.rs`:
- Around line 913-915: The test is configuring an external relay via
RelayMap::try_from_iter(["https://iroh-relay.rustonbsd.com"]) which creates an
external network dependency; remove or replace that by either (a) not calling
relay_mode at all (rely on default/local transports) or (b) using a local/test
relay address, or (c) mark the test as #[ignore] to make it an
integration/stress test; update the configuration around
clear_relay_transports(), relay_mode(...), and hooks(auth_a.clone()) accordingly
so unit tests do not contact the external URL.
- Around line 294-300: The current release_in_flight implementation (using
self.in_flight.lock(), .entry(endpoint_id).or_insert_with(||
WatchableEndpoint::new(None)).set(None)) leaves stale HashMap entries and even
creates new ones; change it to remove the map entry instead of inserting and
setting to None: lock self.in_flight, try to remove the endpoint_id (e.g.,
in_flight.remove(&endpoint_id)), and if a watcher was returned call
watcher.set(None) on it. Replace all similar patterns (the block, timeout paths,
and after_handshake timeout/reject paths that use
.entry(...).or_insert_with(...).set(None)) with this removal helper so no
orphaned entries are left and no new entries are created.
- Around line 907-911: The ALPN loop is wrong: endpoint_a_builder.alpns(...)
replaces the list each call so only the last "/dummy/{i}" is registered; instead
build a Vec of all ALPN byte vectors (e.g., map 0..parallel_count to
format!("/dummy/{}", i).into_bytes()), then call
endpoint_a_builder.alpns(all_alpns) exactly once (look for endpoint_a_builder
and .alpns usage). Also update the connection handling (where results are
currently silenced with .ok()) to collect each connection Result into a Vec,
assert success (e.g., expect or assert_ok) for every entry so failures are not
discarded — adjust the code paths around parallel_count connection spawns to
push results and verify them.
- Around line 798-808: The test currently spawns tasks with tokio::spawn and
waits on a shared atomic `worked`, which can hang forever if a spawned task
panics; instead collect the JoinHandles (or use tokio::task::JoinSet) for the
tasks created in the loop that call `run_auth_parallel_test(secret, secret,
count)`, await them all (checking each handle’s Result to detect panics/errors)
and wrap the entire wait/join phase in a `tokio::time::timeout(...)` so the test
fails fast on panics or non-termination; update the loop that creates tasks (the
block using `tokio::spawn` and `worked`) to push handles into a Vec or JoinSet
and then await/join them under a timeout rather than polling `worked`
indefinitely.
- Around line 562-584: Replace the Option<PublicKey> in the in-flight watcher
with an explicit AuthState enum (e.g., enum AuthState { Pending,
Authenticated(PublicKey) }) and initialize the watcher with AuthState::Pending
instead of WatchableEndpoint::new(None) where the entry is created in the
in_flight map; update any code that previously called
set(Some(pubkey))/set(None) (including release_in_flight) to set
AuthState::Authenticated(public_key) on success and remove the entry (or set a
terminal state) on timeout/reject so notifications occur reliably; finally
update the wait_for_auth async waiter (the block that reads
in_flight_watcher.watch().stream() and checks
in_flight/is_authenticated(&endpoint_id)) to treat AuthState::Authenticated(_)
as completion and AuthState::Pending as waiting.
- Around line 317-331: The final stream drain uses recv.read_to_end(usize::MAX)
which allows an untrusted peer to force unbounded buffering; change the drain to
(1) use a small bounded maximum (e.g., a few KB) instead of usize::MAX and (2)
wrap the await in a timeout using TRANSMISSION_TIMEOUT so the task fails if the
peer keeps sending; preserve the existing error mapping to
AuthenticatorError::OpenFailed/AcceptFailed and keep the warn!("[end_of_auth]
{}", err) path so timeouts or size-limit errors are logged consistently when
recv.read_to_end (or its replacement) returns an error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9a0cabf5-fe4e-44e2-9142-7aba63a5b79b

📥 Commits

Reviewing files that changed from the base of the PR and between 4294fad and 08e8930.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • Cargo.toml
  • README.md
  • src/lib.rs

Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
Comment on lines +798 to +808
for _ in 0..count {
let worked = worked.clone();
tokio::spawn(async move {
assert!(run_auth_parallel_test(secret, secret, count).await.unwrap());
worked.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
});
}

while worked.load(std::sync::atomic::Ordering::SeqCst) < count {
tokio::time::sleep(Duration::from_millis(100)).await;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Join spawned tasks under a timeout.

A panic inside any spawned task is never observed, and worked will stop incrementing, causing this test to loop forever. Collect the join handles or use JoinSet, then wrap the whole wait in a timeout.

🧪 Proposed direction
-        for _ in 0..count {
+        let mut tasks = tokio::task::JoinSet::new();
+        for _ in 0..count {
             let worked = worked.clone();
-            tokio::spawn(async move {
-                assert!(run_auth_parallel_test(secret, secret, count).await.unwrap());
+            tasks.spawn(async move {
+                let result = run_auth_parallel_test(secret, secret, count).await;
+                assert!(result.unwrap());
                 worked.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
             });
         }
 
-        while worked.load(std::sync::atomic::Ordering::SeqCst) < count {
-            tokio::time::sleep(Duration::from_millis(100)).await;
-        }
+        timeout(AUTH_TIMEOUT * 4, async {
+            while let Some(result) = tasks.join_next().await {
+                result.unwrap();
+            }
+        })
+        .await
+        .expect("parallel auth test timed out");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib.rs` around lines 798 - 808, The test currently spawns tasks with
tokio::spawn and waits on a shared atomic `worked`, which can hang forever if a
spawned task panics; instead collect the JoinHandles (or use
tokio::task::JoinSet) for the tasks created in the loop that call
`run_auth_parallel_test(secret, secret, count)`, await them all (checking each
handle’s Result to detect panics/errors) and wrap the entire wait/join phase in
a `tokio::time::timeout(...)` so the test fails fast on panics or
non-termination; update the loop that creates tasks (the block using
`tokio::spawn` and `worked`) to push handles into a Vec or JoinSet and then
await/join them under a timeout rather than polling `worked` indefinitely.

Comment thread src/lib.rs Outdated
Comment on lines +907 to +911
let mut endpoint_a_builder = iroh::Endpoint::builder(iroh::endpoint::presets::N0);
for i in 0..parallel_count {
endpoint_a_builder =
endpoint_a_builder.alpns(vec![format!("/dummy/{}", i).into_bytes()]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

iroh 0.97.0 Endpoint builder alpns method accumulate or replace

💡 Result:

In iroh 0.97.0, the EndpointBuilder's alpns method sets (replaces) the list of ALPN protocols that the endpoint will accept on incoming connections. It takes a Vec<Vec> and overrides any previously set ALPNs. There is no built-in "accumulate" behavior; calling alpns multiple times will replace the list each time. To accumulate ALPNs, retrieve the current list (if the Builder exposed it publicly, which it does not in the current API), append new ones, and set again—or construct the full list in one call. To accept incoming connections, at least one ALPN must be set. Not setting any allows outbound connections but not inbound. Example usage: let endpoint = Endpoint::builder(presets::N0) .alpns(vec![b"my-protocol".to_vec, b"another".to_vec]) .bind .await?; Source code confirms alpn_protocols: Vec<Vec> is directly replaced in the builder struct.

Citations:


Fix ALPN loop: builder.alpns() replaces, not accumulates—only the last ALPN is registered.

The current loop repeatedly calls .alpns(), which overwrites the previous ALPN list each iteration. This leaves only the final /dummy/{parallel_count-1} ALPN registered. Parallel connections attempting other ALPNs will fail to negotiate. Build the complete ALPN list once and call .alpns() once. Additionally, collect and assert every connection result instead of discarding with .ok().

🧪 Proposed direction
-        let mut endpoint_a_builder = iroh::Endpoint::builder(iroh::endpoint::presets::N0);
-        for i in 0..parallel_count {
-            endpoint_a_builder =
-                endpoint_a_builder.alpns(vec![format!("/dummy/{}", i).into_bytes()]);
-        }
+        let alpns: Vec<Vec<u8>> = (0..parallel_count)
+            .map(|i| format!("/dummy/{}", i).into_bytes())
+            .collect();
+        let endpoint_a_builder =
+            iroh::Endpoint::builder(iroh::endpoint::presets::N0).alpns(alpns.clone());
-        for i in 0..parallel_count {
+        let mut connects = tokio::task::JoinSet::new();
+        for i in 0..parallel_count {
             tokio::time::sleep(Duration::from_millis(100)).await;
-            spawn({
+            connects.spawn({
                 let endpoint_a = endpoint_a.clone();
                 let endpoint_b = endpoint_b.clone();
                 async move {
+                    let alpn = format!("/dummy/{}", i).into_bytes();
                     endpoint_a
-                        .connect(
-                            endpoint_b.addr(),
-                            format!("/dummy/{}", i).into_bytes().as_slice(),
-                        )
+                        .connect(endpoint_b.addr(), alpn)
                         .await
-                        .ok();
                 }
             });
         }
+
+        while let Some(result) = connects.join_next().await {
+            result.map_err(|e| e.to_string())?
+                .map_err(|e| e.to_string())?;
+        }

Also applies to: 923-926, 954-999

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib.rs` around lines 907 - 911, The ALPN loop is wrong:
endpoint_a_builder.alpns(...) replaces the list each call so only the last
"/dummy/{i}" is registered; instead build a Vec of all ALPN byte vectors (e.g.,
map 0..parallel_count to format!("/dummy/{}", i).into_bytes()), then call
endpoint_a_builder.alpns(all_alpns) exactly once (look for endpoint_a_builder
and .alpns usage). Also update the connection handling (where results are
currently silenced with .ok()) to collect each connection Result into a Vec,
assert success (e.g., expect or assert_ok) for every entry so failures are not
discarded — adjust the code paths around parallel_count connection spawns to
push results and verify them.

Comment thread src/lib.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib.rs (1)

681-702: ⚠️ Potential issue | 🟡 Minor

Blocked peers should reject with 403, not 401.

When wait_for_auth completes because the state transitions to AuthState::Blocked, is_authenticated returns false and this branch rejects with VarInt::from_u32(401) + "Authentication failed", even though a few lines above (line 652) the explicit Blocked branch correctly uses 403. Surface the terminal state to the caller so the reject code is consistent with the Blocked path earlier in this function.

🧹 Proposed direction
-        let wait_for_auth = async {
-            let mut stream = in_flight_watcher.watch().stream();
-            while let Some(in_flight) = stream.next().await {
-                if matches!(
-                    in_flight,
-                    AuthState::Unauthenticated | AuthState::Authenticated | AuthState::Blocked
-                ) {
-                    return;
-                }
-            }
-        };
-
-        match timeout(AUTH_TIMEOUT, wait_for_auth).await {
-            Ok(_) => {
-                if self.is_authenticated(&endpoint_id) {
-                    AfterHandshakeOutcome::accept()
-                } else {
-                    AfterHandshakeOutcome::Reject {
-                        error_code: VarInt::from_u32(401),
-                        reason: b"Authentication failed".to_vec(),
-                    }
-                }
-            }
+        let wait_for_auth = async {
+            let mut stream = in_flight_watcher.watch().stream();
+            while let Some(state) = stream.next().await {
+                if matches!(
+                    state,
+                    AuthState::Unauthenticated | AuthState::Authenticated | AuthState::Blocked
+                ) {
+                    return Some(state);
+                }
+            }
+            None
+        };
+
+        match timeout(AUTH_TIMEOUT, wait_for_auth).await {
+            Ok(Some(AuthState::Authenticated)) => AfterHandshakeOutcome::accept(),
+            Ok(Some(AuthState::Blocked)) => AfterHandshakeOutcome::Reject {
+                error_code: VarInt::from_u32(403),
+                reason: b"Endpoint is blocked".to_vec(),
+            },
+            Ok(_) => AfterHandshakeOutcome::Reject {
+                error_code: VarInt::from_u32(401),
+                reason: b"Authentication failed".to_vec(),
+            },
             Err(_) => { ... }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib.rs` around lines 681 - 702, The current timeout branch returns a
generic 401 when wait_for_auth finishes but is_authenticated(&endpoint_id) is
false; instead, inspect the final authentication state produced by wait_for_auth
(or query the same AuthState used earlier) and if that terminal state is
AuthState::Blocked return AfterHandshakeOutcome::Reject with
VarInt::from_u32(403) and an appropriate "Authentication blocked" reason,
otherwise keep the 401/"Authentication failed" behavior; update the logic around
timeout(AUTH_TIMEOUT, wait_for_auth).await to use the final AuthState rather
than only is_authenticated when choosing the reject code so it matches the
explicit Blocked branch above.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib.rs`:
- Line 141: The auth_state HashMap is never pruning entries, causing unbounded
growth; update register_in_flight/release_in_flight and the failure paths in
ProtocolHandler::accept and EndpointHooks::after_handshake to remove auth_state
entries (the WatchableRemote for an EndpointId) when a peer lands in terminal
Unauthenticated or on timeout, and implement eviction for Blocked entries
(either remove on transition after a TTL or enforce a capped size) so
transient/failed peers do not permanently remain in auth_state; specifically,
locate the auth_state field and calls to register_in_flight/release_in_flight
and ensure release_in_flight removes the map entry for Unauthenticated, add
removal on timeout paths in ProtocolHandler::accept, and add TTL or size-cap
logic when inserting Blocked WatchableRemote entries.
- Around line 203-220: The PartialEq impl for WatchableRemote currently compares
self.state() but Hash uses self.id(), violating the Eq/Hash contract; change the
impl PartialEq for WatchableRemote to compare IDs (use self.id() == other.id())
so equality and hashing both rely on id(), or remove/rename the state-based
equality; ensure the impl PartialEq<PublicKey> remains comparing self.id() to
&PublicKey and keep impl std::hash::Hash hashing self.id() so all equality and
hashing consistently use id() (refer to the impl blocks named PartialEq for
WatchableRemote, PartialEq<PublicKey> for WatchableRemote, the Hash impl, and
the methods id() and state()).

---

Outside diff comments:
In `@src/lib.rs`:
- Around line 681-702: The current timeout branch returns a generic 401 when
wait_for_auth finishes but is_authenticated(&endpoint_id) is false; instead,
inspect the final authentication state produced by wait_for_auth (or query the
same AuthState used earlier) and if that terminal state is AuthState::Blocked
return AfterHandshakeOutcome::Reject with VarInt::from_u32(403) and an
appropriate "Authentication blocked" reason, otherwise keep the
401/"Authentication failed" behavior; update the logic around
timeout(AUTH_TIMEOUT, wait_for_auth).await to use the final AuthState rather
than only is_authenticated when choosing the reject code so it matches the
explicit Blocked branch above.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5759d919-f8ca-458c-b8eb-90a2063beb02

📥 Commits

Reviewing files that changed from the base of the PR and between 08e8930 and 7a89f10.

📒 Files selected for processing (1)
  • src/lib.rs

Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
@rustonbsd

rustonbsd commented Apr 21, 2026

Copy link
Copy Markdown
Owner Author

todo:

  • what happens when an incoming and outgoing auth connection happen simultaneously from the perspective of one peer (add test)
  • test in network degradation reliability-test in iroh-lan

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (3)
src/tests.rs (2)

163-225: ⚠️ Potential issue | 🔴 Critical

Build the full ALPN list once and assert every connection result.

Lines 165 and 178 appear to overwrite the endpoint ALPN list on each loop iteration, leaving only the last /dummy/{i} route registered. Lines 216-222 then discard connection failures, so the stress test can pass without exercising the intended parallel routes.

🧪 Proposed direction
-        let mut endpoint_a_builder = iroh::Endpoint::builder(iroh::endpoint::presets::N0);
-        for i in 0..parallel_count {
-            endpoint_a_builder =
-                endpoint_a_builder.alpns(vec![format!("/dummy/{}", i).into_bytes()]);
-        }
+        let alpns: Vec<Vec<u8>> = (0..parallel_count)
+            .map(|i| format!("/dummy/{}", i).into_bytes())
+            .collect();
+        let endpoint_a_builder =
+            iroh::Endpoint::builder(iroh::endpoint::presets::N0).alpns(alpns.clone());
@@
-        let mut endpoint_b_builder = iroh::Endpoint::builder(iroh::endpoint::presets::N0);
-        for i in 0..parallel_count {
-            endpoint_b_builder =
-                endpoint_b_builder.alpns(vec![format!("/dummy/{}", i).into_bytes()]);
-        }
+        let endpoint_b_builder =
+            iroh::Endpoint::builder(iroh::endpoint::presets::N0).alpns(alpns);
@@
-        for i in 0..parallel_count {
+        let mut connects = tokio::task::JoinSet::new();
+        for i in 0..parallel_count {
             tokio::time::sleep(Duration::from_millis(100)).await;
-            tokio::spawn({
+            connects.spawn({
                 let endpoint_a = endpoint_a.clone();
                 let endpoint_b = endpoint_b.clone();
                 async move {
+                    let alpn = format!("/dummy/{}", i).into_bytes();
                     endpoint_a
-                        .connect(
-                            endpoint_b.addr(),
-                            format!("/dummy/{}", i).into_bytes().as_slice(),
-                        )
+                        .connect(endpoint_b.addr(), alpn)
                         .await
-                        .ok();
                 }
             });
         }
+
+        while let Some(result) = connects.join_next().await {
+            result
+                .expect("connect task panicked")
+                .map_err(|e| e.to_string())?;
+        }

Please verify the builder semantics for the exact iroh version:

iroh 0.98 Endpoint builder alpns method replaces or appends ALPN protocols
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tests.rs` around lines 163 - 225, The ALPN list is being overwritten in
each loop by repeated calls to Endpoint::builder(...).alpns(...) (see
endpoint_a_builder and endpoint_b_builder); instead build a single Vec of all
ALPN bytes (e.g. collect format!("/dummy/{}", i).into_bytes() for
0..parallel_count) and call .alpns(...) once on the builder before
.hooks(...).bind(); also stop discarding connection results in the tokio::spawn
block — capture the Result from endpoint_a.connect(...).await and assert or
propagate errors (use expect/map_err? or assert!(result.is_ok())) so connection
failures to any /dummy/{i} are detected.

62-75: ⚠️ Potential issue | 🟠 Major

Join spawned auth tasks under a timeout.

A panic before worked.fetch_add(...) leaves this test spinning forever. Use JoinSet/handles and wrap the join phase in timeout(...).

🧪 Proposed direction
-        let worked = Arc::new(AtomicUsize::new(0));
         let count = 10;
+        let mut tasks = tokio::task::JoinSet::new();
         for _ in 0..count {
-            let worked = worked.clone();
-            tokio::spawn(async move {
+            tasks.spawn(async move {
                 assert!(run_auth_parallel_test(secret, secret, count).await.unwrap());
-                worked.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
             });
         }
 
-        while worked.load(std::sync::atomic::Ordering::SeqCst) < count {
-            tokio::time::sleep(Duration::from_millis(100)).await;
-        }
+        timeout(AUTH_TIMEOUT * 4, async {
+            while let Some(result) = tasks.join_next().await {
+                result.expect("parallel auth task panicked");
+            }
+        })
+        .await
+        .expect("parallel auth test timed out");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tests.rs` around lines 62 - 75, The test spawns tasks with tokio::spawn
and relies on worked.fetch_add to finish, which can hang if a task panics;
replace the detached spawns with a JoinSet or collect JoinHandles for the tasks
that call run_auth_parallel_test(secret, secret, count) and then join them,
wrapping the join phase in tokio::time::timeout to fail the test instead of
spinning forever; reference the existing symbols worked, count,
run_auth_parallel_test, and the tokio::spawn usage so you know where to replace
spawns with JoinSet/handles and apply timeout around awaiting all joins.
src/protocol.rs (1)

279-314: ⚠️ Potential issue | 🟠 Major

Evict terminal failure states from auth_state.

release_in_flight(..., Unauthenticated) leaves a permanent map entry, and Blocked entries are also unbounded. A long-running node can accumulate one entry per failed/transient peer.

🧹 Suggested direction
                 AuthState::InFlight => {
-                    entry.get().set_state(target_state);
+                    entry.get().set_state(target_state.clone());
+                    if matches!(target_state, AuthState::Unauthenticated) {
+                        entry.remove();
+                    }
                     Ok(())
                 }

For Blocked, use a TTL or the added lru dependency so block retention is bounded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/protocol.rs` around lines 279 - 314, The release_in_flight function
currently leaves permanent entries for terminal failure states (e.g., releasing
to AuthState::Unauthenticated or adding AuthState::Blocked), causing the
in_flight HashMap to grow unbounded; update release_in_flight (and related
WatchableRemote usage) to evict entries when setting terminal states (remove the
Entry from the guarded in_flight map after setting or instead of inserting) and
implement bounded retention for Blocked (either drop entries immediately or
integrate an LRU/TTL eviction policy using the added lru dependency or a
timestamp+cleanup pass) so failed peers do not remain forever in the map. Ensure
you reference and modify release_in_flight, the Branch handling
AuthState::Unauthenticated and AuthState::Blocked, and how
WatchableRemote::set_state is used so entries are removed or TTL'd
appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 25: Replace the bare tokio dependency with an explicit feature list so
the crate gets the APIs it uses: include "rt-multi-thread" (for spawn and
multi-threaded runtime), "macros" (for #[tokio::main]/#[tokio::test]), "sync"
(for tokio::sync::Mutex) and "time" (for tokio::time). Update the tokio entry in
Cargo.toml (the tokio dependency line) to specify these features alongside
version = "1".

In `@src/auth.rs`:
- Around line 209-238: The timeout handler currently only checks for the outer
timeout Err and ignores the inner Result from recv.read_to_end(MAX_READ_SIZE),
so read failures or oversized trailing data are swallowed; update the logic (the
tokio::time::timeout(...).await handling around recv.read_to_end(MAX_READ_SIZE)
in auth.rs) to inspect both outcomes: match the await result, convert a timeout
into AuthenticatorError::OpenFailed/AcceptFailed as you already do, and when the
timeout returns Ok(inner_result) handle inner Err by mapping the read error into
AuthenticatorError::OpenFailed/AcceptFailed (including the error details) and
logging it the same way as the timeout case. Ensure you preserve use of
AUTH_TIMEOUT and the open flag when constructing the error variants.
- Around line 419-424: The loop that retries endpoint.connect is not bounded
because the await can block past AUTH_TIMEOUT; inside the while
(start_time.elapsed() < AUTH_TIMEOUT) in the before_connect logic wrap the
connect future with a timeout computed from the remaining window (AUTH_TIMEOUT -
start_time.elapsed()) so each await respects the overall AUTH_TIMEOUT, handle
the timeout/error path from that timeout wrapper (treat as a failed attempt and
continue or break appropriately) and ensure the code uses the ALPN and remote_id
parameters when calling endpoint.connect as before.

In `@src/error.rs`:
- Around line 25-28: The error variant AuthenticatorError::EndpointNotSet
returns a stale help message referencing authenticator.start(endpoint); update
the message in the Display/formatting arm for AuthenticatorError::EndpointNotSet
to instruct callers to use the new API, e.g. call set_endpoint(...).await
(replace the old "authenticator.start(endpoint)" text with
"set_endpoint(...).await") so the error guidance matches the current
Authenticator API.

In `@src/protocol.rs`:
- Around line 209-251: The code currently accepts the app connection immediately
after registering auth as InFlight or when AlreadyInFlight, allowing a race;
change the logic so that when register_in_flight(...) returns
RegisterResponse::InFlightRegistered you still spawn_auth_worker(remote_id,
endpoint).await but do NOT return BeforeConnectOutcome::Accept—return
BeforeConnectOutcome::Reject (or otherwise block) so the connection waits for
auth to reach a terminal state; likewise, for RegisterResponse::AlreadyInFlight,
if is_authenticated(&remote_id).await is false, return
BeforeConnectOutcome::Reject instead of falling through to accept; keep the
existing behavior for AlreadyAuthenticated and AlreadyBlocked and preserve
logging.

---

Duplicate comments:
In `@src/protocol.rs`:
- Around line 279-314: The release_in_flight function currently leaves permanent
entries for terminal failure states (e.g., releasing to
AuthState::Unauthenticated or adding AuthState::Blocked), causing the in_flight
HashMap to grow unbounded; update release_in_flight (and related WatchableRemote
usage) to evict entries when setting terminal states (remove the Entry from the
guarded in_flight map after setting or instead of inserting) and implement
bounded retention for Blocked (either drop entries immediately or integrate an
LRU/TTL eviction policy using the added lru dependency or a timestamp+cleanup
pass) so failed peers do not remain forever in the map. Ensure you reference and
modify release_in_flight, the Branch handling AuthState::Unauthenticated and
AuthState::Blocked, and how WatchableRemote::set_state is used so entries are
removed or TTL'd appropriately.

In `@src/tests.rs`:
- Around line 163-225: The ALPN list is being overwritten in each loop by
repeated calls to Endpoint::builder(...).alpns(...) (see endpoint_a_builder and
endpoint_b_builder); instead build a single Vec of all ALPN bytes (e.g. collect
format!("/dummy/{}", i).into_bytes() for 0..parallel_count) and call .alpns(...)
once on the builder before .hooks(...).bind(); also stop discarding connection
results in the tokio::spawn block — capture the Result from
endpoint_a.connect(...).await and assert or propagate errors (use
expect/map_err? or assert!(result.is_ok())) so connection failures to any
/dummy/{i} are detected.
- Around line 62-75: The test spawns tasks with tokio::spawn and relies on
worked.fetch_add to finish, which can hang if a task panics; replace the
detached spawns with a JoinSet or collect JoinHandles for the tasks that call
run_auth_parallel_test(secret, secret, count) and then join them, wrapping the
join phase in tokio::time::timeout to fail the test instead of spinning forever;
reference the existing symbols worked, count, run_auth_parallel_test, and the
tokio::spawn usage so you know where to replace spawns with JoinSet/handles and
apply timeout around awaiting all joins.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b59a0a31-174a-4add-b50b-978c05a0ca33

📥 Commits

Reviewing files that changed from the base of the PR and between 7a89f10 and be02a69.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • Cargo.toml
  • examples/basic.rs
  • src/auth.rs
  • src/error.rs
  • src/lib.rs
  • src/protocol.rs
  • src/secret.rs
  • src/tests.rs

Comment thread Cargo.toml Outdated
Comment thread src/auth.rs
Comment thread src/auth.rs Outdated
Comment thread src/error.rs
Comment thread src/protocol.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Around line 39-41: Remove "gossip-example" from the default features in
Cargo.toml so example-only dependencies are not pulled into downstream crates:
change the features table so that gossip-example remains defined but default =
[] (or otherwise excludes "gossip-example"); ensure the feature name
"gossip-example" and the default key "default" in the Cargo.toml features
section are updated accordingly so consumers must opt into the example via
--features gossip-example.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 184400fc-2cb1-4351-a173-9080673cd770

📥 Commits

Reviewing files that changed from the base of the PR and between be02a69 and 1219b38.

📒 Files selected for processing (6)
  • Cargo.toml
  • examples/gossip.rs
  • src/auth.rs
  • src/lib.rs
  • src/protocol.rs
  • src/tests.rs

Comment thread Cargo.toml Outdated
…ync and require .await, if not indicated by full version upgrade we run the risk that futures are just never executed in updated projects)
@rustonbsd

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@rustonbsd

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
README.md (2)

22-25: ⚠️ Potential issue | 🟡 Minor

Update the installation snippet for the released versions.

Cargo.toml now publishes iroh-auth as 0.2.0 and depends on iroh = "0.98", but the README still tells users to install iroh = "0.96" and iroh-auth = "0.1".

📚 Proposed docs fix
 [dependencies]
-iroh = "0.96"
-iroh-auth = "0.1"
+iroh = "0.98"
+iroh-auth = "0.2"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 22 - 25, Update the Cargo.toml example in the README:
change the dependency versions from iroh = "0.96" and iroh-auth = "0.1" to the
released versions iroh = "0.98" and iroh-auth = "0.2.0" so the example matches
the published crate versions (look for the TOML snippet with [dependencies] and
the iroh / iroh-auth entries).

47-54: ⚠️ Potential issue | 🟡 Minor

Fix the README example to match the current async setup API.

The example uses Endpoint::builder() without the presets::N0 argument and calls auth.set_endpoint(&endpoint) without .await. This will cause compile errors; the method is async. The correct code is:

Proposed fix
-    let endpoint = Endpoint::builder()
+    let endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
         .hooks(auth.clone())
         .bind()
         .await.map_err(|e| e.to_string())?;
 
     // 3. The authenticator needs a reference to the bound endpoint 
     // to initiate authentication handshakes.
-    auth.set_endpoint(&endpoint);
+    auth.set_endpoint(&endpoint).await;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 47 - 54, Update the README example to match the
current async API by calling Endpoint::builder with the required presets
argument (e.g., Endpoint::builder(presets::N0)) and await the async
authenticator method call (use auth.set_endpoint(&endpoint).await); keep the
existing bind().await usage and error mapping but replace the builder()
invocation and add .await to set_endpoint to avoid compilation errors.
♻️ Duplicate comments (3)
src/error.rs (1)

25-28: ⚠️ Potential issue | 🟡 Minor

Replace the stale setup method in the error guidance.

The API is set_endpoint(&endpoint).await, not start(endpoint).await, so this message sends users to a non-existent setup path.

✏️ Proposed fix
             AuthenticatorError::EndpointNotSet => write!(
                 f,
-                "Authenticator endpoint not set: missing authenticator.start(endpoint).await"
+                "Authenticator endpoint not set: missing authenticator.set_endpoint(&endpoint).await"
             ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/error.rs` around lines 25 - 28, Update the error message for
AuthenticatorError::EndpointNotSet: change the guidance string in the write!
call (the Display/format implementation that constructs the message) to
reference the correct API usage set_endpoint(&endpoint).await instead of the
stale start(endpoint).await so users are directed to the existing method.
src/auth.rs (1)

212-240: ⚠️ Potential issue | 🟠 Major

Handle the inner read_to_end error instead of dropping it.

timeout(...).await returns Ok(Err(_)) for read failures or oversized trailing data, and this branch currently falls through as success. That can hide malformed auth stream shutdowns.

🛡️ Proposed fix
         const MAX_READ_SIZE: usize = 1024;
-        if let Err(err) = tokio::time::timeout(AUTH_TIMEOUT, recv.read_to_end(MAX_READ_SIZE))
-            .await
-            .map_err(|_| {
-                if open {
-                    AuthenticatorError::OpenFailed(
-                        "Failed to wait for stream stopped: timeout".to_string(),
-                    )
-                } else {
-                    AuthenticatorError::AcceptFailed(
-                        "Failed to wait for stream stopped: timeout".to_string(),
-                    )
-                }
-            })
-            .map_err(|err| {
-                if open {
-                    AuthenticatorError::OpenFailed(format!(
-                        "Failed to wait for stream stopped: {}",
-                        err
-                    ))
-                } else {
-                    AuthenticatorError::AcceptFailed(format!(
-                        "Failed to wait for stream stopped: {}",
-                        err
-                    ))
-                }
-            })
-        {
-            warn!("[end_of_auth] {}", err);
-        }
+        match tokio::time::timeout(AUTH_TIMEOUT, recv.read_to_end(MAX_READ_SIZE)).await {
+            Ok(Ok(_)) => {}
+            Ok(Err(err)) => {
+                let err = if open {
+                    AuthenticatorError::OpenFailed(format!(
+                        "Failed to wait for stream stopped: {}",
+                        err
+                    ))
+                } else {
+                    AuthenticatorError::AcceptFailed(format!(
+                        "Failed to wait for stream stopped: {}",
+                        err
+                    ))
+                };
+                warn!("[end_of_auth] {}", err);
+            }
+            Err(_) => {
+                let err = if open {
+                    AuthenticatorError::OpenFailed(
+                        "Failed to wait for stream stopped: timeout".to_string(),
+                    )
+                } else {
+                    AuthenticatorError::AcceptFailed(
+                        "Failed to wait for stream stopped: timeout".to_string(),
+                    )
+                };
+                warn!("[end_of_auth] {}", err);
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auth.rs` around lines 212 - 240, The current timeout(...) call only maps
the outer timeout error but ignores the inner read_to_end error (Ok(Err(_)))
which lets read failures or oversized trailing data pass as success; change the
logic around tokio::time::timeout(AUTH_TIMEOUT,
recv.read_to_end(MAX_READ_SIZE)).await so you explicitly match its result and
convert both Err(timeout) and Ok(Err(read_err)) into the appropriate
AuthenticatorError (use AuthenticatorError::OpenFailed or ::AcceptFailed
depending on open), preserving the read_err message in the formatted error;
ensure the warn!("[end_of_auth] {}", err) branch is executed for both failure
cases instead of letting inner errors fall through.
src/protocol.rs (1)

202-243: ⚠️ Potential issue | 🟠 Major

before_connect still accepts app connections before auth reaches a terminal state.

The InFlightRegistered arm spawns the worker and falls through to Accept at Line 243; the AlreadyInFlight arm also falls through to Accept when is_authenticated is false. That lets the application connection race ahead of the SPAKE2 worker — the previous review’s after_handshake-style wait is still the right shape here.

Additionally, when self.endpoint().await fails at Lines 211–217, register_in_flight has already promoted the cache entry to InFlight, but this path returns Reject without calling release_in_flight(.., Unauthenticated). The peer is then stuck InFlight until LRU eviction, and any future after_handshake for it will pay AUTH_TIMEOUT before rejecting.

🔒 Proposed direction
             Ok(RegisterResponse::InFlightRegistered) => {
                 debug!(
                     "[before_connect] registered in-flight auth for {}, performing auth",
                     remote_id
                 );
-                let endpoint = match self.endpoint().await {
-                    Ok(ep) => ep,
-                    Err(_) => {
-                        warn!("[before_connect] authenticator endpoint not set");
-                        return iroh::endpoint::BeforeConnectOutcome::Reject;
-                    }
-                };
+                let endpoint = match self.endpoint().await {
+                    Ok(ep) => ep,
+                    Err(_) => {
+                        warn!("[before_connect] authenticator endpoint not set");
+                        release_in_flight(
+                            self.auth_state.clone(),
+                            remote_id,
+                            AuthState::Unauthenticated,
+                        )
+                        .await
+                        .ok();
+                        return iroh::endpoint::BeforeConnectOutcome::Reject;
+                    }
+                };
                 self.spawn_auth_worker(remote_id, endpoint).await;
             }
@@
-        iroh::endpoint::BeforeConnectOutcome::Accept
+        // Wait until the worker marks the peer Authenticated / Blocked / Unauthenticated
+        // (bounded by AUTH_TIMEOUT) before accepting the app connection.
+        if timeout(AUTH_TIMEOUT, async {
+            loop {
+                if self.is_authenticated(&remote_id).await { return true; }
+                if let Some(st) = get_auth_state(self.auth_state.clone(), remote_id).await {
+                    if matches!(st.state(), AuthState::Blocked | AuthState::Unauthenticated) {
+                        return false;
+                    }
+                }
+                tokio::time::sleep(std::time::Duration::from_millis(50)).await;
+            }
+        }).await.unwrap_or(false) {
+            iroh::endpoint::BeforeConnectOutcome::Accept
+        } else {
+            iroh::endpoint::BeforeConnectOutcome::Reject
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/protocol.rs` around lines 202 - 243, The before_connect branch currently
accepts connections while auth is still "InFlight"; change it so that
InFlightRegistered and AlreadyInFlight (when is_authenticated(&remote_id) is
false) do NOT fall through to Accept but instead defer acceptance to the
after_handshake flow (i.e. return Reject here), and ensure any early error paths
release the in-flight slot: after register_in_flight returns InFlightRegistered,
call spawn_auth_worker(remote_id, endpoint).await but then return
BeforeConnectOutcome::Reject (do not Accept); for the AlreadyInFlight arm, if
is_authenticated(&remote_id).await is false return Reject; for the
endpoint().await failure path call release_in_flight(remote_id, Unauthenticated)
before returning Reject; keep the AlreadyAuthenticated and AlreadyBlocked
handling as-is. Use the symbols register_in_flight,
RegisterResponse::InFlightRegistered/AlreadyInFlight/AlreadyAuthenticated/AlreadyBlocked,
spawn_auth_worker, is_authenticated, release_in_flight, and BeforeConnectOutcome
to locate and apply these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/auth.rs`:
- Around line 423-436: The auth_open call currently uses a fresh AUTH_TIMEOUT,
allowing the worker to exceed the intended deadline; recompute the remaining
time after the successful connect and use that remaining duration for the
auth.auth_open(conn) timeout. In the loop that computes t before
endpoint.connect(remote_id, ALPN), after receiving Ok(Ok(conn)) calculate
remaining =
AUTH_TIMEOUT.saturating_sub(Instant::now().saturating_duration_since(start_time))
(or reuse a recomputed variable) and call timeout(remaining,
auth.auth_open(conn)); if remaining is zero treat it as a timeout/failure and
handle accordingly.

In `@src/tests.rs`:
- Around line 126-155: Extract the duplicated wait/timeout/shutdown/assert logic
from run_auth_test and run_auth_parallel_test into two helpers: a
wait_for_auth_terminal(auth_a: &AuthType, auth_b: &AuthType) that runs the
wait_loop (polling list_authenticated/list_blocked with time::sleep) wrapped in
timeout(AUTH_TIMEOUT * 2, ...) and returns a Result<(), String>, and a
shutdown_routers(router_a, router_b) that calls router_a.shutdown().await.ok()
and router_b.shutdown().await.ok(); then replace the duplicated block in both
tests with a call to wait_for_auth_terminal(&auth_a, &auth_b)?/handle Err by
calling shutdown_routers(...) and returning the Err string, then call
shutdown_routers(...) and finally perform the same final
is_authenticated(&endpoint_b.id()).await &&
is_authenticated(&endpoint_a.id()).await check and return it.
- Around line 183-186: Remove the dead commented-out relay configuration
referencing RelayMap in the tests: either delete the commented block containing
.clear_relay_transports(), .relay_mode(iroh::RelayMode::Custom(...)) and the
RelayMap::try_from_iter(...) call, or if you intend to keep it for optional
testing, wrap it behind a feature flag (e.g., cfg(feature = "relay")) and add
the necessary import for RelayMap so the code compiles when the feature is
enabled; update references in the test around clear_relay_transports and
relay_mode accordingly.
- Around line 55-76: The test_auth_parallel function currently detaches
tokio::spawn tasks so panics are lost and the worked counter can hang; change it
to collect the JoinHandles returned by tokio::spawn, await each handle and
propagate errors (use handle.await.unwrap() or propagate JoinError), and wrap
the whole wait in a timeout (tokio::time::timeout) so the test fails
deterministically instead of blocking; ensure you still call
run_auth_parallel_test(secret, secret, count).await.unwrap() inside the spawned
futures but capture the JoinHandle results and assert their success after the
timeout expires, updating or removing the worked counter as needed.

---

Outside diff comments:
In `@README.md`:
- Around line 22-25: Update the Cargo.toml example in the README: change the
dependency versions from iroh = "0.96" and iroh-auth = "0.1" to the released
versions iroh = "0.98" and iroh-auth = "0.2.0" so the example matches the
published crate versions (look for the TOML snippet with [dependencies] and the
iroh / iroh-auth entries).
- Around line 47-54: Update the README example to match the current async API by
calling Endpoint::builder with the required presets argument (e.g.,
Endpoint::builder(presets::N0)) and await the async authenticator method call
(use auth.set_endpoint(&endpoint).await); keep the existing bind().await usage
and error mapping but replace the builder() invocation and add .await to
set_endpoint to avoid compilation errors.

---

Duplicate comments:
In `@src/auth.rs`:
- Around line 212-240: The current timeout(...) call only maps the outer timeout
error but ignores the inner read_to_end error (Ok(Err(_))) which lets read
failures or oversized trailing data pass as success; change the logic around
tokio::time::timeout(AUTH_TIMEOUT, recv.read_to_end(MAX_READ_SIZE)).await so you
explicitly match its result and convert both Err(timeout) and Ok(Err(read_err))
into the appropriate AuthenticatorError (use AuthenticatorError::OpenFailed or
::AcceptFailed depending on open), preserving the read_err message in the
formatted error; ensure the warn!("[end_of_auth] {}", err) branch is executed
for both failure cases instead of letting inner errors fall through.

In `@src/error.rs`:
- Around line 25-28: Update the error message for
AuthenticatorError::EndpointNotSet: change the guidance string in the write!
call (the Display/format implementation that constructs the message) to
reference the correct API usage set_endpoint(&endpoint).await instead of the
stale start(endpoint).await so users are directed to the existing method.

In `@src/protocol.rs`:
- Around line 202-243: The before_connect branch currently accepts connections
while auth is still "InFlight"; change it so that InFlightRegistered and
AlreadyInFlight (when is_authenticated(&remote_id) is false) do NOT fall through
to Accept but instead defer acceptance to the after_handshake flow (i.e. return
Reject here), and ensure any early error paths release the in-flight slot: after
register_in_flight returns InFlightRegistered, call spawn_auth_worker(remote_id,
endpoint).await but then return BeforeConnectOutcome::Reject (do not Accept);
for the AlreadyInFlight arm, if is_authenticated(&remote_id).await is false
return Reject; for the endpoint().await failure path call
release_in_flight(remote_id, Unauthenticated) before returning Reject; keep the
AlreadyAuthenticated and AlreadyBlocked handling as-is. Use the symbols
register_in_flight,
RegisterResponse::InFlightRegistered/AlreadyInFlight/AlreadyAuthenticated/AlreadyBlocked,
spawn_auth_worker, is_authenticated, release_in_flight, and BeforeConnectOutcome
to locate and apply these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0ee639c6-345a-43de-a829-1b23f6de5f75

📥 Commits

Reviewing files that changed from the base of the PR and between 4294fad and dda68ad.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • README.md
  • examples/basic.rs
  • examples/gossip.rs
  • src/auth.rs
  • src/error.rs
  • src/lib.rs
  • src/protocol.rs
  • src/secret.rs
  • src/tests.rs

Comment thread src/auth.rs Outdated
Comment thread src/tests.rs Outdated
Comment thread src/tests.rs Outdated
Comment thread src/tests.rs Outdated
@rustonbsd

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

54-54: ⚠️ Potential issue | 🟡 Minor

README usage snippet is missing .await on set_endpoint.

Authenticator::set_endpoint is now async (as reflected in both examples/basic.rs and examples/gossip.rs), but line 54 of this README still calls it synchronously. The snippet will not compile as-is and will mislead new users copy-pasting from the docs.

✏️ Proposed fix
-    auth.set_endpoint(&endpoint);
+    auth.set_endpoint(&endpoint).await;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 54, The README usage snippet calls
auth.set_endpoint(&endpoint) synchronously but Authenticator::set_endpoint is
async; update the snippet to call auth.set_endpoint(&endpoint).await and ensure
the surrounding example is inside an async context (e.g., async fn main or a
tokio::main runtime) consistent with examples/basic.rs and examples/gossip.rs so
the code compiles when copy-pasted.
♻️ Duplicate comments (5)
src/error.rs (1)

25-28: ⚠️ Potential issue | 🟡 Minor

Error message still references the non-existent start method.

The help text points users to authenticator.start(endpoint).await, but the actual API is set_endpoint(...).await (see src/auth.rs and the examples). This is misleading guidance to a caller who hit this error.

✏️ Proposed fix
-                "Authenticator endpoint not set: missing authenticator.start(endpoint).await"
+                "Authenticator endpoint not set: missing authenticator.set_endpoint(endpoint).await"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/error.rs` around lines 25 - 28, The error message for
AuthenticatorError::EndpointNotSet incorrectly refers to
authenticator.start(endpoint).await; update the Display/format string in the
AuthenticatorError::EndpointNotSet arm (in src/error.rs) to reference the
correct API call authenticator.set_endpoint(...).await (or
set_endpoint(endpoint).await) so callers are pointed to the real method (matches
src/auth.rs and examples).
src/tests.rs (2)

57-75: ⚠️ Potential issue | 🟠 Major

test_auth_parallel still detaches tasks and busy-polls without a timeout.

The tokio::spawn handles are discarded, so any panic inside assert!/expect is lost; if a worker panics before incrementing worked, the while loop at lines 72–74 spins forever and the test only terminates when the CI runner kills it. Collect JoinHandles (or a JoinSet) and wrap the wait in tokio::time::timeout so failures surface deterministically.

🔧 Proposed fix
-        let secret = b"supersecrettoken1234567890123456";
-        let worked = Arc::new(AtomicUsize::new(0));
-        let count = 10;
-        for _ in 0..count {
-            let worked = worked.clone();
-            tokio::spawn(async move {
-                assert!(run_auth_parallel_test(secret, secret, count)
-                    .await
-                    .expect("auth_parallel test failed"));
-                worked.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
-            });
-        }
-
-        while worked.load(std::sync::atomic::Ordering::SeqCst) < count {
-            tokio::time::sleep(Duration::from_millis(100)).await;
-        }
+        let secret = b"supersecrettoken1234567890123456";
+        let count = 10;
+        let mut tasks = tokio::task::JoinSet::new();
+        for _ in 0..count {
+            tasks.spawn(async move {
+                run_auth_parallel_test(secret, secret, count).await
+            });
+        }
+        timeout(AUTH_TIMEOUT * 4, async {
+            while let Some(res) = tasks.join_next().await {
+                assert!(res.expect("task panicked").expect("auth failed"));
+            }
+        })
+        .await
+        .expect("test_auth_parallel timed out");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tests.rs` around lines 57 - 75, The test_auth_parallel function currently
detaches tokio::spawn tasks and busy-waits on the worked AtomicUsize, losing
panics and risking an infinite loop; collect the spawned task JoinHandle values
(or use a tokio::task::JoinSet) instead of discarding them, await all handles
and propagate panics/results from run_auth_parallel_test so failures surface,
and wrap the join/wait phase in tokio::time::timeout to bound the wait; update
references to the worked Arc, the run_auth_parallel_test calls and the spawn
logic to push handles into a Vec/JoinSet and then await them with timeout.

127-155: 🧹 Nitpick | 🔵 Trivial

Factor out the duplicated wait/timeout/shutdown block.

run_auth_test and run_auth_parallel_test repeat the same timeout(AUTH_TIMEOUT * 2, wait_loop) → router shutdown → is_authenticated cross-check. Extract a small helper (e.g. wait_for_auth_terminal(&auth_a, &auth_b, ...) plus shutdown_routers(router_a, router_b)) so the two helpers do not drift.

Also applies to: 230-260

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tests.rs` around lines 127 - 155, The test file duplicates the same
wait/timeout/shutdown/is_authenticated logic across run_auth_test and
run_auth_parallel_test; extract a helper like wait_for_auth_terminal(auth_a,
auth_b, timeout) that encapsulates building the wait_loop and awaiting
timeout(AUTH_TIMEOUT * 2, ...), and another helper shutdown_routers(router_a,
router_b) to call router.shutdown().await.ok(); then replace the duplicated
blocks in run_auth_test and run_auth_parallel_test with calls to
wait_for_auth_terminal(&auth_a, &auth_b, AUTH_TIMEOUT * 2) followed by
shutdown_routers(router_a, router_b) and finally use
auth_a.is_authenticated(&endpoint_b.id()).await &&
auth_b.is_authenticated(&endpoint_a.id()).await for the cross-check.
src/auth.rs (1)

213-241: ⚠️ Potential issue | 🟠 Major

Inner read_to_end error is still silently swallowed.

The Ok(Err(_)) branch of timeout(..., recv.read_to_end(MAX_READ_SIZE)).await is still dropped: the first .map_err only fires on the outer Elapsed, and the second only operates on the already-mapped AuthenticatorError, so a failing read or an oversized trailing payload returns Ok(()) here. The past review flagged this and it's marked as addressed in commits, but the structural bug remains in the current tree. Please convert this to an explicit match over Ok(Ok(_)) / Ok(Err(_)) / Err(_) as previously suggested.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auth.rs` around lines 213 - 241, The current chained .map_err calls on
tokio::time::timeout(AUTH_TIMEOUT, recv.read_to_end(MAX_READ_SIZE)).await
silently drop an inner read error; replace the chain with an explicit match on
the await result: match tokio::time::timeout(...).await { Ok(Ok(_)) => {},
Ok(Err(inner_err)) => map inner_err to AuthenticatorError::OpenFailed or
::AcceptFailed (using the open boolean) with a message like "Failed to wait for
stream stopped: {inner_err}" and warn! it, Err(_) => map the timeout to the same
OpenFailed/AcceptFailed timeout message and warn! it }. Use the same symbols
AUTH_TIMEOUT, MAX_READ_SIZE, recv, open, and the AuthenticatorError variants
OpenFailed/AcceptFailed so the correct error branches are handled instead of
being swallowed.
src/protocol.rs (1)

224-232: ⚠️ Potential issue | 🟠 Major

AlreadyInFlight still races ahead of the concurrent auth worker.

This branch accepts the app connection whether the cached state is AlreadyInFlight or AlreadyAuthenticated. For the in-flight case, another worker is still running perform_auth; returning Accept here lets the application connection proceed before that worker finishes, which is exactly the race the earlier review flagged. Only the InFlightRegistered arm was fixed (it now blocks on perform_auth); concurrent callers entering via AlreadyInFlight get no such wait.

Additionally, the debug! on Line 226 ("already authenticated (in flight), accepting connection") only fires when is_authenticated is true, i.e. the AlreadyAuthenticated case — so in the actually-in-flight case the accept is silent.

Consider either awaiting the existing in-flight watcher (mirroring after_handshake's wait_for_auth) or rejecting until is_authenticated becomes true.

🔒 Suggested direction
-            Ok(RegisterResponse::AlreadyInFlight) | Ok(RegisterResponse::AlreadyAuthenticated) => {
-                if self.is_authenticated(&remote_id).await {
-                    debug!(
-                    "[before_connect] already authenticated (in flight), accepting connection to {}",
-                    remote_id
-                );
-                }
-                iroh::endpoint::BeforeConnectOutcome::Accept
-            }
+            Ok(RegisterResponse::AlreadyAuthenticated) => {
+                debug!("[before_connect] already authenticated, accepting connection to {}", remote_id);
+                iroh::endpoint::BeforeConnectOutcome::Accept
+            }
+            Ok(RegisterResponse::AlreadyInFlight) => {
+                // Wait for the concurrent worker to reach a terminal state
+                // instead of letting the app connection race ahead.
+                match self.wait_for_terminal_state(remote_id).await {
+                    AuthState::Authenticated => iroh::endpoint::BeforeConnectOutcome::Accept,
+                    _ => iroh::endpoint::BeforeConnectOutcome::Reject,
+                }
+            }

This also directly ties into the PR TODO ("test for an incoming and outgoing auth connection occurring simultaneously from the perspective of one peer") — the current behaviour would likely fail such a test deterministically on the second concurrent caller.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/protocol.rs` around lines 224 - 232, The AlreadyInFlight arm currently
returns Accept and can race with the concurrent auth worker; change the logic so
that when RegisterResponse::AlreadyInFlight is encountered you do not
immediately Accept but instead wait for the existing in-flight authentication to
finish (mirror the behavior used for InFlightRegistered by awaiting the same
watcher or calling the same wait_for_auth helper used by after_handshake) and
only return iroh::endpoint::BeforeConnectOutcome::Accept once
self.is_authenticated(&remote_id).await is true; alternatively return Reject
until authenticated—ensure the debug log still reflects the actual authenticated
state (use RegisterResponse::AlreadyAuthenticated to short-circuit Accept
immediately and keep the wait path for AlreadyInFlight).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/auth.rs`:
- Around line 48-54: The comments on the RegisterResponse enum contain
misspellings of "authenticated" and the function/flag name "is_authenticated";
update all occurrences in the RegisterResponse variant comments
(InFlightRegistered, AlreadyInFlight, AlreadyAuthenticated, AlreadyBlocked) to
use "authenticated" and "is_authenticated" consistently and correct the wording
(e.g., "is_authenticaded" -> "is_authenticated", "authenticaded" ->
"authenticated") so the comments are accurate and typo-free.
- Around line 149-161: Replace the read that uses LruCache::get in
is_authenticated with a non-promoting peek: lock auth_state in is_authenticated
and call .peek(id) instead of .get(id), and simplify the closure to return
w.state() == AuthState::Authenticated (or inline the comparison) so the method
becomes a simple .peek(id).map(|w| w.state() ==
AuthState::Authenticated).unwrap_or(false); this avoids LRU promotion while
preserving the boolean auth check for PublicKey lookups.
- Around line 422-436: The timeout call around endpoint.online() in perform_auth
currently leaks the InFlight state and has a redundant .map_err chain; before
returning the AuthenticatorError on timeout, call release_in_flight(peer_id,
AuthState::Unauthenticated) (use the same peer identifier used when marking
InFlight) so the peer is released, then return the error; also remove the second
.map_err so only the first map_err converting the Elapsed into
AuthenticatorError::OpenFailed remains (apply the same map_err cleanup pattern
in end_of_auth if present).

In `@src/error.rs`:
- Around line 29-34: The two AuthenticatorError match arms for
AcceptFailedAndBlock and OpenFailedAndBlock currently output the same message;
update the Display implementation (the write!(f, ...) calls inside the
AuthenticatorError match) so each variant uses a distinct, variant-specific
prefix (e.g., "Blocked on accept: ..." vs "Blocked on open: ..." or similar) and
preserves the existing msg and id values so logs/callers can distinguish which
side of the handshake triggered the block.

In `@src/protocol.rs`:
- Around line 209-222: Update the stale comment above the before_connect logic:
replace the lines that say it "spawns the authentication worker in a new task"
and "returns directly with BeforeConnectOutcome::Accept" with a short accurate
description that before_connect awaits perform_auth inline using
self.endpoint().await and then returns
iroh::endpoint::BeforeConnectOutcome::Accept or ::Reject based on the result;
reference the before_connect function, the endpoint() call, and perform_auth to
clarify where the behavior now occurs.

In `@src/tests.rs`:
- Around line 116-125: The spawned task silently discards the result of
endpoint_a.connect(...).await.ok(), so connection failures are hidden; change
the tokio::spawn block to capture the Result from
endpoint_a.connect(endpoint_b.addr(), b"/dummy/1").await, and on Err send the
error (or a stringified form) back to the test via a oneshot/mpsc channel (or at
minimum processLogger.error) so the test can fail fast; modify the closure that
currently references endpoint_a and endpoint_b to take a sender, send Err(e) on
failure and Ok(()) on success, and have the test await the receiver to surface
the real connect error instead of timing out during authentication.
- Around line 230-248: The exit condition in wait_loop (inside wait_a and
wait_b) only waits for len() + list_blocked().len() < 1, so a single
authenticated/blocked peer can satisfy the check; update the per-side condition
to require all expected peers by comparing to parallel_count (e.g., replace the
"< 1" check with "< parallel_count" or otherwise require >= parallel_count).
Keep the combined success check using success.load(...) < parallel_count as-is
so the loop only exits when both the per-side auth counts and the overall
success counter reach parallel_count. Ensure the change is applied symmetrically
to both wait_a and wait_b.

---

Outside diff comments:
In `@README.md`:
- Line 54: The README usage snippet calls auth.set_endpoint(&endpoint)
synchronously but Authenticator::set_endpoint is async; update the snippet to
call auth.set_endpoint(&endpoint).await and ensure the surrounding example is
inside an async context (e.g., async fn main or a tokio::main runtime)
consistent with examples/basic.rs and examples/gossip.rs so the code compiles
when copy-pasted.

---

Duplicate comments:
In `@src/auth.rs`:
- Around line 213-241: The current chained .map_err calls on
tokio::time::timeout(AUTH_TIMEOUT, recv.read_to_end(MAX_READ_SIZE)).await
silently drop an inner read error; replace the chain with an explicit match on
the await result: match tokio::time::timeout(...).await { Ok(Ok(_)) => {},
Ok(Err(inner_err)) => map inner_err to AuthenticatorError::OpenFailed or
::AcceptFailed (using the open boolean) with a message like "Failed to wait for
stream stopped: {inner_err}" and warn! it, Err(_) => map the timeout to the same
OpenFailed/AcceptFailed timeout message and warn! it }. Use the same symbols
AUTH_TIMEOUT, MAX_READ_SIZE, recv, open, and the AuthenticatorError variants
OpenFailed/AcceptFailed so the correct error branches are handled instead of
being swallowed.

In `@src/error.rs`:
- Around line 25-28: The error message for AuthenticatorError::EndpointNotSet
incorrectly refers to authenticator.start(endpoint).await; update the
Display/format string in the AuthenticatorError::EndpointNotSet arm (in
src/error.rs) to reference the correct API call
authenticator.set_endpoint(...).await (or set_endpoint(endpoint).await) so
callers are pointed to the real method (matches src/auth.rs and examples).

In `@src/protocol.rs`:
- Around line 224-232: The AlreadyInFlight arm currently returns Accept and can
race with the concurrent auth worker; change the logic so that when
RegisterResponse::AlreadyInFlight is encountered you do not immediately Accept
but instead wait for the existing in-flight authentication to finish (mirror the
behavior used for InFlightRegistered by awaiting the same watcher or calling the
same wait_for_auth helper used by after_handshake) and only return
iroh::endpoint::BeforeConnectOutcome::Accept once
self.is_authenticated(&remote_id).await is true; alternatively return Reject
until authenticated—ensure the debug log still reflects the actual authenticated
state (use RegisterResponse::AlreadyAuthenticated to short-circuit Accept
immediately and keep the wait path for AlreadyInFlight).

In `@src/tests.rs`:
- Around line 57-75: The test_auth_parallel function currently detaches
tokio::spawn tasks and busy-waits on the worked AtomicUsize, losing panics and
risking an infinite loop; collect the spawned task JoinHandle values (or use a
tokio::task::JoinSet) instead of discarding them, await all handles and
propagate panics/results from run_auth_parallel_test so failures surface, and
wrap the join/wait phase in tokio::time::timeout to bound the wait; update
references to the worked Arc, the run_auth_parallel_test calls and the spawn
logic to push handles into a Vec/JoinSet and then await them with timeout.
- Around line 127-155: The test file duplicates the same
wait/timeout/shutdown/is_authenticated logic across run_auth_test and
run_auth_parallel_test; extract a helper like wait_for_auth_terminal(auth_a,
auth_b, timeout) that encapsulates building the wait_loop and awaiting
timeout(AUTH_TIMEOUT * 2, ...), and another helper shutdown_routers(router_a,
router_b) to call router.shutdown().await.ok(); then replace the duplicated
blocks in run_auth_test and run_auth_parallel_test with calls to
wait_for_auth_terminal(&auth_a, &auth_b, AUTH_TIMEOUT * 2) followed by
shutdown_routers(router_a, router_b) and finally use
auth_a.is_authenticated(&endpoint_b.id()).await &&
auth_b.is_authenticated(&endpoint_a.id()).await for the cross-check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d95108f1-0424-4ce3-b57b-4fe66e5ed081

📥 Commits

Reviewing files that changed from the base of the PR and between 4294fad and a8e2514.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • README.md
  • examples/basic.rs
  • examples/gossip.rs
  • src/auth.rs
  • src/error.rs
  • src/lib.rs
  • src/protocol.rs
  • src/secret.rs
  • src/tests.rs

Comment thread src/auth.rs
Comment thread src/auth.rs
Comment thread src/auth.rs Outdated
Comment thread src/error.rs
Comment thread src/protocol.rs Outdated
Comment thread src/tests.rs
Comment thread src/tests.rs Outdated
@rustonbsd

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@rustonbsd
rustonbsd merged commit 23b1c63 into main May 28, 2026
2 checks passed
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