How iroh Endpoint Hooks Filter Incoming Connections
Iroh endpoint hooks filter incoming connections by implementing the after_handshake method on the EndpointHooks trait, returning AfterHandshakeOutcome::Accept to allow the connection or AfterHandshakeOutcome::Reject to terminate it immediately with a QUIC error code.
The n0-computer/iroh repository provides a flexible networking stack where endpoint hooks act as gatekeepers during connection establishment. When a remote peer attempts to connect, these hooks inspect the connection details after the TLS handshake completes and decide whether to keep or discard the session. This mechanism allows developers to implement custom authentication, blocklisting, or rate limiting directly within the endpoint configuration.
The Connection Filtering Flow
When an incoming connection arrives at an iroh Endpoint, the filtering process follows a strict sequence defined in iroh/src/endpoint/hooks.rs. The hooks are invoked synchronously during the handshake completion phase.
1. Handshake Completion
First, the TLS handshake completes. At this point, the remote peer’s endpoint ID and ALPN (Application-Layer Protocol Negotiation) are known and available to the hook implementation.
2. Hook Invocation
The EndpointHooks::after_handshake method is called for every hook registered on the endpoint. According to the source code in iroh/src/endpoint/hooks.rs (lines 87-94), the method signature allows asynchronous inspection of the connection:
fn after_handshake<'a>(
&'a self,
conn: &'a Connection,
) -> impl Future<Output = AfterHandshakeOutcome> + Send + 'a
3. Outcome Determination
Each hook returns an AfterHandshakeOutcome. The enum defines two critical variants:
Accept– Signals that the connection should proceed normally.Reject { error_code, reason }– Immediately closes the connection with the specified QUIC error code (as aVarInt) and reason string.
If any hook returns Reject, the processing stops immediately and subsequent hooks are not invoked. This short-circuit behavior means the first rejection effectively filters the connection without incurring the cost of running additional hooks.
The default implementation of after_handshake simply returns Accept, meaning connections are allowed unless a custom hook explicitly overrides this behavior.
Implementing a Custom Filter Hook
To filter incoming connections, implement the EndpointHooks trait and override the after_handshake method. The following example demonstrates a blocklist hook that rejects connections from specific endpoint IDs:
use iroh::endpoint::{Builder, EndpointHooks, AfterHandshakeOutcome};
use iroh::endpoint::connection::Connection;
use iroh::endpoint::quic::VarInt;
/// A hook that rejects connections from blacklisted endpoint IDs.
#[derive(Debug)]
struct BlocklistHook {
blocked_id: iroh_base::EndpointId,
}
impl EndpointHooks for BlocklistHook {
fn after_handshake<'a>(
&'a self,
conn: &'a Connection,
) -> impl Future<Output = AfterHandshakeOutcome> + Send + 'a {
async move {
if conn.remote_id() == self.blocked_id {
// 0x01 = "Application error" (QUIC error code)
AfterHandshakeOutcome::reject(VarInt::from_u32(0x01), b"blocked endpoint")
} else {
AfterHandshakeOutcome::Accept
}
}
}
}
// Build an endpoint with the hook installed
let endpoint = Builder::default()
.hooks(BlocklistHook { blocked_id: ... })
.bind()?;
In this implementation, the hook inspects conn.remote_id() after the handshake. If the ID matches the blocklist, it returns Reject, causing the connection to close instantly with error code 0x01 and the reason "blocked endpoint".
Common Hook Patterns
The iroh repository provides several examples demonstrating different filtering strategies:
- Authentication Hook (
iroh/examples/auth-hook.rs): Verifies tokens presented by the remote after the TLS handshake. ReturnsRejectif the authentication token mismatches or is missing. - Remote Information Hook (
iroh/examples/remote-info.rs): Records remote endpoint details for logging or metrics. Typically returnsAcceptunless recording fails. - Incoming Filter (
iroh/examples/incoming-filter.rs): Implements higher-level filtering usingIncomingFilterOutcome(defined iniroh/src/protocol.rs). This wrapper providesAccept,Reject,Retry, orIgnoredecisions based on remote address validation status.
Key Source Files
| File | Purpose |
|---|---|
iroh/src/endpoint/hooks.rs |
Defines EndpointHooks, BeforeConnectOutcome, AfterHandshakeOutcome, and the hook-invocation logic. |
iroh/src/protocol.rs |
Implements IncomingFilter and IncomingFilterOutcome for higher-level filtering abstractions. |
iroh/examples/auth-hook.rs |
Demonstrates rejecting connections based on custom authentication logic. |
iroh/examples/incoming-filter.rs |
Shows retry logic for connections with unvalidated remote addresses. |
Summary
- Endpoint hooks intercept incoming connections after the TLS handshake in
iroh/src/endpoint/hooks.rs. - The
after_handshakemethod returnsAfterHandshakeOutcome::Acceptto allow connections orRejectto close them with a QUIC error code. - Hooks process sequentially, and the first
Rejectoutcome stops the chain, preventing later hooks from executing. - The
Connectionobject provides access to the remote endpoint ID and ALPN for inspection. - Default behavior is permissive (
Accept), requiring explicit override for filtering.
Frequently Asked Questions
How do endpoint hooks differ from incoming filters?
Incoming filters are a higher-level abstraction defined in iroh/src/protocol.rs that wrap the low-level hook outcomes. While AfterHandshakeOutcome provides binary Accept/Reject decisions, IncomingFilterOutcome adds Retry and Ignore variants for more nuanced handling of connection attempts. The underlying mechanism still relies on the EndpointHooks trait.
Can multiple hooks be registered on a single endpoint?
Yes. The Builder accepts hooks via the .hooks() method, and multiple hooks can be chained. However, if any hook returns Reject, the connection is terminated immediately and remaining hooks are skipped. This design ensures that security-critical hooks (like authentication) can prevent wasted computation on subsequent filters.
What error codes should be used when rejecting connections?
The Reject variant accepts a VarInt error code and a byte slice reason string. While iroh uses standard QUIC error codes internally, application-specific hooks typically use codes in the range reserved for application errors (e.g., 0x01 through 0x3fff). The auth-hook.rs example demonstrates using custom error codes to signal specific failure modes to the connecting peer.
Is there a hook for filtering outgoing connections?
Yes. The same iroh/src/endpoint/hooks.rs file defines BeforeConnectOutcome for filtering outgoing connections before they are established. While the after_handshake flow handles incoming connections, outgoing filtering uses the before_connect method to decide whether to initiate a connection to a remote endpoint based on local policy.
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 →