Files
Aleksandr Bogomiakov 2166ba4800 Separate internal WireGuard encryption from data carriers
AWG-only policy also selected a competing end-to-end plugin, so compatible
external links could still have no compatible encrypted data channel. Carrier
changes replaced tunnel state and status selected counters from different
engines. Use one internal WireGuard plugin and a separate DataCarrier contract
for external capabilities/lifecycle. Keep the logical link, keys, handshake,
interface and counter epoch across policy, fallback and direct/relay changes.
Control/discovery transport selection and membership remain independent.

Share the bounded per-neighbour UDP WireGuard engine between ordinary WG and
Amnezia masking. Encrypt the relay envelope inside the outer hop channel with
fresh pair keys/PSK. Remove duplicate WG key/device code and the TCP/TLS inner
cipher. TLS carries opaque internal WireGuard frames. Refuse missing native
transports instead of letting default iroh impersonate their protocol names.
Probe the actual authenticated carrier, independently of the shared IP tunnel;
failed local probe submissions count as loss too.

Apply the vendored iroh interface exclusion to actual direct candidates, not
just public discovery records. Exclude overlay subnets from introductions and
propagate the actual owned interface name, including kernel-assigned utun names,
to every carrier. Production logs showed paths through the overlay itself.
This removes a recursive transport path; it does not establish the historical
cause of the reported 100 GB absolute counter. Exit-node traffic is also real
peer IP traffic even when local applications are idle.

Hide inner cipher choices/handshake clutter in CLI and tray. Report external
routes, readiness and actionable policy/version failures. Count original IP
packets only when their ciphertext is accepted by the logical transport,
including queued packets' exact unpadded lengths. Exclude physical probes and
keepalives. Export counter epochs; sum per-peer rate deltas and discard stale
histories so joins/restarts cannot turn old totals into fresh traffic. Status
also prints exact IP byte totals for comparison with the tray.

Coordinated upgrade: data ALPN 6, internal wireguard-e2e capability v4,
wg/awg v2, QUIC carrier v1, TLS v2, local IPC v23. Control ALPN 8, device/network
identity derivation, signed records and saved network choices stay unchanged.
Keep the historical wg-quic key store; transactionally import missing keys from
wg then awg without replacing canonical keys. Forget removes legacy copies too,
on the blocking pool, to prevent resurrection after restart. Preserve historic
cipher options as shared settings, rejecting contradictory values.

Validation on Linux in the Nix development shell:
- cargo fmt --all -- --check
- cargo clippy --locked --workspace --all-targets -- -D warnings
- cargo test --locked --workspace --all-targets (562 passed, 3 ignored)
The inherited service socket/secret/group environment overrides were removed
for isolated CLI process tests. A new carrier test initially raced the first
peer announcement; its deadline polling now waits for a peer row explicitly.
Real loopback endpoints, membership, SQLite, WG/AWG/TLS encryption and memory
TUNs cover mixed-carrier relaying and one smoltcp TCP connection surviving
relay/direct/policy changes and a dropped transmission. Counter tests check
exact plaintext lengths, refused sends, epochs, joins and stale histories.
Public DHT, privileged kernel and manual benchmark tests remain ignored;
macOS/Windows native TUNs, desktop GUI and WAN/DPI behavior were not run.
2026-10-22 01:07:52 +01:00
..

iroh

less net work for networks

Documentation Crates.io Chat Youtube License: MIT License: Apache 2.0


Iroh is a Rust library to establish direct connections between endpoints. It gives you an API for dialing by public key. You say "connect to that endpoint", and iroh finds and maintains the best connection for you.

Under the hood iroh establishes peer-to-peer QUIC connections between endpoints. The fastest route is a direct connection, so iroh tries to hole-punch one whenever it can. If that fails it falls back to using relay servers.

Because iroh is built on QUIC, all connections are end-to-end encrypted and may carry any number of concurrent streams. Dialing by public key also makes them mutually authenticated, because each endpoint's public key is its TLS identity.

Overview

An iroh endpoint is created and controlled by the Endpoint. Each endpoint has a unique SecretKey, whose public key is the endpoint's identity, the EndpointId. Connections are authenticated against this key, which means an EndpointId can't be impersonated.

A connection is usually established with the help of a relay server. When an endpoint is created it connects to the closest relay and designates it as its home relay. Other endpoints reach it first through this relay, then both sides use QUIC NAT traversal to establish a direct connection. In the rare cases where a direct connection is not possible, traffic keeps flowing over the relay.

Relay servers only forward encrypted packets addressed to Endpoint IDs, they cannot read any traffic between endpoints.

Endpoints can also connect directly without a relay, as long as the accepting endpoint is directly reachable at one of its addresses.

To discover addressing information for an endpoint, iroh uses address lookup services. With address lookup, you can connect to other endpoints with only their EndpointId. Addressing information will then be resolved on-demand.

The N0 preset installs the DNS/Pkarr address lookup service, which uses servers hosted by n0 to provide global lookup for endpoints.

Example

This is an echo protocol: the accepting side copies back whatever it receives. The full, commented version is in echo.rs.

use iroh::{
    Endpoint,
    endpoint::{Connection, presets},
    protocol::{AcceptError, ProtocolHandler, Router},
};

/// Each protocol is identified by its ALPN, exchanged during the handshake.
const ALPN: &[u8] = b"iroh-example/echo/0";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // The accepting side: bind an endpoint and route the ALPN to a handler.
    let endpoint = Endpoint::bind(presets::N0).await?;
    let router = Router::builder(endpoint.clone()).accept(ALPN, Echo).spawn();
    endpoint.online().await;
    // Get the endpoint's address so that we can connect to it.
    let addr = endpoint.addr();

    // The connecting side: dial the accepting endpoint by its address.
    {
        let other_endpoint = Endpoint::bind(presets::N0).await?;

        let conn = other_endpoint.connect(addr, ALPN).await?;
        let (mut send, mut recv) = conn.open_bi().await?;
        send.write_all(b"Hello, world!").await?;
        send.finish()?;
        let response = recv.read_to_end(1000).await?;
        assert_eq!(&response, b"Hello, world!");
        conn.close(0u32.into(), b"bye!");

        other_endpoint.close().await;
    }

    router.shutdown().await?;
    Ok(())
}

#[derive(Debug, Clone)]
struct Echo;

impl ProtocolHandler for Echo {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        let (mut send, mut recv) = connection.accept_bi().await?;
        // Echo bytes back until the sender signals the end of data.
        tokio::io::copy(&mut recv, &mut send).await?;
        send.finish()?;
        // Wait until the other endpoint closes the connection.
        connection.closed().await;
        Ok(())
    }
}

More examples live in iroh/examples. Run them with cargo run --example NAME. Details for each are in the file itself.

Compose protocols

Instead of writing your own, you can build on protocols that already exist on top of iroh:

  • iroh-blobs for BLAKE3-based content-addressed blob transfer, scaling from kilobytes to terabytes.
  • iroh-gossip for publish-subscribe overlay networks that scale down to what an average phone can handle.
  • and many more.

To use iroh from other languages, see iroh-ffi.

Development

For notes on iroh's structured events and how to build the documentation, see DEVELOPMENT.md.

License

This project is licensed under either of

at your option.

Contribution

See CONTRIBUTING.md for how to get involved.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.