How Iroh Endpoint Hooks Filter Incoming Connections: A Complete Guide
Iroh endpoint hooks filter incoming connections by implementing the after_handshake method to return either Accept or Reject, where the first rejection immediately terminates the connection and prevents subsequent hooks from executing.
The n0-computer/iroh repository provides a Rust networking stack that allows developers to intercept incoming connections at the endpoint level. Understanding how iroh endpoint hooks filter incoming connections enables you to implement custom authentication, blocklists, and access control policies before the application layer processes any data.
The Connection Establishment Flow
When a remote peer attempts to connect to your iroh endpoint, the networking stack executes a specific sequence before allowing the connection to proceed. First, the TLS handshake completes, making the remote's endpoint ID and Application-Layer Protocol Negotiation (ALPN) available to the local node.
Immediately after handshake completion, the endpoint invokes the EndpointHooks::after_handshake method for every hook registered on the endpoint. According to the implementation in iroh/src/endpoint/hooks.rs (lines 87-94), this occurs at a critical decision point where the connection exists but no application data has been exchanged.
How Filtering Decisions Work
Each endpoint hook must return an AfterHandshakeOutcome that determines the connection's fate. The enum provides two variants for filtering:
AfterHandshakeOutcome::Accept– Allows the connection to proceed normally.AfterHandshakeOutcome::Reject { error_code, reason }– Immediately closes the connection with the specified QUIC error code and reason string.
The filtering logic implements a short-circuit pattern: if any hook returns Reject, the endpoint closes the connection instantly and skips all remaining hooks. This design ensures that security-critical hooks—such as those handling authentication or blocklists—can terminate unwanted connections without wasting resources on additional validation.
The default implementation of after_handshake simply returns Accept, meaning connections are allowed unless explicitly overridden by custom hook logic.
Implementing a Custom Connection Filter
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 simple hook that rejects connections from a black‑listed endpoint ID.
#[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 TLS handshake completes. If the remote endpoint ID matches the blocklist entry, the hook returns Reject with a QUIC error code 0x01, causing the connection to close immediately with the reason string "blocked endpoint".
Higher-Level Incoming Filters
While EndpointHooks provide low-level control, iroh also offers higher-level abstractions in iroh/src/protocol.rs. The IncomingFilter trait wraps the low-level hook outcomes and provides the IncomingFilterOutcome enum with four states:
Accept– Proceed with the connection.Reject– Close the connection immediately.Retry– Request the remote to retry (useful for address validation).Ignore– Silently drop the connection.
The repository includes practical examples demonstrating these patterns:
iroh/examples/auth-hook.rs– Shows authentication token validation that rejects connections with custom error codes.iroh/examples/incoming-filter.rs– Demonstrates address validation using the retry mechanism.
Summary
- Iroh endpoint hooks intercept connections after the TLS handshake via the
after_handshakemethod defined iniroh/src/endpoint/hooks.rs. - Hooks return
AfterHandshakeOutcome::Acceptto allow connections orAfterHandshakeOutcome::Rejectto close them with specific QUIC error codes. - The first rejection wins: subsequent hooks do not execute once a hook returns
Reject. - The default implementation permits all connections, requiring custom logic to implement filtering.
- Higher-level IncomingFilter abstractions in
iroh/src/protocol.rsprovide additionalRetryandIgnoreoutcomes for complex handshaking scenarios.
Frequently Asked Questions
What happens when multiple hooks are registered on an iroh endpoint?
When multiple hooks are registered, iroh executes them sequentially in the order they were added. However, the system short-circuits on the first Reject outcome, meaning if Hook A rejects the connection, Hook B never executes. This behavior ensures that security-critical filters can block malicious connections before resource-intensive validation logic runs.
Can iroh endpoint hooks modify connections instead of just filtering them?
No, the EndpointHooks trait is designed strictly for accept/reject decisions immediately after the handshake. The after_handshake method signature allows inspecting the Connection object to read remote endpoint IDs and ALPN information, but it does not provide mutable access to modify connection parameters. For protocol-level modifications, implement custom logic in the application layer after the connection is accepted.
What is the difference between EndpointHooks and IncomingFilter in iroh?
EndpointHooks provide low-level QUIC connection control with binary Accept/Reject outcomes, operating immediately after the TLS handshake in iroh/src/endpoint/hooks.rs. IncomingFilter, defined in iroh/src/protocol.rs, offers a higher-level abstraction with four outcomes: Accept, Reject, Retry, and Ignore. IncomingFilter is typically used for application-layer protocol handlers that need to request retries or silently ignore connections rather than just accepting or rejecting them.
How do I provide a custom error code when rejecting a connection in iroh?
When returning AfterHandshakeOutcome::Reject, specify a QUIC error code using VarInt::from_u32() and provide a byte string reason. For example: AfterHandshakeOutcome::reject(VarInt::from_u32(0x01), b"blocked endpoint"). The error code 0x01 typically represents an application-level error, while custom codes in the range reserved for private use can be defined for your specific protocol.
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 →