How to Implement Connection Authentication and Authorization in iroh

Implement connection authentication and authorization in iroh by leveraging the EndpointHooks trait to intercept connections before and after the TLS handshake, using a dedicated authentication protocol to exchange tokens and a shared state to track verified peers.

The iroh networking library provides a flexible, hook-based extension point that allows developers to implement custom connection authentication and authorization without modifying core protocol handlers. By implementing the EndpointHooks trait, you can inspect every connection attempt and reject unauthorized peers before they reach your application logic. This approach enables you to secure existing iroh protocols—such as file transfer or NAT traversal—by simply mounting authentication hooks on your Endpoint builder.

Understanding the EndpointHooks Architecture

The authentication system in iroh operates through the EndpointHooks trait defined in iroh/src/protocol.rs. This trait provides two critical interception points: before_connect for outbound connections and after_handshake for inbound connections.

The before_connect Hook

The before_connect method runs when your endpoint attempts to connect to a remote peer. It receives the EndpointAddr and ALPN identifier, allowing you to implement pre-authentication logic. According to the implementation in iroh/examples/auth-hook.rs, this hook can return BeforeConnectOutcome::Accept to proceed or BeforeConnectOutcome::Reject to terminate the connection before any packets are exchanged.

The after_handshake Hook

For incoming connections, the after_handshake method executes immediately after the TLS handshake completes. This hook receives the Connection object and can return AfterHandshakeOutcome::Accept or AfterHandshakeOutcome::Reject. This is where you verify whether the remote peer has previously authenticated through your custom protocol.

Building a Token-Based Authentication Flow

The reference implementation in iroh/examples/auth-hook.rs demonstrates a complete token-based pre-authentication design. The flow works as follows:

  1. Setup – Both sides create auth hooks (incoming for server, outgoing for client) and attach them to the endpoint builder.
  2. Router configuration – The server registers the AuthProtocol for the auth ALPN alongside application protocols.
  3. Connection – When Endpoint::connect is called, the OutgoingAuthHook checks the allowed_remotes set. If the remote is not authenticated, it spawns a temporary connection using the auth ALPN (b"iroh-example/auth/0") to exchange the secret token.

OutgoingAuthHook and OutgoingAuthTask

The OutgoingAuthHook implements EndpointHooks::before_connect. When your application calls Endpoint::connect, this hook checks whether the remote EndpointId exists in a shared allowed_remotes set. If not, it sends a request to the OutgoingAuthTask, which initiates a secondary connection using the dedicated authentication ALPN to exchange the secret token.

AuthProtocol Handler

The AuthProtocol is a standard ProtocolHandler registered on the router that listens for the authentication ALPN. When a client connects, it reads the token from a unidirectional stream and compares it against the expected secret. On success, it inserts the remote's EndpointId into the allowed_remotes set and closes the connection with a success code.

IncomingAuthHook

The IncomingAuthHook implements EndpointHooks::after_handshake. After the TLS handshake completes, this hook verifies whether the remote peer exists in the allowed_remotes set. If the peer is unauthenticated and the current connection is not the authentication protocol itself, the hook returns AfterHandshakeOutcome::Reject, preventing unauthorized access to application protocols.

Implementation Example

The following code demonstrates how to set up a server that requires token-based authentication before allowing connections to an application protocol. This example uses the auth::incoming function from the reference implementation.

use iroh::{Endpoint, endpoint::presets, protocol::Router};
use iroh::examples::auth;

// Initialize the incoming hook with a secret token
let (in_hook, auth_proto) = auth::incoming(b"super-secret".to_vec());

// Build the endpoint with the authentication hook
let server_ep = Endpoint::builder(presets::N0)
    .hooks(in_hook)
    .bind()
    .await?;

// Create router that accepts auth protocol and application protocol
let router = Router::builder(server_ep)
    .accept(auth::ALPN, auth_proto)
    .accept(echo::ALPN, Echo)
    .spawn();

The client setup requires mounting the outgoing hook and spawning the background authentication task:

let (out_hook, out_task) = auth::outgoing(b"super-secret".to_vec());

let client_ep = Endpoint::builder(presets::N0)
    .hooks(out_hook)
    .bind()
    .await?;

// Spawn the background authentication task
let _guard = out_task.spawn(client_ep.clone());

// Connect to server - authentication happens transparently
Echo::connect(&client_ep, server_addr, b"hello").await?;

Hook Implementation Details

The actual hook logic in iroh/examples/auth-hook.rs handles edge cases such as skipping authentication when the auth ALPN itself is requested. This prevents the authentication protocol from requiring its own authentication, avoiding circular dependencies.

Outgoing hook logic:

impl EndpointHooks for OutgoingAuthHook {
    async fn before_connect<'a>(
        &'a self,
        remote_addr: &'a EndpointAddr,
        alpn: &'a [u8],
    ) -> BeforeConnectOutcome {
        // Allow auth protocol connections without additional checks
        if alpn == auth::ALPN {
            return BeforeConnectOutcome::Accept;
        }

        // Check if remote is pre-authenticated
        match self.authenticate(remote_addr.id).await {
            Ok(()) => BeforeConnectOutcome::Accept,
            Err(_) => BeforeConnectOutcome::Reject,
        }
    }
}

Auth protocol implementation:

impl ProtocolHandler for AuthProtocol {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        let mut stream = connection.accept_uni().await?;
        let token = stream.read_to_end(256).await?;
        
        if token == self.token {
            self.allowed_remotes.lock().unwrap()
                .insert(connection.remote_id());
            connection.close(CLOSE_ACCEPTED.into(), b"accepted");
        } else {
            connection.close(CLOSE_DENIED.into(), b"rejected");
        }
        Ok(())
    }
}

Key Source Files

The authentication mechanism relies on the following files in the n0-computer/iroh repository. Understanding these modules helps you customize the hook behavior for your specific security requirements.

  • iroh/examples/auth-hook.rs – Complete reference implementation showing the OutgoingAuthHook, IncomingAuthHook, OutgoingAuthTask, and AuthProtocol working together.
  • iroh/src/endpoint.rs – Defines EndpointBuilder and the hooks method used to install custom EndpointHooks.
  • iroh/src/protocol.rs – Contains the EndpointHooks trait, ProtocolHandler trait, and outcome enums (BeforeConnectOutcome, AfterHandshakeOutcome).
  • iroh/src/runtime.rs – Provides async runtime utilities used by OutgoingAuthTask for managing background tasks.

Summary

  • Mount EndpointHooks on your Endpoint builder to intercept connections at before_connect (outbound) and after_handshake (inbound) phases.
  • Implement a custom ProtocolHandler for the authentication ALPN to exchange tokens and populate a shared allowed_remotes set.
  • Return Reject outcomes from your hooks to block unauthorized connections before they reach application protocols.
  • Reference iroh/examples/auth-hook.rs for the complete token-based authentication implementation.

Frequently Asked Questions

Can I use this hook system with existing iroh protocols?

Yes. Because authentication operates at the endpoint level through hooks, you can secure any existing iroh protocol—including file transfer or NAT traversal—without modifying the protocol implementation itself. Simply mount the hooks on your Endpoint builder and register the authentication protocol alongside your application protocols.

What happens if the authentication token is incorrect?

If the token verification fails in the AuthProtocol handler, the connection is closed with a rejection code and the remote EndpointId is not added to the allowed_remotes set. Subsequent connection attempts from that peer will be rejected by the IncomingAuthHook returning AfterHandshakeOutcome::Reject, or by the OutgoingAuthHook failing its internal check.

How does the outbound hook avoid infinite loops when connecting to the auth service?

The OutgoingAuthHook checks if the current connection's ALPN matches the authentication protocol's ALPN (b"iroh-example/auth/0"). If it does, the hook immediately returns BeforeConnectOutcome::Accept, allowing the authentication connection to proceed without attempting recursive pre-authentication.

Where is the authenticated peer state stored?

The authenticated peer state is stored in a thread-safe shared data structure, typically a Mutex<HashSet<EndpointId>> wrapped in an Arc, and shared between the protocol handler and the hooks. In the reference implementation, this is the allowed_remotes field that tracks which peers have successfully presented valid tokens.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →