How to Use Connection Hooks for Custom Authentication in Iroh
You can implement custom authentication in Iroh by creating a struct that implements the EndpointHooks trait and registering it via Endpoint::builder().hooks(), which intercepts connections at before_connect and after_handshake lifecycle points to accept or reject based on your validation logic.
Iroh provides a pluggable hook system that lets you inspect and control the connection lifecycle without modifying core library code. By implementing the EndpointHooks trait, you can inject custom authentication logic that executes during the QUIC handshake process. This approach allows you to validate remote identities, check tokens, or reject connections based on arbitrary criteria before data transfer begins.
How Connection Hooks Work in Iroh
Iroh’s endpoint architecture is built around hooks – pluggable components that are invoked at specific points of the connection lifecycle. The hook system is defined in iroh/src/endpoint/hooks.rs and integrated into the endpoint builder in iroh/src/endpoint.rs.
Hook Registration and Execution Order
When constructing an Endpoint, you attach hooks via the builder method Endpoint::builder().hooks(my_hook). According to the source code in iroh/src/endpoint.rs, hooks are stored in an EndpointHooksList and are executed in the order they were added.
Lifecycle Hook Points
The EndpointHooks trait defines two async callbacks that control connection flow:
-
before_connect(&self, remote_addr: &SocketAddr, alpn: &str) -> BeforeConnectOutcome– Called before the QUIC handshake starts. You can reject connections early based on IP address or ALPN protocol. -
after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome– Called after the handshake has completed and the remote endpoint’s identity is known. This is where you implement identity validation.
Both callbacks return an enum with Accept or Reject variants. Returning Reject aborts the connection immediately and prevents later hooks from running.
Implementing Custom Authentication Logic
To create a custom authentication hook, you define a struct that holds your validation state and implement the EndpointHooks trait.
Define the Hook Structure
Store any state you need for authentication, such as shared secrets, token validators, or database clients:
struct MyAuthHook {
secret: Vec<u8>, // Your secret or validator
}
Implement the EndpointHooks Trait
Use async_trait to implement the required methods. In after_handshake, you receive a &Connection which provides the remote endpoint ID via conn.remote_id() and a weak handle for later interaction:
#[async_trait::async_trait]
impl EndpointHooks for MyAuthHook {
async fn before_connect(
&self,
_remote_addr: &SocketAddr,
_alpn: &str,
) -> BeforeConnectOutcome {
// Optional: Reject connections early based on IP, etc.
BeforeConnectOutcome::Accept
}
async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome {
// The remote endpoint ID is now known:
let remote_id = conn.remote_id();
// Extract authentication token (typically sent via a dedicated QUIC stream)
let token = match conn.receive_auth_token().await {
Ok(t) => t,
Err(_) => return AfterHandshakeOutcome::Reject,
};
// Validate the token using your secret
if token == self.secret {
AfterHandshakeOutcome::Accept
} else {
AfterHandshakeOutcome::Reject
}
}
}
Note: The actual transport of the token is not prescribed by Iroh. You typically open a dedicated QUIC stream after the handshake and exchange a small authentication payload. The hook decides whether to allow the connection to proceed.
Register the Hook with the Endpoint Builder
Install your hook when building the endpoint:
let secret = b"my-shared-secret".to_vec();
let auth_hook = MyAuthHook { secret };
let endpoint = iroh::Endpoint::builder()
.bind_addr("[::]:0".parse().unwrap())
.hooks(auth_hook) // Install the custom auth hook
.build()
.await?;
Now every incoming connection passes through MyAuthHook. If the hook returns Reject, the connection closes immediately and the remote side receives a handshake abort error.
Client-Side Authentication Patterns
For mutual authentication where the client must prove its identity to the server, create a pair of hooks:
- Outgoing hook – Sends a token once the connection is established
- Incoming hook – Validates the token as shown above
The repository’s iroh/examples/auth-hook.rs demonstrates this pattern with helper functions auth::outgoing and auth::incoming that return (hook, task) tuples. You can adapt these helpers to use JWT, API keys, or TLS-derived secrets.
Key Source Files and Implementation Details
| Feature | File | Purpose |
|---|---|---|
| Hook trait definition & outcome enums | iroh/src/endpoint/hooks.rs |
Core hook API (EndpointHooks, BeforeConnectOutcome, AfterHandshakeOutcome) |
| Builder integration | iroh/src/endpoint.rs |
Builder::hooks method and storage of EndpointHooksList |
| Connection handling that invokes hooks | iroh/src/endpoint/connection.rs |
Calls before_connect and after_handshake at the right moments |
| Example authentication hook | iroh/examples/auth-hook.rs |
Full runnable demo of outgoing & incoming auth hooks |
| Remote-map hook (per-connection state) | iroh/examples/remote-info.rs |
Demonstrates a hook that records remote endpoint information |
| Monitoring connection example | iroh/examples/monitor-connections.rs |
Shows a hook that logs connection details for diagnostics |
Summary
- Register your hook with
Endpoint::builder().hooks(my_hook)to install it in the endpoint'sEndpointHooksList. - Implement
before_connectto filter connections early by IP or ALPN, andafter_handshaketo validate the authenticated peer's identity. - Use
AfterHandshakeOutcome::Rejectto abort unauthenticated connections before data transfer begins. - Access the remote endpoint ID via
conn.remote_id()insideafter_handshakefor identity validation. - Combine an outgoing hook (client) and an incoming hook (server) for mutual authentication schemes.
Frequently Asked Questions
What is the difference between before_connect and after_handshake hooks?
The before_connect hook runs before the QUIC handshake begins and only has access to the remote socket address and ALPN protocol string. The after_handshake hook runs after the cryptographic handshake completes, giving you access to the authenticated Connection object including the remote endpoint ID. Use before_connect for IP-based filtering and after_handshake for identity verification.
How do I access the remote peer's identity in an authentication hook?
Inside the after_handshake method, call conn.remote_id() on the &Connection parameter to retrieve the remote endpoint's identity. This method returns the verified public key or identifier of the peer, which you can validate against your authentication database or token.
Can I maintain state between hook invocations?
Yes. Since you define the hook struct yourself, you can store any stateful components such as database connections, cache clients, or shared secrets. For per-connection state tracking, see the iroh/examples/remote-info.rs example which demonstrates how to record remote endpoint information across the connection lifecycle.
What happens if a hook returns Reject?
When any hook returns BeforeConnectOutcome::Reject or AfterHandshakeOutcome::Reject, the connection is aborted immediately. The QUIC handshake fails, and the remote peer receives an error indicating the connection was rejected. Subsequent hooks in the EndpointHooksList are not executed for rejected connections.
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 →