How to Use EndpointHooks in Iroh for Connection Filtering
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, 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:
| 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 as the EndpointHooks trait:
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:
- Before Connect Phase: The endpoint invokes
before_connecton each hook in order. If any hook returnsReject, the connection attempt aborts immediately and subsequent hooks are skipped. - After Handshake Phase: Upon successful QUIC handshake, the endpoint invokes
after_handshakeon each hook. The firstRejectcauses 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:
- Define a struct containing your filter state (whitelists, rate limiters, or configuration).
- Implement
EndpointHooksfor your struct, overridingbefore_connectfor outbound filtering orafter_handshakefor inbound validation. - Return rejection outcomes with appropriate QUIC error codes (as
VarInt) when connections violate your policy. - 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 that demonstrates rejecting connections based on source IP address. Here is the essential implementation pattern:
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:
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: Demonstrates IP-based filtering usingafter_handshake.iroh/examples/auth-hook.rs: Shows advanced authentication hooks that validate endpoint IDs and certificates.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 and iroh/src/endpoint/connection.rs:
- Avoid storing
Endpointreferences 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::spawnor channels to avoid blocking the QUIC event loop. - Use
after_handshakefor identity validation. The remote endpoint ID is only available after the handshake completes, making this the correct hook for cryptographic authentication. - Use
before_connectfor 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_connectandafter_handshakeinterception points for fine-grained connection control, defined iniroh/src/endpoint/hooks.rs. - The
EndpointHookstrait requires implementing async methods that returnBeforeConnectOutcomeorAfterHandshakeOutcometo accept or reject connections. - Hooks execute sequentially in an
EndpointHooksList, with the first rejection terminating the connection attempt. - Inbound filtering should use
after_handshaketo access the remote endpoint ID and ALPN, as demonstrated iniroh/examples/incoming-filter.rs. - Outbound filtering should use
before_connectto abort connections locally before any network traffic occurs. - Never store
Endpointinstances 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:
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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →