Raw capture โ raw/tunnet.md
url: https://github.com/tunnetio/Tunnet title: Tunnet โ Open-source mesh VPN, serve, tunnel, send, and SSH platform author: tunnetio date_fetched: 2026-07-18 date_published: 2025
Tunnet is an open-source platform (AGPL-3.0) that connects machines into a private mesh network with an integrated suite of networking primitives: mesh VPN, internal service exposure (Serve), public tunneling (Tunnel), P2P file transfer (Send), and identity-based SSH. It operates in two modes โ Managed (control plane with dashboard, SSO, policies) and Direct (pure P2P with CRDT membership, no server needed) โ and supports migration between them.
Written in Rust with a TypeScript management UI, the project is ~26K lines of Rust across six crates plus TypeScript packages for the dashboard, management API, and SDK.
crates/
tunnet-common/ โ shared types: policy engine, wire protocol, IPv6 helpers, relay control, recording format
tunnet-core/ โ core library: node bootstrap, routing table, ACL engine, connection pool, tunnel/serve/send managers, Direct mode membership, stream protocol
tunnet-agent/ โ CLI + system service: TUN data plane, SSH server, DNS, firewall, service management
tunnet-control/ โ control plane server: HTTP API (axum), PostgreSQL, policy store, SSH auth, HA, enrollment
tunnet-relay/ โ public edge relay for tunnels: HTTPS termination, ACME, reverse-tunnel agent acceptor
tunnet-node-napi/ โ Node.js native addon (napi-rs) for the @tunnet/sdk npm package
apps/
dashboard/ โ React admin dashboard (Vite)
management/ โ management API server (Bun)
docs/ โ documentation site (VitePress)
Everything runs over QUIC via the iroh library. Each node has an Ed25519 keypair; the endpoint ID is the hex-encoded verifying key (AgentIdentity::endpoint_id_hex() in crates/tunnet-core/src/identity.rs). The endpoint binds with per-feature ALPNs:
tunnet/stream/1 โ general-purpose stream multiplexing (proxy, SSH proxy, SDK connections)tunnet/dgram/1 โ datagram tunnel for mesh IP packetstunnet/relay/1 โ relay control channeltunnet/direct-auth/2 โ Direct mode PSK challenge-responsetunnet/recording/1 โ SSH session recording casttunnet/send-blobs/1 โ P2P file transfer offersiroh-docs and iroh-gossip โ Direct mode membership CRDTcrates/tunnet-core/src/node.rs)CoreNode::bootstrap() dispatches to bootstrap_managed() or bootstrap_direct() based on PersistedState:
Managed mode (bootstrap_managed):
1. Signs and sends a register request to the control plane with hostname, version, and metadata
2. Receives an EndpointSnapshot containing all memberships, policies, and routes for the org
3. Builds the routing table from the snapshot via apply_membership()
4. Opens a WebSocket to the control plane for real-time updates (snapshots, deltas, policy changes, open-tunnel commands)
5. Falls back to a cached snapshot on register failure (offline bootstrap)
6. Also runs a polling fallback loop as a backup sync mechanism
Direct mode (bootstrap_direct):
1. Loads all joined Direct networks from persisted state
2. For each network, derives an IPv4 address from the endpoint ID (FNV-1a hash in 100.64.0.0/10 CGNAT space) with a collision counter for birthday collisions
3. Bootstraps an iroh-docs document per network โ the membership CRDT
4. Each doc has keys like peers/<endpoint_id>/hostname, peers/<endpoint_id>/ip, peers/<endpoint_id>/tags
5. Changes to the docs document replicate via iroh-gossip; a Direct sync loop watches for membership changes and rebuilds the routing table
6. PSK transport auth: before any app ALPN is accepted, peers must complete an HMAC challenge-response over tunnet/direct-auth/2, proving knowledge of that specific network's secret
7. Discovery: peers are seeded from invite coordinator + doc membership, with optional Mainline DHT announce for topic liveness
crates/tunnet-core/src/routing.rs)The RoutingTable is the central lookup structure. It uses Arc<ArcSwap<Tables>> for lock-free reads โ the entire table is atomically swapped on updates. The Tables struct holds:
by_ip: HashMap<Ipv4Addr, Arc<PeerInfo>> โ primary mesh IP โ peer lookupby_network_ip: HashMap<(Uuid, Ipv4Addr), Arc<PeerInfo>> โ per-network IP lookup (for multi-network Direct mode)by_endpoint: HashMap<String, Arc<PeerInfo>> โ endpoint hex โ peerby_hostname: HashMap<String, Arc<PeerInfo>> โ hostname โ peer (for PeerDNS)subnets: Vec<(Ipv4Net, Arc<PeerInfo>)> โ subnet routes, sorted by prefix length descending (longest-prefix-match)hostname_exact/hostname_wildcards โ hostname route resolution (Serve-like features)exit_node: Option<Arc<PeerInfo>> โ selected exit nodeThe lookup chain for lookup_ip() is: direct peer โ subnet LPM โ exit node. For each Direct network, replace_network() sorts by join_index and first-joined wins on IP collisions. The synthetic_ip_for() function uses FNV-1a hash of the hostname to produce a stable IP in 100.100.0.0/16 for wildcard hostname route DNS resolution.
crates/tunnet-core/src/iroh_pool.rs)ConnPool wraps iroh connection management with an on-demand policy:
keep_alive = true โ connections stay openkeep_alive = false โ idle connections auto-close after 120s and reconnect when traffic resumesEach peer has a PeerSlot with states Connected โ Suspended โ Reconnecting. When a packet arrives for a suspended peer, the pool buffers it (up to 64 packets / 1MB), initiates reconnect, and flushes on success. Metrics track reconnect attempts, success/failure, latency, and dropped packets. An idle sweeper runs every 10s to close connections idle past the timeout.
crates/tunnet-agent/src/dataplane.rs, tun_io.rs)The data plane is hot-swappable via an IPC command channel (DataPlaneHandle). bring_up():
1. Creates a TUN device via tun-rs
2. Assigns the mesh IPv4 address
3. Configures system DNS to point at the PeerDNS magic IP (100.100.100.53)
4. Installs system routes for remote subnets
5. Spawns the outbound loop: reads packets from TUN, looks up destination in the routing table, applies ACL checks, and sends via iroh datagrams
6. For Direct mode, applies per-network firewall rules
bring_down() tears everything down: aborts the outbound task, unapplies system routes, restores DNS, drops the TUN device.
crates/tunnet-core/src/acl.rs, tunnet-common/src/policy.rs)The AclEngine evaluates policy rules against a connection context (EvalCtx):
- self_endpoint_hex, self_ip, self_tags, self_network
- peer_endpoint_hex, peer_ip, peer_tags, peer_network
- dst_port, protocol (TCP/UDP/ICMP/Any)
Rules use selectors (Any, Endpoint, Tag, Network, Cidr) with priorities and port ranges. The default is deny; when the ACL bundle is stale (poll failed) and has no explicit rules, it falls back to allow โ a deliberate fail-open for connectivity.
For Managed mode, the policy bundle is signed by the control plane's Ed25519 key and delivered via WebSocket. For Direct mode, the firewall is user-configured and synced through the iroh-docs membership document. The FirewallEngine (crates/tunnet-core/src/direct/firewall.rs) adds stateful connection tracking: outbound connections automatically allow return traffic via a conntrack table with protocol-specific timeouts (TCP 300s active, 10s TIME_WAIT; UDP 30s; ICMP 10s).
crates/tunnet-core/src/tunnel.rs)The tunnel feature connects to a relay over iroh via RELAY_ALPN, sends a RelayCtrl::Register message with tunnel ID/subdomain/auth token, then maintains a control channel with keepalive pings every 20s. Incoming relay-forwarded streams are proxied to localhost. For HTTPS tunnels, the manager parses the HTTP request path to match against redirect rules (path-based routing to different local ports). For TCP tunnels, it reads a Forward header with target port and optional target IP.
crates/tunnet-core/src/serve.rs)Serve binds a TLS or TCP listener on the node's mesh IP. From bootstrap_managed(), the Serve manager receives start/stop commands via WebSocket from the control plane. It supports three ACL modes: all_peers (open), machines (specific endpoint IDs), and tags (tag-based). Peer identity is reverse-resolved from the connecting IP through the routing table, and join/leave events with byte counters are reported to the control plane via the WebSocket channel.
crates/tunnet-agent/src/ssh/)The SSH server runs on the mesh IP using the russh crate. Key features:
- Identity-based: no key distribution โ auth is tied to Tunnet identity and organization SSH policies
- Port NAT: ssh_nat.rs rewrites inbound connections from the external SSH port (typically 22 on mesh) to an internal listen port, and rewrites outbound replies in the TUN loop
- Session recording: via tee.rs, sessions are recorded to cast files and uploaded to the control plane
- Session registry: SshSessionRegistry tracks active sessions with kill capability โ the control plane can request session termination via KillSshSession WebSocket message
- Re-auth: SSH policies support action: check with configurable re-auth periods
- SFTP: built-in via russh-sftp
crates/tunnet-core/src/send.rs)Uses iroh-blobs for content-addressed file transfer. Files are chunked and verified with BLAKE3. Transfer offers are sent over a dedicated ALPN (SEND_ALPN). The consent model supports auto_accept, prompt, and deny modes. Files arrive in a configurable inbox directory. Transfers can target specific peers or tagged groups.
crates/tunnet-control/)The control plane is an axum HTTP server with PostgreSQL:
- /v1/enroll, /v1/register, /v1/poll โ device lifecycle
- /v1/ws โ WebSocket for real-time sync (snapshots, deltas, commands)
- /v1/tunnels โ tunnel creation and lifecycle
- /v1/ssh-auth/* โ SSH auth evaluation and verification
- /v1/ssh-recordings โ session recording upload and playback
- /v1/relay/* โ relay registration and heartbeat
- Admin API on separate port for internal management
Background tasks: Postgres LISTEN/NOTIFY for cross-instance state sync, stale device eviction (60s), presence sweeping (30s), tunnel TTL expiry (30s), device auto-cleanup (60s). High-availability is supported via ha.rs with cross-instance notification.
crates/tunnet-relay/)The relay is a standalone binary that:
1. Binds an iroh endpoint with RELAY_ALPN
2. Runs an HTTPS server (rustls) on 0.0.0.0:443 with optional ACME
3. Accepts agent reverse-tunnel connections and registers them in TunnelRegistry
4. Forwards incoming HTTPS connections to the correct agent by subdomain/sni lookup
5. Reports traffic stats and heartbeats to the control plane
6. Supports TCP port mapping for non-HTTP tunnels
The control plane holds a policy signing key (Ed25519). All PolicyBundle objects carry a signature. Agents verify this signature before applying policy changes, ensuring agents only accept policy from their own control plane. The ServiceAuth mechanism uses a shared secret for internal service-to-service authentication.
The routing table uses Arc<ArcSwap<Tables>> โ on the hot path (packet forwarding), reads are lock-free atomic pointer swaps. Rebuilds lock a mutex-protected BTreeMap<Uuid, NetworkSlice>, construct a new Tables struct, then atomically swap. DNS resolution creates synthetic IPs in a concurrent DashMap to avoid blocking the main table rebuild.
Agent secrets (identity key, network PSKs, doc tickets) are stored in state.enc with an accompanying state.enc.meta file. The SealTier enum (None, Machine, User) determines the protection level. On macOS, Machine tier uses the Keychain; on Linux it can use the TPM or a machine key. The AgentSecrets struct holds the decrypted material in memory; PersistedState has #[serde(skip)] on secret fields so they never serialize to the plaintext state.json.
Direct mode uses iroh-docs (a CRDT document store) as its membership database โ not a custom protocol. Each network is one document with keyspaced entries (peers/<endpoint_id>/hostname, meta/name, etc.). Changes replicate via iroh-gossip, and a sync loop watches LiveEvent streams from the docs engine to detect membership changes. This means Direct mode gets eventual consistency and offline operation for free from the CRDT layer.
Direct mode PSK auth (crates/tunnet-core/src/direct/auth.rs) doesn't just verify knowledge of the secret โ it binds the claimed network_id into the HMAC proof so the server only checks against that specific network's secret, avoiding a try-all-secrets vulnerability. The AuthCache tracks authenticated peers per network, and the DirectAuthHook (iroh endpoint hook) blocks non-auth ALPNs until the peer is authenticated for at least one joined network.
The Direct mode firewall (crates/tunnet-core/src/direct/firewall.rs) distinguishes between Deny (silent drop) and Reject (TCP RST / ICMP unreachable). Silent drops cause timeouts; rejects give immediate feedback to applications. The conntrack table uses a DashMap for concurrent access with a garbage collection sweep every 10s.
Managed mode uses a persistent WebSocket for real-time updates, but also runs a polling loop (spawn_poll_fallback) at a configurable interval (default 30s) as a backup. This dual-path sync means the agent stays current even if the WebSocket disconnects and reconnects. The poll fallback marks the ACL as "stale" on failure, switching to fail-open to preserve connectivity.
When a Direct mode connection is suspended and a packet arrives, ConnPool::send_or_buffer() queues the packet (up to 64 packets / 1MB per peer), initiates reconnection, and flushes on success. If the buffer fills, packets are dropped (with metrics). This means Direct mode peers can appear "always connected" to applications even though connections are on-demand underneath.
ssh_nat::rewrite_outbound() runs on every outbound packet in the TUN loop, rewriting source ports for SSH connections. This is a deliberately minimal check โ it only touches packets from the internal SSH port โ so the overhead on non-SSH traffic is a single comparison.
Tunnet bundles six major features (mesh, serve, tunnel, send, SSH, relay) into a single codebase. The upside: everything shares one identity system, one policy engine, and one routing table. The downside: ~26K lines of Rust with deep cross-crate coupling. CoreNode holds references to everything (ServeManager, TunnelManager, SendManager, ConnPool, RoutingTable, AclEngine), making it the god object of the system.
The project is heavily dependent on the iroh ecosystem (iroh, iroh-gossip, iroh-blobs, iroh-docs). This gives QUIC transport, NAT traversal, content-addressed blobs, and CRDT membership for free. The cost is tight coupling to iroh's API and release cycle, and the complexity of managing iroh endpoint hooks and ALPN negotiation correctly.
Supporting both Managed and Direct mode means every subsystem (routing, ACL, connection management, membership) has two code paths. The CoreNode fields express this: signed: Option<SignedClient> (Managed only), direct_auth: Option<AuthCache> (Direct only), direct: HashMap<Uuid, DirectNetworkRuntime> (empty in Managed). This is cleaner than a trait-based abstraction but means every consumer must handle both cases.
When the control plane is unreachable and the ACL bundle has no explicit rules, the agent fails open (allows all traffic). This prioritizes connectivity over security โ a deliberate choice that mirrors how Tailscale and similar tools handle control plane outages. The stale flag in AclEngine tracks this state explicitly.
Direct mode defaults to on-demand connections (idle peers are disconnected and reconnected when traffic flows). This saves resources for personal/small-group use but means the first packet to a sleeping peer incurs reconnection latency (~5s timeout). Managed mode defaults to keep-alive, appropriate for organizational deployments where connectivity is critical.
| Technique | Where | Why |
|---|---|---|
Arc<ArcSwap<T>> for routing |
routing.rs |
Lock-free reads on the hot packet-forwarding path |
| iroh-docs CRDT for membership | direct/membership.rs |
Free eventual consistency, offline operation, gossip replication |
| HMAC challenge-response with network_id binding | direct/auth.rs |
PSK auth without try-all-secrets vulnerability |
| Conntrack firewall with reject/drop distinction | direct/firewall.rs |
Stateful L4 filtering with immediate feedback on blocked connections |
| WebSocket + poll fallback dual sync | sync.rs, ws_client.rs |
Real-time updates with offline resilience |
| Packet buffering during reconnect | iroh_pool.rs |
On-demand connections that feel always-on to applications |
| Tiered secret sealing (Keychain/TPM/plaintext) | secret_store.rs |
Defense in depth for key material, platform-appropriate |
| FNV-1a hash for synthetic DNS IPs | routing.rs |
Stable, deterministic IP assignment for hostname routes without coordination |
| ALPN-based feature negotiation | node.rs |
Each feature (stream, datagram, relay, auth) gets its own protocol lane over a single QUIC connection |
| Signed policy bundles | policy.rs, signing_key.rs |
Agents only accept policy from their own control plane |
| Ed25519 identity = endpoint ID | identity.rs |
Self-certifying: the public key IS the node identifier, no CA needed |
vs. Tailscale: Tailscale uses WireGuard for the data plane with a coordination server for key distribution and ACLs. Tunnet uses QUIC (iroh) instead of WireGuard, bundles more features (tunnel, serve, send, SSH) into one agent, and offers a pure P2P mode with no server dependency. Tailscale's control plane is closed-source; Tunnet's is fully open.
vs. NetBird: NetBird also uses WireGuard + a management server. Tunnet's Direct mode (CRDT membership via iroh-docs) has no equivalent in NetBird. Tunnet's relay/tunnel system is more feature-rich (path-based redirects, TCP port mapping, ACME).
vs. ngrok/Cloudflare Tunnel: These are SaaS-only. Tunnet's relay is self-hosted and open-source. Tunnet bundles tunneling with mesh networking โ a tunnel endpoint can also be a mesh peer.
vs. ZeroTier: ZeroTier uses a custom P2P protocol with a root server network. Tunnet's Direct mode is conceptually similar (CRDT-governed membership) but uses iroh's QUIC transport instead of a custom protocol, and Tunnet bundles SSH, file transfer, and serve features that ZeroTier lacks.
vs. Headscale: Headscale is an open-source control server for Tailscale clients. Tunnet is a ground-up reimplementation with its own agent, control plane, and relay โ not a compatibility layer.
The key architectural difference from all these: Tunnet is a single identity system with a unified policy engine, where mesh networking, service exposure, tunneling, file transfer, and SSH all share the same routing table, ACL engine, and connection pool. Most competitors treat these as separate products or bolt them on.
0.6.0 (Rust workspace, edition 2024, MSRV 1.96). AGPL-3.0 with commercial licensing available.
Last updated: 2026-07-18