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

> Master iroh connection lifecycle events by implementing before_connect and after_handshake hooks. Intercept and inspect connections efficiently with the EndpointHooks trait.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-19

---

**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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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.

```rust
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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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:

```rust
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:

```rust
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:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/remote-info.rs) demonstrates storing weak handles to connections for later aggregation:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/auth-hook.rs) shows both callbacks working together. The outgoing side uses `before_connect` to verify pre-authentication:

```rust
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:

```rust
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:

- **[`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs)** – Defines `EndpointHooks`, `BeforeConnectOutcome`, `AfterHandshakeOutcome`, and `EndpointHooksList`
- **[`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)** – Contains `Builder::hooks` registration and invocation points at lines 1096, 1195, and 2597
- **[`iroh/examples/remote-info.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/remote-info.rs)** – Complete `after_handshake` example with path statistics
- **[`iroh/examples/auth-hook.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/auth-hook.rs)** – Full demonstration of both callbacks with authentication logic
- **[`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs)** – Shows protocol-specific hook integration

## 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.