How to Implement Endpoint Hooks for Before-Connect and After-Handshake Events in iroh

Implement endpoint hooks in iroh by creating a type that implements the EndpointHooks trait and registering it via Endpoint::builder().hooks(your_hook), allowing you to intercept connections before they are established and immediately after the TLS handshake completes.

The iroh networking stack from n0-computer provides a robust hook system for controlling connection lifecycles. By implementing endpoint hooks, you can execute custom logic at two critical moments: immediately before an outgoing connection attempt begins, and right after any connection's TLS handshake succeeds. This guide explains how to implement endpoint hooks for before-connect and after-handshake events in iroh based on the actual source implementation.

The EndpointHooks Trait Interface

The hook system is defined in iroh/src/endpoint/hooks.rs. Any type implementing the EndpointHooks trait can intercept connections through two asynchronous callbacks:

Before-Connect Callback

The before_connect method runs synchronously when Endpoint::connect or Endpoint::connect_with_opts is invoked, but before any packets leave the node. It receives the remote address and ALPN protocol identifier, returning a BeforeConnectOutcome that either accepts or rejects the attempt.

async fn before_connect<'a>(
    &'a self,
    remote_addr: &'a EndpointAddr,
    alpn: &'a [u8],
) -> BeforeConnectOutcome;

Use this for pre-authentication checks, rate-limiting, or blocking specific endpoint IDs.

After-Handshake Callback

The after_handshake method executes after the TLS handshake completes for both incoming and outgoing connections. At this point, the remote's endpoint ID and ALPN are fully validated. It returns an AfterHandshakeOutcome allowing you to accept the connection or reject it with a specific QUIC error code and reason string.

async fn after_handshake<'a>(&'a self, conn: &'a Connection) -> AfterHandshakeOutcome;

Use this for logging connection metrics, enforcing post-handshake authorization, or inspecting path statistics.

Registering and Dispatching Hooks

Hooks are registered during endpoint construction through Builder::hooks(hook) in iroh/src/endpoint.rs. The builder stores hooks in an internal EndpointHooksList, which supports multiple hooks called in registration order.

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

According to the source code analysis, the dispatcher invokes these hooks at specific points:

  • before_connect is called at lines 1096 and 1195 of iroh/src/endpoint.rs when initiating outbound connections
  • after_handshake is called at line 2597 and within the accept flow after the TLS handshake finishes

If any hook returns Reject, the connection attempt aborts immediately without further network activity.

Implementing a Before-Connect Hook

The following example demonstrates a hook that blocks connections to specific endpoint IDs before any network packets are transmitted:

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
        }
    }
}

Install the hook during endpoint setup:

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

Implementing an After-Handshake Hook

The following hook logs connection details after the TLS handshake completes, using Connection::remote_id and Connection::alpn to inspect the established session:

use iroh::{
    endpoint::{EndpointHooks, AfterHandshakeOutcome, 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
    }
}

To reject a connection with a custom error code, return AfterHandshakeOutcome::Reject:

AfterHandshakeOutcome::Reject {
    error_code: 403u32.into(),
    reason: b"not authenticated".to_vec(),
}

Production Patterns from the iroh Repository

The iroh codebase includes two canonical examples demonstrating hook patterns:

Remote Mapping (iroh/examples/remote-info.rs) Implements RemoteMapHook using only after_handshake to record connections and aggregate path statistics. The hook stores a weak handle to each connection via Connection::weak_handle, allowing safe monitoring without preventing connection closure.

Authentication (iroh/examples/auth-hook.rs) Demonstrates both callbacks working together:

  • OutgoingAuthHook implements before_connect to verify pre-authentication status before dialing
  • IncomingAuthHook implements after_handshake to reject unauthenticated peers with a 403 error code

Critical Implementation Guidelines

When implementing EndpointHooks, observe these constraints from the source code:

  • Avoid Reference Cycles: Do not store an Endpoint inside your hook. The hook is reference-counted by the endpoint, creating a cycle that leaks memory (documented in the "Notes to implementers" comment in iroh/src/endpoint/hooks.rs).
  • Use Weak Handles: If you need to reference a connection after the hook returns, use Connection::weak_handle instead of holding the connection directly.
  • Minimize Latency: Hooks run on the connection task. Offload heavy work to background tasks via channels to avoid blocking the endpoint.

Summary

  • Implement the EndpointHooks trait defined in iroh/src/endpoint/hooks.rs to create custom hooks
  • Use before_connect to intercept outgoing connections before packet transmission via BeforeConnectOutcome::Accept or Reject
  • Use after_handshake to inspect established connections and optionally close them with custom QUIC error codes via AfterHandshakeOutcome
  • Register hooks using Endpoint::builder().hooks(hook); multiple hooks execute in registration order
  • Consult iroh/examples/remote-info.rs for monitoring patterns and iroh/examples/auth-hook.rs for authentication flows
  • Never store Endpoint instances inside hooks to prevent memory leaks

Frequently Asked Questions

What is the difference between before_connect and after_handshake hooks?

The before_connect hook runs only for outgoing connections initiated via Endpoint::connect, allowing you to reject attempts before any network activity occurs. The after_handshake hook runs for both incoming and outgoing connections immediately after the TLS handshake succeeds, when the remote endpoint ID and ALPN are known but before application data flows.

Can I register multiple hooks on a single endpoint?

Yes. The Builder::hooks method appends hooks to an internal EndpointHooksList. When events trigger, iroh iterates through all registered hooks in order, stopping if any hook returns a Reject outcome. This allows composable security policies where multiple checks must pass.

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

In after_handshake, return AfterHandshakeOutcome::Reject { error_code, reason } where error_code is a QUIC application error code (converted via .into()) and reason is a byte slice containing a UTF-8 explanation. The remote peer receives this error when the connection closes.

What happens if my hook performs expensive operations?

Hooks execute on the connection's async task. Blocking operations will delay connection establishment or acceptance. According to the implementation notes in iroh/src/endpoint/hooks.rs, you should offload heavy computation to background tasks using channels or spawn tasks, keeping the hook callback lightweight and fast.

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 →