# How to Use EndpointHooks in Iroh for Connection Filtering

> Control Iroh connections with EndpointHooks. Learn how to use before_connect for outbound filtering and after_handshake for inbound validation to accept or reject connections.

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

---

**Iroh's `EndpointHooks` API allows you to intercept and control connection attempts by implementing the `EndpointHooks` trait, providing `before_connect` for outbound filtering and `after_handshake` for inbound validation to accept or reject connections with custom QUIC error codes.**

Iroh is a modern peer-to-peer networking library that exposes fine-grained control over QUIC connections through its `Endpoint` abstraction. The **EndpointHooks in Iroh for connection filtering** feature enables developers to implement security policies, rate limiting, and access control by inspecting connection metadata before and during the handshake phase. These hooks integrate directly into the connection lifecycle defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), allowing you to reject unwanted peers without establishing costly transport state.

## Endpoint Hook Points and Lifecycle

Iroh provides two distinct intervention points in the connection establishment flow, defined in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs):

| Hook Point | When It Executes | Return Type | Effect |
|------------|------------------|-------------|---------|
| `before_connect` | Immediately after `Endpoint::connect` is called, **before any packets are sent** | `BeforeConnectOutcome` (`Accept` or `Reject`) | Returning `Reject` aborts the connection attempt locally with zero network traffic |
| `after_handshake` | After the QUIC/TLS handshake completes for **both incoming and outgoing** connections | `AfterHandshakeOutcome` (`Accept` or `Reject { error_code, reason }`) | Returning `Reject` transmits a QUIC `CONNECTION_CLOSE` frame with the specified error code and reason string |

The `after_handshake` hook is particularly powerful because it executes after the remote's **endpoint ID** (a 256-bit public key) and ALPN protocol are known, enabling cryptographic identity-based filtering.

## Core Trait Definition

The hook interface is defined in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs) as the `EndpointHooks` trait:

```rust
pub trait EndpointHooks: std::fmt::Debug + Send + Sync {
    fn before_connect<'a>(
        &'a self,
        _remote_addr: &'a EndpointAddr,
        _alpn: &'a [u8],
    ) -> impl Future<Output = BeforeConnectOutcome> + Send + 'a {
        async { BeforeConnectOutcome::Accept }
    }

    fn after_handshake<'a>(
        &'a self,
        _conn: &'a Connection,
    ) -> impl Future<Output = AfterHandshakeOutcome> + Send + 'a {
        async { AfterHandshakeOutcome::Accept }
    }
}

```

Both methods provide default implementations that accept all connections, so you only need to override the specific hook point relevant to your use case.

## How the Hook Chain Works

When you attach multiple hooks via `Builder::hooks`, the endpoint constructs an **`EndpointHooksList`** (an internal `Vec<Box<dyn DynEndpointHooks>>`) that executes hooks sequentially in registration order:

1. **Before Connect Phase**: The endpoint invokes `before_connect` on each hook in order. If any hook returns `Reject`, the connection attempt aborts immediately and subsequent hooks are skipped.
2. **After Handshake Phase**: Upon successful QUIC handshake, the endpoint invokes `after_handshake` on each hook. The first `Reject` causes the connection to close with the specified error code.

This chaining behavior allows you to compose multiple filters—place the most specific or cheapest checks first to minimize latency.

## Implementing a Custom Connection Filter

To create a custom filter using **EndpointHooks in Iroh**, follow these implementation steps:

1. **Define a struct** containing your filter state (whitelists, rate limiters, or configuration).
2. **Implement `EndpointHooks`** for your struct, overriding `before_connect` for outbound filtering or `after_handshake` for inbound validation.
3. **Return rejection outcomes** with appropriate QUIC error codes (as `VarInt`) when connections violate your policy.
4. **Attach the hook** during endpoint construction using the builder pattern.

## Example: Simple Inbound IP Filter

The repository includes a concrete example in [`iroh/examples/incoming-filter.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/incoming-filter.rs) that demonstrates rejecting connections based on source IP address. Here is the essential implementation pattern:

```rust
use iroh::endpoint::{Connection, EndpointHooks, AfterHandshakeOutcome};
use iroh_base::endpoint::EndpointAddr;
use iroh_quinn::VarInt;
use ipnet::Ipv4Net;
use std::net::IpAddr;

#[derive(Debug)]
struct IncomingFilter {
    allowed_subnet: Ipv4Net,
}

impl EndpointHooks for IncomingFilter {
    async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome {
        let remote_ip = conn.remote_addr().ip();
        if self.allowed_subnet.contains(&remote_ip) {
            AfterHandshakeOutcome::Accept
        } else {
            // QUIC error 0x0b = "Connection Refused"
            AfterHandshakeOutcome::reject(VarInt::from_u32(0x0b), b"IP not allowed")
        }
    }
}

```

When deployed, this hook inspects every incoming connection after the handshake completes. Connections originating outside the configured subnet receive a QUIC `CONNECTION_CLOSE` frame with error code `0x0b` and the reason string "IP not allowed".

## Example: Outbound Whitelist by Endpoint ID

For outbound connections, use `before_connect` to filter by the remote's **endpoint ID** (public key) before any network packets leave the local machine:

```rust
use iroh::endpoint::{EndpointHooks, BeforeConnectOutcome, EndpointAddr};
use iroh_base::endpoint::EndpointId;
use std::collections::HashSet;

#[derive(Debug)]
struct OutboundWhitelist {
    allowed_ids: HashSet<EndpointId>,
}

impl EndpointHooks for OutboundWhitelist {
    async fn before_connect(
        &self,
        remote_addr: &EndpointAddr,
        _alpn: &[u8],
    ) -> BeforeConnectOutcome {
        if self.allowed_ids.contains(&remote_addr.id) {
            BeforeConnectOutcome::Accept
        } else {
            // Abort immediately without transmitting any packets
            BeforeConnectOutcome::Reject
        }
    }
}

```

This pattern is ideal for implementing strict trust boundaries where you only communicate with pre-approved peers, as the rejection occurs entirely locally with zero network overhead.

## Additional Reference Implementations

The Iroh repository provides several complete examples demonstrating **EndpointHooks for connection filtering**:

- **[`iroh/examples/incoming-filter.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/incoming-filter.rs)**: Demonstrates IP-based filtering using `after_handshake`.
- **[`iroh/examples/auth-hook.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/auth-hook.rs)**: Shows advanced authentication hooks that validate endpoint IDs and certificates.
- **[`iroh/examples/monitor-connections.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/monitor-connections.rs)**: Illustrates logging and monitoring hooks that track connection establishment without rejecting traffic.

## Best Practices for Endpoint Hooks

When implementing **connection filtering with EndpointHooks in Iroh**, adhere to these guidelines from the source code in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs) and [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs):

- **Avoid storing `Endpoint` references in hooks**. This creates reference cycles that prevent the endpoint from shutting down properly.
- **Keep hook logic lightweight**. Hooks execute on the connection task's executor; perform heavy work asynchronously via `tokio::spawn` or channels to avoid blocking the QUIC event loop.
- **Use `after_handshake` for identity validation**. The remote endpoint ID is only available after the handshake completes, making this the correct hook for cryptographic authentication.
- **Use `before_connect` for early rejection**. Filter by IP or endpoint ID before any packets are sent to minimize resource consumption and exposure.
- **Provide descriptive error codes**. When rejecting in `after_handshake`, use standard QUIC error codes and clear reason strings to aid remote diagnostics.

## Summary

- **EndpointHooks** in Iroh provide `before_connect` and `after_handshake` interception points for fine-grained connection control, defined in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs).
- The `EndpointHooks` trait requires implementing async methods that return `BeforeConnectOutcome` or `AfterHandshakeOutcome` to accept or reject connections.
- Hooks execute sequentially in an `EndpointHooksList`, with the first rejection terminating the connection attempt.
- **Inbound filtering** should use `after_handshake` to access the remote endpoint ID and ALPN, as demonstrated in [`iroh/examples/incoming-filter.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/incoming-filter.rs).
- **Outbound filtering** should use `before_connect` to abort connections locally before any network traffic occurs.
- Never store `Endpoint` instances inside hook structs to avoid memory leaks.

## Frequently Asked Questions

### How do I attach multiple EndpointHooks to a single Iroh endpoint?

You can chain multiple hooks using the builder pattern's `hooks` method. Each call appends the hook to the internal `EndpointHooksList`, and they execute in the order registered:

```rust
let endpoint = Endpoint::builder()
    .hooks(ip_filter)
    .hooks(auth_filter)
    .bind(local_addr)?
    .listen()
    .await?;

```

### What is the difference between Reject in before_connect versus after_handshake?

A `Reject` from `before_connect` aborts the connection locally before any QUIC packets are transmitted, resulting in zero network overhead. A `Reject` from `after_handshake` occurs after the cryptographic handshake completes and sends a QUIC `CONNECTION_CLOSE` frame to the peer with a specified error code and reason string, allowing the remote side to log why the connection was refused.

### Can I access the remote's public key (endpoint ID) in before_connect?

No. The remote endpoint ID is only established after the cryptographic handshake completes. In `before_connect`, you have access to the `EndpointAddr` (which may contain a provisional ID based on the address) and the ALPN, but cryptographic verification happens later. Use `after_handshake` to filter based on the authenticated endpoint ID.

### Are EndpointHooks called for every connection or just incoming ones?

EndpointHooks are invoked for **both incoming and outgoing** connections. The `before_connect` hook only executes for outbound connections (initiated via `Endpoint::connect`), while `after_handshake` executes for all connections after the TLS/QUIC handshake completes, regardless of which side initiated the connection.