How to Implement before-connect and after-handshake Hooks in iroh

You can intercept connection lifecycle events in iroh by implementing the EndpointHooks trait, which provides before_connect to filter outgoing connections before they start and after_handshake to inspect any connection after TLS completes.

The iroh networking library provides a powerful hook system that lets you control and monitor QUIC connections at critical lifecycle moments. By implementing the EndpointHooks trait defined in iroh/src/endpoint/hooks.rs, you can execute custom logic before an outgoing connection attempt or immediately after any TLS handshake completes. This guide walks through the implementation using real source code from the n0-computer/iroh repository.

Understanding the EndpointHooks Trait

The EndpointHooks trait is the core abstraction for connection interception. Any type implementing this trait can be registered with an Endpoint to receive callbacks at specific stages of the connection lifecycle.

The before_connect Callback

The before_connect method runs immediately before an outgoing connection is created, specifically when Endpoint::connect or Endpoint::connect_with_opts is called. It receives the remote address and ALPN protocol bytes, and returns a BeforeConnectOutcome enum with two variants:

  • BeforeConnectOutcome::Accept – Proceed with the connection attempt
  • BeforeConnectOutcome::Reject – Abort the connection before any packet leaves the node

This hook is ideal for pre-authentication, rate-limiting, or address filtering. According to the source code in iroh/src/endpoint.rs, the dispatcher at line 1096 checks this outcome before initiating the QUIC handshake.

The after_handshake Callback

The after_handshake method executes after the TLS handshake completes for any connection, incoming or outgoing. At this point, the remote's endpoint ID and ALPN are known. The method returns an AfterHandshakeOutcome enum:

  • AfterHandshakeOutcome::Accept – Keep the connection open
  • AfterHandshakeOutcome::Reject { error_code, reason } – Close the connection with a specific QUIC error code and UTF-8 reason string

As implemented in iroh/src/endpoint.rs at line 2597, this callback allows you to inspect the established Connection, record metrics, or reject based on path statistics.

How the Hook System Works

Registration via Builder::hooks

You install hooks during endpoint construction using the builder pattern. The Builder::hooks method stores your hook in an internal EndpointHooksList structure. Multiple hooks can be added; they execute in registration order.

let endpoint = Endpoint::builder(presets::N0)
    .hooks(my_hook)          // Install the hook here
    .bind()
    .await?;

Invocation Points in the Connection Lifecycle

The hook system invokes callbacks at specific points in iroh/src/endpoint.rs:

  1. Outgoing connections: Lines 1096 and 1195 call EndpointHooksList::before_connect before the handshake begins
  2. All connections: Line 2597 calls EndpointHooksList::after_handshake immediately after TLS completion

If any hook returns a rejection outcome, the connection aborts immediately without further processing.

Handling Outcomes

The outcome enums in iroh/src/endpoint/hooks.rs provide semantic control:

  • BeforeConnectOutcome is binary—accept or reject silently
  • AfterHandshakeOutcome::reject lets you specify a custom error code and reason bytes for debugging or protocol compliance

Designing Effective Hooks

Avoiding Memory Leaks

Do not store an Endpoint reference inside your hook. The hook is reference-counted by the endpoint; keeping a strong reference creates a cycle that leaks memory. The source code explicitly warns about this in the "Notes to implementers" comment.

If you need to persist a connection reference after the hook returns, use Connection::weak_handle. This weak reference does not keep the connection alive but allows later inspection.

Performance Considerations

Hook callbacks run synchronously on the connection's task. Heavy computation blocks the async runtime:

  • Offload CPU-intensive work to background tasks via channels
  • Keep database lookups and cryptographic operations minimal
  • Return outcomes quickly to prevent handshake timeouts

Implementation Examples

Blocking Specific Endpoint IDs

This minimal hook implements before_connect to blocklist specific remote IDs before any network traffic occurs:

use iroh::{
    endpoint::{EndpointHooks, BeforeConnectOutcome},
    EndpointAddr,
};

#[derive(Debug)]
struct BlocklistHook {
    blocked: iroh::EndpointId,
}

impl EndpointHooks for BlocklistHook {
    async fn before_connect<'a>(
        &'a self,
        remote_addr: &'a EndpointAddr,
        _alpn: &'a [u8],
    ) -> BeforeConnectOutcome {
        if remote_addr.id == self.blocked {
            BeforeConnectOutcome::Reject
        } else {
            BeforeConnectOutcome::Accept
        }
    }
}

Installation:

let blocked = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".parse().unwrap();
let hook = BlocklistHook { blocked };
let endpoint = iroh::Endpoint::builder(presets::N0)
    .hooks(hook)
    .bind()
    .await?;

Logging Successful Handshakes

This after_handshake hook logs every connection after TLS completes:

use iroh::{
    endpoint::{EndpointHooks, AfterHandshakeOutcome},
    endpoint::Connection,
};
use tracing::info;

#[derive(Debug)]
struct LoggingHook;

impl EndpointHooks for LoggingHook {
    async fn after_handshake<'a>(
        &'a self,
        conn: &'a Connection,
    ) -> AfterHandshakeOutcome {
        info!(
            remote = %conn.remote_id().fmt_short(),
            alpn = %std::str::from_utf8(conn.alpn()).unwrap_or("<invalid>"),
            "handshake completed"
        );
        AfterHandshakeOutcome::Accept
    }
}

Recording Remote Statistics

The RemoteMapHook from iroh/examples/remote-info.rs demonstrates storing weak handles to connections for later aggregation:

use iroh::{
    endpoint::{EndpointHooks, AfterHandshakeOutcome},
    endpoint::remote_map::{RemoteMap, RemoteMapHook},
};

let (hook, remote_map) = RemoteMap::new();
let endpoint = iroh::Endpoint::builder(presets::N0)
    .hooks(hook)                // Registers the RemoteMapHook (after_handshake only)
    .bind()
    .await?;

This pattern allows querying live connections and computing RTT statistics without keeping connections alive indefinitely.

Authenticating Connections

The complete example in iroh/examples/auth-hook.rs shows both callbacks working together. The outgoing side uses before_connect to verify pre-authentication:

impl EndpointHooks for OutgoingAuthHook {
    async fn before_connect<'a>(
        &'a self,
        remote_addr: &'a EndpointAddr,
        alpn: &'a [u8],
    ) -> BeforeConnectOutcome {
        if alpn == auth::ALPN {
            return BeforeConnectOutcome::Accept; // Don't block auth protocol
        }
        match self.authenticate(remote_addr.id).await {
            Ok(()) => BeforeConnectOutcome::Accept,
            Err(_) => BeforeConnectOutcome::Reject,
        }
    }
}

The incoming side uses after_handshake to enforce authorization:

impl EndpointHooks for IncomingAuthHook {
    async fn after_handshake<'a>(
        &'a self,
        conn: &'a Connection,
    ) -> AfterHandshakeOutcome {
        if conn.alpn() == auth::ALPN
            || self.allowed_remotes.lock().unwrap().contains(&conn.remote_id())
        {
            AfterHandshakeOutcome::Accept
        } else {
            AfterHandshakeOutcome::Reject {
                error_code: 403u32.into(),
                reason: b"not authenticated".to_vec(),
            }
        }
    }
}

Key Source Files

These files contain the reference implementations:

Summary

  • Implement EndpointHooks to intercept connections in iroh using two async callbacks: before_connect for outgoing connections and after_handshake for all connections after TLS
  • Register hooks via Endpoint::builder().hooks(hook) before calling bind(); multiple hooks execute in registration order
  • Return outcomes to control flow: BeforeConnectOutcome accepts or rejects silently, while AfterHandshakeOutcome can reject with custom QUIC error codes
  • Avoid cycles by never storing the Endpoint inside your hook; use Connection::weak_handle for persistent references
  • Keep callbacks fast to prevent blocking the connection task; offload heavy work to background threads

Frequently Asked Questions

What is the difference between before_connect and after_handshake in iroh?

The before_connect hook runs only for outgoing connections immediately before the handshake begins, allowing you to prevent connection attempts based on address or ALPN without network traffic. The after_handshake hook runs after TLS completes for both incoming and outgoing connections, when the remote identity is verified and the connection is technically established but not yet accepted for application use.

Can I install multiple hooks on a single iroh Endpoint?

Yes. The Builder::hooks method appends hooks to an internal EndpointHooksList vector. When events fire, iroh invokes each hook in the order they were registered. If any hook returns a rejection outcome, the connection aborts immediately and subsequent hooks are not called for that specific event.

How do I reject a connection with a custom error code in iroh?

Use AfterHandshakeOutcome::Reject { error_code, reason } in your after_handshake implementation. The error code must be a VarInt (typically a u32 converted via .into()), and the reason is a Vec<u8> containing UTF-8 text. This sends a QUIC CONNECTION_CLOSE frame with your specified code to the remote peer.

What happens if a hook panics during execution?

The hook callbacks run synchronously on the connection task. If a hook panics, it will poison the task and likely cause the endpoint to stop accepting or establishing connections. You should design hooks to be infallible by catching errors internally and returning Accept with logged warnings rather than unwinding the stack.

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 →