# How to Monitor Connection Events and Path Changes Using iroh's Hooks System

> Learn how to monitor connection events and path changes with irohs hooks system. Implement the EndpointHooks trait to intercept lifecycle events and metrics during endpoint construction.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-19

---

**iroh provides an asynchronous hook system that lets you intercept connection lifecycle events and path metrics by implementing the `EndpointHooks` trait and registering it during endpoint construction.**

The iroh networking library exposes a lightweight hook mechanism for observing QUIC connection lifecycle events without blocking the async runtime. By implementing the `EndpointHooks` trait defined in [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs), you can capture handshake details, monitor path characteristics like RTT and MTU, and log final connection statistics when connections close.

## How the Endpoint Hooks System Works

The hook system centers around the **`EndpointHooks`** trait, which acts as a callback interface for connection lifecycle events. When you build an `Endpoint` using `Endpoint::builder`, the system stores your hooks in an `EndpointHooksList` (a `Vec<Box<dyn DynEndpointHooks>>`) that the endpoint consults during connection state transitions.

Hooks are invoked at three critical lifecycle points defined in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs):

- **`before_connect`** – Called before a connection is accepted or initiated, allowing you to reject incoming connections early.
- **`after_handshake`** – Called immediately after the QUIC handshake completes, providing access to the negotiated ALPN and remote endpoint ID.
- **`after_close`** – Called when the connection terminates, offering final statistics and closure reasons.

Each method returns an outcome enum (`Accept` or `Reject`) that determines whether the connection proceeds. This design guarantees that **hooks run synchronously** during state transitions while still allowing asynchronous monitoring through weak handles.

## Capturing Connections with Weak Handles

The key to non-blocking monitoring is the **`WeakConnectionHandle`** obtained via `conn.weak_handle()` inside the `after_handshake` method. This handle is cheap to clone and store because it does not prevent the connection from closing, avoiding reference-count cycles that could leak memory.

To monitor path metrics asynchronously:

1. Store the `WeakConnectionHandle` in a background task.
2. Call `handle.upgrade()` to temporarily obtain a `Connection` reference.
3. Access `paths()` to iterate over active paths and read RTT, MTU, and other socket metrics.
4. Await `handle.closed()` to receive final statistics (bytes sent/received) when the connection ends.

Because `upgrade()` returns `None` if the connection has already closed, this pattern is safe for long-running monitoring tasks without risking use-after-close errors.

## Implementing the EndpointHooks Trait

To create a custom monitor, implement the `EndpointHooks` trait and handle the `after_handshake` method. The trait uses async methods that receive a `&Connection` reference, allowing you to extract static metadata (ALPN protocol, remote endpoint ID) before spawning background tasks.

The implementation must avoid storing the `Endpoint` itself to prevent reference cycles, as noted in the safety comments within [`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs). Instead, channel senders or other lightweight synchronization primitives should ferry data to your monitoring infrastructure.

## Complete Working Example

Below is a self-contained implementation that mirrors the official [`monitor-connections.rs`](https://github.com/n0-computer/iroh/blob/main/monitor-connections.rs) example. It demonstrates installing a hook, capturing connection metadata, and asynchronously logging path RTT and closure statistics.

```rust
use std::{sync::Arc, time::Duration};
use iroh::{
    Endpoint,
    endpoint::{AfterHandshakeOutcome, Connection, EndpointHooks, WeakConnectionHandle, presets},
};
use tokio::{
    sync::mpsc::{UnboundedReceiver, UnboundedSender},
    task::JoinSet,
};
use tracing::{info, info_span, Instrument};

/// Hook implementation that forwards connection info to a background task.
#[derive(Clone, Debug)]
struct ConnectionMonitor {
    tx: UnboundedSender<MonitoredConnection>,
    _task: Arc<tokio::task::JoinHandle<()>>,
}

#[derive(Debug)]
struct MonitoredConnection {
    alpn: Vec<u8>,
    remote_id: iroh::EndpointId,
    handle: WeakConnectionHandle,
}

impl EndpointHooks for ConnectionMonitor {
    async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome {
        let info = MonitoredConnection {
            alpn: conn.alpn().to_vec(),
            remote_id: conn.remote_id(),
            handle: conn.weak_handle(),
        };
        // Forward to background logger; failure is non-fatal.
        let _ = self.tx.send(info);
        AfterHandshakeOutcome::Accept
    }
}

impl ConnectionMonitor {
    fn new() -> Self {
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
        let task = tokio::spawn(Self::run(rx).instrument(info_span!("monitor")));
        Self {
            tx,
            _task: Arc::new(task),
        }
    }

    async fn run(mut rx: UnboundedReceiver<MonitoredConnection>) {
        let mut tasks = JoinSet::new();
        while let Some(MonitoredConnection { alpn, remote_id, handle }) = rx.recv().await {
            let alpn_str = String::from_utf8_lossy(&alpn).to_string();
            let remote_short = remote_id.fmt_short();

            info!(%remote_short, %alpn_str, "new connection established");

            // Spawn task to await closure and log final stats.
            tasks.spawn(async move {
                match handle.closed().await {
                    Some(closed) => {
                        // Access path metrics if connection still exists.
                        let rtt = handle.upgrade()
                            .and_then(|c| c.paths().iter().map(|p| p.rtt()).min());
                        
                        info!(%remote_short, %alpn_str,
                              rtt = ?rtt,
                              udp_rx = closed.stats.udp_rx.bytes,
                              udp_tx = closed.stats.udp_tx.bytes,
                              "connection closed");
                    }
                    None => {
                        info!(%remote_short, %alpn_str, "connection closed before tracking");
                    }
                }
            }.instrument(tracing::Span::current()));
        }

        while let Some(res) = tasks.join_next().await {
            res.expect("connection monitor task panicked");
        }
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt().init();
    
    let monitor = ConnectionMonitor::new();
    
    let endpoint = Endpoint::builder(presets::Minimal)
        .hooks(monitor.clone())
        .bind()
        .await?;
    
    // Server logic continues here...
    Ok(())
}

```

## Key Source Files

The hook system spans several critical files in the iroh repository:

- **[`iroh/src/endpoint/hooks.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/hooks.rs)** – Defines the `EndpointHooks` trait, `EndpointHooksList`, and outcome enums (`AfterHandshakeOutcome`, `BeforeConnectOutcome`).
- **[`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs)** – Contains the invocation points where the endpoint calls hook methods during connection establishment and teardown.
- **[`iroh/examples/monitor-connections.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/monitor-connections.rs)** – Official demonstration showing production-ready patterns for connection monitoring and path observation.
- **[`iroh/src/socket/metrics.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/metrics.rs)** – Provides the `Path` structure and RTT accessors used when iterating over `conn.paths()`.

## Summary

- **Register hooks** during endpoint construction using `Endpoint::builder(...).hooks(my_hook)` to intercept connection events.
- **Implement `EndpointHooks`** to receive callbacks at `before_connect`, `after_handshake`, and `after_close` lifecycle points.
- **Use `WeakConnectionHandle`** to monitor connections asynchronously without preventing garbage collection or creating reference cycles.
- **Access path metrics** via `handle.upgrade()?.paths()` to read RTT, MTU, and socket statistics, and await `handle.closed()` for final byte counters.
- **Return outcomes** (`Accept` or `Reject`) from hook methods to programmatically filter connections based on custom logic.

## Frequently Asked Questions

### What is the difference between `before_connect` and `after_handshake` hooks?

The `before_connect` hook fires before any network handshake occurs, allowing you to reject connections based on IP address or initial metadata without allocating connection resources. The `after_handshake` hook fires after the QUIC cryptographic handshake completes, giving you access to the negotiated ALPN protocol and authenticated remote endpoint ID. According to the source code in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs), `before_connect` is ideal for early filtering, while `after_handshake` is where you capture the `WeakConnectionHandle` for monitoring.

### How do I access real-time path metrics without blocking the endpoint?

Capture a `WeakConnectionHandle` inside `after_handshake` using `conn.weak_handle()`, then spawn a background task that periodically calls `handle.upgrade()`. If `upgrade()` returns `Some(conn)`, you can iterate over `conn.paths()` to read RTT, MTU, and congestion statistics. This pattern avoids keeping a strong reference to the connection, ensuring the endpoint can close connections naturally while your monitor observes metrics asynchronously.

### Can I reject connections based on custom logic in the hooks?

Yes. Both `before_connect` and `after_handshake` return an outcome enum that controls connection acceptance. Return `BeforeConnectOutcome::Reject` or `AfterHandshakeOutcome::Reject` to terminate the connection immediately. The endpoint respects these outcomes synchronously, preventing the connection from proceeding further into the QUIC stack.

### Where can I find the official example demonstrating this pattern?

The repository includes [`iroh/examples/monitor-connections.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/monitor-connections.rs), which demonstrates a complete implementation of the `EndpointHooks` trait that logs connection events, tracks path RTT minima, and reports final UDP byte counts upon connection closure. This example serves as the reference implementation for production monitoring systems built on iroh.