# How to Implement Custom Connection Filtering Using EndpointHooks in iroh

> Learn to implement custom connection filtering in iroh using EndpointHooks. Intercept connections before or after TLS handshake with this guide. Enhance your iroh endpoint capabilities today.

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

---

**You can implement custom connection filtering in iroh by implementing the `EndpointHooks` trait and attaching it to your `Endpoint` builder, allowing you to intercept connections before they are established (`before_connect`) or after the TLS handshake completes (`after_handshake`).**

The `iroh` library from n0-computer provides a robust QUIC-based networking stack with a flexible **Endpoint Hooks** mechanism for custom connection filtering. 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 inspect, log, or reject connections based on IP addresses, endpoint IDs, or application-specific logic without modifying the core networking code.

## Understanding Endpoint Hooks in iroh

The Endpoint Hooks system provides two distinct interception points in the connection lifecycle. When you attach hooks to an endpoint using `Builder::hooks`, they are stored in an `EndpointHooksList` and executed sequentially in the order they were added.

### The Two Hook Points

Hooks can intercept connections at two critical stages:

- **`before_connect`**: Runs immediately after an outgoing connection is requested via `Endpoint::connect` but **before any packets are sent**. It receives the remote address and ALPN identifier, returning `BeforeConnectOutcome::Accept` to proceed or `BeforeConnectOutcome::Reject` to abort locally without network traffic.

- **`after_handshake`**: Executes after the TLS (QUIC) handshake completes for both incoming and outgoing connections. At this stage, the remote's endpoint ID and ALPN are known via the `Connection` object. It returns `AfterHandshakeOutcome::Accept` or `AfterHandshakeOutcome::Reject { error_code, reason }` to close the connection with a specific QUIC error code.

### The EndpointHooks Trait Definition

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

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

```

When building an endpoint, the `Builder::hooks` method stores each hook in a `Vec<Box<dyn DynEndpointHooks>>`, allowing heterogeneous hook types to be chained together.

## Implementing Custom Connection Filtering

To create a custom filter, you define a struct holding your filtering criteria and implement `EndpointHooks` with your specific logic.

### Step-by-Step Implementation

1. **Define your filter struct** containing state such as whitelists, blacklists, or rate limiters.

2. **Implement `EndpointHooks`** for your struct, overriding `before_connect` for outbound filtering or `after_handshake` for inbound filtering.

3. **Return rejection outcomes** when connections should be denied. For `after_handshake`, provide a `VarInt` error code and reason string.

4. **Attach the hook** when constructing your endpoint using the builder pattern.

```rust
use iroh::endpoint::{Endpoint, EndpointHooks, Connection, BeforeConnectOutcome, AfterHandshakeOutcome};
use iroh::EndpointAddr;
use std::collections::HashSet;
use std::net::IpAddr;

#[derive(Debug)]
struct IpFilter {
    blocked_ips: HashSet<IpAddr>,
}

impl EndpointHooks for IpFilter {
    async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome {
        let remote_ip = conn.remote_addr().ip();
        if self.blocked_ips.contains(&remote_ip) {
            // QUIC error code 0x0b represents "Connection Refused"
            AfterHandshakeOutcome::reject(
                iroh::endpoint::VarInt::from_u32(0x0b),
                b"IP address blocked"
            )
        } else {
            AfterHandshakeOutcome::Accept
        }
    }
}

// Usage
let filter = IpFilter { blocked_ips: HashSet::new() };
let endpoint = Endpoint::builder()
    .hooks(filter)
    .bind("127.0.0.1:0".parse()?)?
    .listen()
    .await?;

```

## Practical Filtering Examples

The iroh repository provides concrete examples demonstrating different filtering strategies.

### Inbound IP Filtering

The file [`iroh/examples/incoming-filter.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/incoming-filter.rs) demonstrates a hook that rejects incoming connections from disallowed IP ranges. The implementation checks the remote IP address during `after_handshake`:

```rust
use iroh::endpoint::{EndpointHooks, Connection, AfterHandshakeOutcome};
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 {
            AfterHandshakeOutcome::reject(
                iroh::endpoint::VarInt::from_u32(0x0b),
                b"IP not in allowed subnet"
            )
        }
    }
}

```

This approach is effective for network-level access control because the TLS handshake completes before rejection, allowing you to log the attempt with full identity information.

### Outbound Endpoint ID Whitelisting

For strict trust boundaries on outgoing connections, implement `before_connect` to filter by the remote's **endpoint ID** (a 256-bit public key). This aborts the connection before any packets leave the local machine:

```rust
use iroh::endpoint::{EndpointHooks, EndpointAddr, BeforeConnectOutcome};
use iroh::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 {
            BeforeConnectOutcome::Reject
        }
    }
}

```

The [`auth-hook.rs`](https://github.com/n0-computer/iroh/blob/main/auth-hook.rs) example in the repository demonstrates advanced authentication patterns using this approach.

## Best Practices for Hook Implementation

When implementing custom connection filtering using EndpointHooks, follow these guidelines to ensure safety and performance:

- **Avoid storing an `Endpoint` reference** inside your hook struct. This creates a reference-count cycle that prevents the endpoint from being dropped, as noted in the documentation for [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs).

- **Keep hook execution lightweight** since they run on the connection task's executor. Offload heavy computations to background tasks using `tokio::spawn` and channels.

- **Prefer `after_handshake` for inbound filtering** because only then is the remote endpoint ID authenticated and available via the `Connection` object.

- **Return descriptive error codes** when rejecting in `after_handshake`. Use appropriate QUIC error codes (e.g., `0x0b` for connection refused) and clear reason strings to aid remote diagnostics.

- **Chain multiple hooks** by calling `.hooks(filter1).hooks(filter2)` on the builder. They execute in registration order, so place the most restrictive filters first.

## Summary

- **EndpointHooks** in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs) provides `before_connect` and `after_handshake` interception points for custom connection filtering.
- **Implement the trait** on your custom struct to define accept/reject logic based on IP addresses, endpoint IDs, or ALPN identifiers.
- **Attach hooks** via `Endpoint::builder().hooks(your_filter)` before calling `bind()` and `listen()`.
- **Use `before_connect`** to prevent outbound connections before packets are transmitted, and **`after_handshake`** to reject inbound connections with specific QUIC error codes.
- **Reference the examples** in [`iroh/examples/incoming-filter.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/incoming-filter.rs) and [`iroh/examples/auth-hook.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/auth-hook.rs) for production-ready implementations.

## Frequently Asked Questions

### What is the difference between `before_connect` and `after_handshake` hooks?

The `before_connect` hook runs when you call `Endpoint::connect()` but before any network packets are transmitted, making it ideal for preventing outbound connections based on unverified criteria like endpoint IDs. The `after_handshake` hook runs after the TLS handshake completes and cryptographic identity is established, allowing you to inspect the authenticated remote endpoint ID and ALPN before accepting or rejecting the connection.

### Can I use multiple hooks for layered filtering?

Yes, you can chain multiple hooks by calling `.hooks(filter1).hooks(filter2)` on the endpoint builder. The hooks are stored in an `EndpointHooksList` and executed sequentially in the order they were registered. If any hook returns `Reject`, the connection attempt aborts immediately without executing subsequent hooks.

### What error codes should I use when rejecting connections?

For `after_handshake` rejections, use QUIC error codes as `VarInt` values. Common choices include `0x00` for no error (graceful drop), `0x0b` (connection refused) for policy violations, or `0x01` (protocol violation) for ALPN mismatches. The error code and reason string are sent to the remote peer in a QUIC close frame, so choose codes that aid debugging without revealing sensitive internal state.

### Is it safe to perform async operations inside hook methods?

Yes, hook methods are async and return futures, but you should keep them lightweight since they execute on the connection task. For expensive operations like database lookups or external API calls, spawn a background task and use a channel to communicate the result. Never store an `Endpoint` inside your hook struct, as this creates a reference cycle that prevents proper cleanup.