How to Implement before-connect and after-handshake Hooks in iroh
You can intercept connection lifecycle events in iroh by implementing the EndpointHooks trait, which provides before_connect to filter outgoing connections before they start and after_handshake to inspect any connection after TLS completes.
The iroh networking library provides a powerful hook system that lets you control and monitor QUIC connections at critical lifecycle moments. By implementing the EndpointHooks trait defined in iroh/src/endpoint/hooks.rs, you can execute custom logic before an outgoing connection attempt or immediately after any TLS handshake completes. This guide walks through the implementation using real source code from the n0-computer/iroh repository.
Understanding the EndpointHooks Trait
The EndpointHooks trait is the core abstraction for connection interception. Any type implementing this trait can be registered with an Endpoint to receive callbacks at specific stages of the connection lifecycle.
The before_connect Callback
The before_connect method runs immediately before an outgoing connection is created, specifically when Endpoint::connect or Endpoint::connect_with_opts is called. It receives the remote address and ALPN protocol bytes, and returns a BeforeConnectOutcome enum with two variants:
BeforeConnectOutcome::Accept– Proceed with the connection attemptBeforeConnectOutcome::Reject– Abort the connection before any packet leaves the node
This hook is ideal for pre-authentication, rate-limiting, or address filtering. According to the source code in iroh/src/endpoint.rs, the dispatcher at line 1096 checks this outcome before initiating the QUIC handshake.
The after_handshake Callback
The after_handshake method executes after the TLS handshake completes for any connection, incoming or outgoing. At this point, the remote's endpoint ID and ALPN are known. The method returns an AfterHandshakeOutcome enum:
AfterHandshakeOutcome::Accept– Keep the connection openAfterHandshakeOutcome::Reject { error_code, reason }– Close the connection with a specific QUIC error code and UTF-8 reason string
As implemented in iroh/src/endpoint.rs at line 2597, this callback allows you to inspect the established Connection, record metrics, or reject based on path statistics.
How the Hook System Works
Registration via Builder::hooks
You install hooks during endpoint construction using the builder pattern. The Builder::hooks method stores your hook in an internal EndpointHooksList structure. Multiple hooks can be added; they execute in registration order.
let endpoint = Endpoint::builder(presets::N0)
.hooks(my_hook) // Install the hook here
.bind()
.await?;
Invocation Points in the Connection Lifecycle
The hook system invokes callbacks at specific points in iroh/src/endpoint.rs:
- Outgoing connections: Lines 1096 and 1195 call
EndpointHooksList::before_connectbefore the handshake begins - All connections: Line 2597 calls
EndpointHooksList::after_handshakeimmediately after TLS completion
If any hook returns a rejection outcome, the connection aborts immediately without further processing.
Handling Outcomes
The outcome enums in iroh/src/endpoint/hooks.rs provide semantic control:
BeforeConnectOutcomeis binary—accept or reject silentlyAfterHandshakeOutcome::rejectlets you specify a custom error code and reason bytes for debugging or protocol compliance
Designing Effective Hooks
Avoiding Memory Leaks
Do not store an Endpoint reference inside your hook. The hook is reference-counted by the endpoint; keeping a strong reference creates a cycle that leaks memory. The source code explicitly warns about this in the "Notes to implementers" comment.
If you need to persist a connection reference after the hook returns, use Connection::weak_handle. This weak reference does not keep the connection alive but allows later inspection.
Performance Considerations
Hook callbacks run synchronously on the connection's task. Heavy computation blocks the async runtime:
- Offload CPU-intensive work to background tasks via channels
- Keep database lookups and cryptographic operations minimal
- Return outcomes quickly to prevent handshake timeouts
Implementation Examples
Blocking Specific Endpoint IDs
This minimal hook implements before_connect to blocklist specific remote IDs before any network traffic occurs:
use iroh::{
endpoint::{EndpointHooks, BeforeConnectOutcome},
EndpointAddr,
};
#[derive(Debug)]
struct BlocklistHook {
blocked: iroh::EndpointId,
}
impl EndpointHooks for BlocklistHook {
async fn before_connect<'a>(
&'a self,
remote_addr: &'a EndpointAddr,
_alpn: &'a [u8],
) -> BeforeConnectOutcome {
if remote_addr.id == self.blocked {
BeforeConnectOutcome::Reject
} else {
BeforeConnectOutcome::Accept
}
}
}
Installation:
let blocked = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".parse().unwrap();
let hook = BlocklistHook { blocked };
let endpoint = iroh::Endpoint::builder(presets::N0)
.hooks(hook)
.bind()
.await?;
Logging Successful Handshakes
This after_handshake hook logs every connection after TLS completes:
use iroh::{
endpoint::{EndpointHooks, AfterHandshakeOutcome},
endpoint::Connection,
};
use tracing::info;
#[derive(Debug)]
struct LoggingHook;
impl EndpointHooks for LoggingHook {
async fn after_handshake<'a>(
&'a self,
conn: &'a Connection,
) -> AfterHandshakeOutcome {
info!(
remote = %conn.remote_id().fmt_short(),
alpn = %std::str::from_utf8(conn.alpn()).unwrap_or("<invalid>"),
"handshake completed"
);
AfterHandshakeOutcome::Accept
}
}
Recording Remote Statistics
The RemoteMapHook from iroh/examples/remote-info.rs demonstrates storing weak handles to connections for later aggregation:
use iroh::{
endpoint::{EndpointHooks, AfterHandshakeOutcome},
endpoint::remote_map::{RemoteMap, RemoteMapHook},
};
let (hook, remote_map) = RemoteMap::new();
let endpoint = iroh::Endpoint::builder(presets::N0)
.hooks(hook) // Registers the RemoteMapHook (after_handshake only)
.bind()
.await?;
This pattern allows querying live connections and computing RTT statistics without keeping connections alive indefinitely.
Authenticating Connections
The complete example in iroh/examples/auth-hook.rs shows both callbacks working together. The outgoing side uses before_connect to verify pre-authentication:
impl EndpointHooks for OutgoingAuthHook {
async fn before_connect<'a>(
&'a self,
remote_addr: &'a EndpointAddr,
alpn: &'a [u8],
) -> BeforeConnectOutcome {
if alpn == auth::ALPN {
return BeforeConnectOutcome::Accept; // Don't block auth protocol
}
match self.authenticate(remote_addr.id).await {
Ok(()) => BeforeConnectOutcome::Accept,
Err(_) => BeforeConnectOutcome::Reject,
}
}
}
The incoming side uses after_handshake to enforce authorization:
impl EndpointHooks for IncomingAuthHook {
async fn after_handshake<'a>(
&'a self,
conn: &'a Connection,
) -> AfterHandshakeOutcome {
if conn.alpn() == auth::ALPN
|| self.allowed_remotes.lock().unwrap().contains(&conn.remote_id())
{
AfterHandshakeOutcome::Accept
} else {
AfterHandshakeOutcome::Reject {
error_code: 403u32.into(),
reason: b"not authenticated".to_vec(),
}
}
}
}
Key Source Files
These files contain the reference implementations:
iroh/src/endpoint/hooks.rs– DefinesEndpointHooks,BeforeConnectOutcome,AfterHandshakeOutcome, andEndpointHooksListiroh/src/endpoint.rs– ContainsBuilder::hooksregistration and invocation points at lines 1096, 1195, and 2597iroh/examples/remote-info.rs– Completeafter_handshakeexample with path statisticsiroh/examples/auth-hook.rs– Full demonstration of both callbacks with authentication logiciroh/src/protocol.rs– Shows protocol-specific hook integration
Summary
- Implement
EndpointHooksto intercept connections in iroh using two async callbacks:before_connectfor outgoing connections andafter_handshakefor all connections after TLS - Register hooks via
Endpoint::builder().hooks(hook)before callingbind(); multiple hooks execute in registration order - Return outcomes to control flow:
BeforeConnectOutcomeaccepts or rejects silently, whileAfterHandshakeOutcomecan reject with custom QUIC error codes - Avoid cycles by never storing the
Endpointinside your hook; useConnection::weak_handlefor persistent references - Keep callbacks fast to prevent blocking the connection task; offload heavy work to background threads
Frequently Asked Questions
What is the difference between before_connect and after_handshake in iroh?
The before_connect hook runs only for outgoing connections immediately before the handshake begins, allowing you to prevent connection attempts based on address or ALPN without network traffic. The after_handshake hook runs after TLS completes for both incoming and outgoing connections, when the remote identity is verified and the connection is technically established but not yet accepted for application use.
Can I install multiple hooks on a single iroh Endpoint?
Yes. The Builder::hooks method appends hooks to an internal EndpointHooksList vector. When events fire, iroh invokes each hook in the order they were registered. If any hook returns a rejection outcome, the connection aborts immediately and subsequent hooks are not called for that specific event.
How do I reject a connection with a custom error code in iroh?
Use AfterHandshakeOutcome::Reject { error_code, reason } in your after_handshake implementation. The error code must be a VarInt (typically a u32 converted via .into()), and the reason is a Vec<u8> containing UTF-8 text. This sends a QUIC CONNECTION_CLOSE frame with your specified code to the remote peer.
What happens if a hook panics during execution?
The hook callbacks run synchronously on the connection task. If a hook panics, it will poison the task and likely cause the endpoint to stop accepting or establishing connections. You should design hooks to be infallible by catching errors internally and returning Accept with logged warnings rather than unwinding the stack.
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 →