Linux IP tunneling over UDP, with authenticated sessions, optional encryption, and SSH deployment.
tuntom connects two Linux TUN interfaces and carries IPv4 and IPv6 traffic
between them. It combines a self-contained C++17 tunnel engine with a bootstrap
script that builds, deploys, and configures both endpoints. Protocol v5 provides
replay protection, automatic path-MTU discovery, and internal fragmentation;
optional Ascon encryption and X25519 rekeying add confidentiality and forward
secrecy.
local / client remote / server
+-------------------+ +-------------------+
| Linux networking | | Linux networking |
+---------+---------+ +---------+---------+
ut42c ut42s
10.254.42.1 10.254.42.2
| |
+---------- UDP / port 40042 ----------+
authenticated v5 session
optional encryption + PFS
Written by Ales Stibal astib@mag0.net.
Licensed under BSD 3-Clause.
OpenAI Codex has been used in code and documentation development. Contributors remain responsible for the changes they submit.
Quick start · Configuration · Security and compatibility · Operations · Build and test
| Area | What tuntom provides |
|---|---|
| Tunnel | IPv4/IPv6 TUN traffic over UDP, NAT-friendly client/server model |
| Sessions | Authenticated v5 handshake, directional keys, replay protection |
| Encryption | Optional Ascon-AEAD128; optional X25519 PFS with periodic rekey |
| MTU | Independent inner/outer MTUs, automatic PMTUD, balanced fragmentation |
| Deployment | Local and remote compilation, staged restart, start/stop helper |
| Networking | IPv4 policy routing, connection marks, MSS clamping, optional SNAT, lifecycle hooks |
| Observability | Text statistics, signal-controlled snapshots, logs, Wireshark dissector |
| Runtime | No external crypto libraries; drops privileges to tuntom:tuntom |
The tunnel engine handles transport. Linux networking and the included
tuntom-net.sh helper handle routing and firewall policy; custom routes and
DNAT rules can be added through hooks.
For the bootstrap workflow, both hosts need:
- Linux with
/dev/net/tunand root access. g++with C++17 support, Bash,iproute2, andiptables.- Standard system utilities, including
tar,mktemp,getent,useradd, andgroupadd. - Synchronized clocks for the v5 handshake.
The caller also needs ssh, working SSH key authentication, and flock.
When started as a normal local user, the script uses sudo -E for privileged
local operations. The remote SSH account must already have root privileges:
remote commands do not use sudo. A bare hostname selects root@host.
The server's UDP port (40000 + tunnel ID) must be reachable from the client.
Set a random 128-bit shared secret as exactly 32 hex characters. For example, if OpenSSL is installed locally:
export TUNTOM_SECRET="$(openssl rand -hex 16)"From the repository directory, create tunnel 42 to sx2:
./mk_tunnel.sh 42 sx2 --pfsThis builds both endpoints, passes the secret over SSH, creates the runtime
account, configures networking, and starts the processes in the background.
--pfs enables encryption and forward secrecy on both ends. Omitting it uses
authentication without encryption; see the mode table.
| Tunnel 42 | Local / client | Remote / server |
|---|---|---|
| Interface | ut42c |
ut42s |
| IPv4 | 10.254.42.1 |
10.254.42.2 |
| IPv6 | fd42::10:254:42:1 |
fd42::10:254:42:2 |
| UDP port | Server destination: 40042 |
Listen: 40042 |
Once the session is established:
ping 10.254.42.2
ping -6 fd42::10:254:42:2Run the same start command again to rebuild and restart the tunnel. Both staged builds finish before the running tunnel is stopped; the switchover briefly interrupts traffic. Keep the secret and desired options when restarting.
./mk_tunnel.sh 42 sx2 --stopStopping removes both processes, interfaces, statistics files, and the helper's
per-tunnel networking rules. It does not require TUNTOM_SECRET.
For a local smoke test, use localhost as the host (root SSH access is still required).
Tunnel IDs range from 1 to 255 and determine interface names, addresses, and the server UDP port.
| Option | Effect |
|---|---|
--pfs |
Require X25519 + AKDF + Ascon-AEAD128 on both endpoints |
--encrypt-ascon |
Require Ascon-AEAD128 without PFS |
--no-stats |
Disable automatic statistics writes and optional sampling |
--snat / --no-snat |
Enable / disable IPv4 MASQUERADE; default: off |
--mss-clamp / --no-mss-clamp |
Enable / disable TCP MSS clamping; default: on |
--stop |
Stop and clean up the tunnel on both hosts |
| Variable | Default | Purpose |
|---|---|---|
TUNTOM_SECRET |
Required to start | 128-bit master key, 32 hex characters |
TUNTOM_PREFIX16 |
10.254 |
First two IPv4 octets; also used in IPv6 addresses |
TUNTOM_MTU |
1500 |
Inner/TUN MTU |
TUNTOM_TRANSPORT_MTU |
1400 |
Outer IP MTU / initial PMTUD target |
TUNTOM_STATS_FORMAT |
txt |
Statistics format; currently only txt |
TUNTOM_PRE_HOOK |
/etc/tuntom/tuntom-pre.sh |
Local source for pre-action hooks |
TUNTOM_POST_HOOK |
/etc/tuntom/tuntom-post.sh |
Local source for post-action hooks |
For example:
TUNTOM_PREFIX16=10.10 TUNTOM_MTU=9000 TUNTOM_TRANSPORT_MTU=1500 \
./mk_tunnel.sh 42 sx2 --pfsThis uses 10.10.42.1 / 10.10.42.2 and
fd42::10:10:42:1 / fd42::10:10:42:2, with a 9000-byte inner MTU.
IPv6 addresses use the prefix text with dots replaced by colons.
Advanced networking overrides are TUNTOM_MARK, TUNTOM_MARK_MASK,
TUNTOM_TABLE, and TUNTOM_CHAIN; see the helper.
The TUN MTU is independent of the outer IP MTU, which includes IP, UDP, and tuntom headers. Oversized inner packets are split into balanced fragments and reassembled at the receiving endpoint:
1500-byte inner packet -> 750 + 750 bytes of fragment payload
1401-byte inner packet -> 701 + 700 bytes of fragment payload
Automatic PMTUD starts at a conservative 500-byte outer MTU. Authenticated
MTU_PROBE / MTU_REPLY messages search for a working size, first targeting the
configured transport MTU and exploring up to at least 1500 bytes (higher if
configured). A probe times out after two seconds. Discovery restarts when the
peer changes or a data send fails; traffic continues at the last known-good MTU.
The standalone binary accepts --no-pmtud to keep --transport-mtu fixed.
The bootstrap does not forward arbitrary binary options.
By default, received IPv4 TTL / IPv6 Hop Limit is incremented by one; the IPv4
header checksum is updated. This compensates for an extra forwarding hop when
connecting routing points. Locally generated packets have not consumed that
hop and may therefore arrive with a value one higher than expected.
Use --no-ttl-compensate when running the binary directly to disable this.
All current modes use the v5 session handshake and a shared master secret. Both endpoints must select the same mode.
| Mode | Suite | Payload encryption | Forward secrecy |
|---|---|---|---|
| Default | 0 | No; authentication only | No |
--encrypt-ascon |
1 | Ascon-AEAD128 | No |
--pfs |
2 | Ascon-AEAD128 | X25519 exchange, rekey every two minutes |
The authentication primitive is specified in AMAC v1. Suite 2 uses a project-specific AMAC-based AKDF v1, not HKDF or a standardized Ascon KDF. X25519 is vendored from Monocypher. Construction details and security assumptions are documented in the v5 wire specification. Encryption adds no wire bytes. Mode mismatches fail the handshake without falling back to plaintext; old receive keys overlap for up to three seconds during PFS rekeying.
The handshake normally takes one RTT before the client can send DATA. Initial
TUN traffic is not buffered, and DATA arriving before CONFIRM is dropped.
Timestamped INITs require synchronized clocks: the default acceptance window is
300 seconds total (±150 seconds). The binary's --init-window accepts an even
value from 2 to 86400 seconds. Expired INITs and previously seen nonces are rejected.
Update both endpoints together. V5 is incompatible with V1–V4. There is no
legacy receive path or automatic downgrade; --allow-v1 and --allow-v2 are
rejected. DATA headers are 25 bytes, fragmented DATA headers 37 bytes. Tunnel ID
stays in configuration/key derivation; only INIT/RESPONSE transmit the version.
Processes start as root to initialize networking, then drop privileges to
tuntom:tuntom, disable core dumps, and set no_new_privs.
Files live on the respective endpoint hosts:
| File | Client | Server |
|---|---|---|
| Log | /tmp/tuntom_42c.log |
/tmp/tuntom_42s.log |
| PID | /run/tuntom/42c.pid |
/run/tuntom/42s.pid |
| Statistics | /run/tuntom/42c.stats |
/run/tuntom/42s.stats |
sudo tail -f /tmp/tuntom_42c.log
sudo cat /run/tuntom/42c.statsStatistics include traffic counters, throughput, sampled processing latency,
PMTUD state, active suite, session readiness, and handshake/rekey counters.
See statistics field definitions.
The standalone binary also accepts --debug and --quiet for logging.
Start with --no-stats to pause automatic writes and optional latency/throughput
sampling. Control each running process separately using its PID:
sudo kill -USR1 <pid> # toggle automatic statistics
sudo kill -USR2 <pid> # write one snapshot, even when disabledThe bootstrap retains the stats destination when disabled. For direct binary
use, supply --stats-file <path> even with --no-stats to allow later writes.
Cumulative counters continue while paused; the last file stays unchanged, so
check updated_unix for age. stats_enabled records the automatic mode.
Throughput windows restart when enabled; snapshots while paused retain the
last optional sampling history.
The bootstrap runs tuntom-net.sh on both hosts to set up IPv4 connection
marking, policy routing for replies, forwarding rules, MSS clamping, and optional
MASQUERADE. Configure the routes, forwarding sysctls, and application-specific
policy needed by your topology; IPv6 forwarding/firewall policy is separate.
Optional hook files exist on the caller only. Their content runs locally and is streamed over SSH for remote execution. Missing hooks are skipped.
pre/down -> network cleanup -> post/down
pre/up -> network setup -> post/up
Hooks receive TUNTOM_SIDE=local|remote, TUNTOM_ACTION=up|down,
TUNTOM_PHASE=pre|post, plus tunnel addresses, interface names, and networking
settings. Use post/up to add custom routes or DNAT rules.
See hook context and the
service ingress example.
tuntom.lua dissects v1, v2, v3, v4, and v5 captures, including handshake fields, session hints, sequence counters, fragments, authentication tags, and PMTUD probes. It reassembles unencrypted v5 DATA and passes inner packets to the IPv4/IPv6 dissector. Encrypted payloads remain encrypted in the capture.
The manual helper simulates silently dropped IPv4 UDP packets above a selected outer size:
sudo bash tests/pmtud-iptables-test.sh --size 1200 --interface eth0
# Start or restart the tunnel and inspect logs/statistics.
sudo bash tests/pmtud-iptables-test.sh --size 1200 --interface eth0 --removeThese rules affect all IPv4 UDP traffic above that size on the selected interface. Remove them after testing.
Build with CMake (3.16+) or open this directory as a CMake project in CLion:
cmake -S . -B /tmp/tuntom-build -DCMAKE_BUILD_TYPE=Release
cmake --build /tmp/tuntom-build
ctest --test-dir /tmp/tuntom-build --output-on-failureOr compile directly:
g++ -std=c++17 -O2 -Wall -Wextra -pedantic src/main.cpp -o /tmp/tuntomThe regression runner also checks header self-containment and runs the dissector
test when tshark and python3 are available. It requires no root or live tunnel:
bash tests/run.shSee test coverage and
standalone binary setup.
Direct execution requires the tuntom user/group and root at startup.
The bootstrap compiles with -O2 -march=native -mtune=native independently on
each host. It streams src/ as a tar archive and removes remote temporary
sources on exit, including compilation failure. CMake is not required for
deployment.
| Path | Contents |
|---|---|
| src/ | C++17 engine, ordinary headers, and main.cpp |
| src/vendor/ | Vendored X25519 implementation and provenance |
| mk_tunnel.sh | Build, deploy, start, restart, and stop |
| tuntom-net.sh | Linux routing and firewall helper |
| tuntom.lua | Wireshark Lua dissector |
| examples/ | Lifecycle hook example |
| tests/ | Regression tests, vectors, and manual PMTUD helper |
| docs/DETAILS.md | Implementation and operating details |
| docs/PROTOCOL_V5.md | Wire format, handshake, and cryptographic constructions |
| CMakeLists.txt | Local build and CTest targets |
| LICENSE.md | BSD 3-Clause license |