# AeroTUN v3 — implementation and deployment, 2026-10-30 V3 is deployed on both DebTik hosts. It replaces the v2 PSK-only handshake with Noise IKpsk2, pinned X25519 identities and ephemeral session keys, while keeping XChaCha20-Poly1305 for IP data. This is a custom protocol integration; the testing below is not an independent cryptographic audit or a security certification. ## Wire protocol and authentication V3 requires the same random 32-byte PSK on both hosts, plus the other endpoint's pinned static public key. Keys accept 64-character hex or base64 encoding of 32 bytes. `github.com/flynn/noise` v1.1.0 implements the handshake pattern `Noise_IKpsk2_25519_ChaChaPoly_SHA256`; the application does not implement its own Diffie-Hellman or AEAD primitive. The handshake prologue binds the AeroTUN v3 protocol name and the initiating 16-byte header. Encrypted handshake metadata carries the receiver index, MTU, and compression capability. Application IP traffic is never sent as 0-RTT data. The responder verifies the initiator's static identity against its configured peer before publishing a pending session. It activates that session only after receiving authenticated transport data, proving completion and PSK possession. A domain-separated HMAC-SHA256 truncated to 16 bytes authenticates handshake packets before expensive DH work. A stateless source cookie binds the sender's IP/port and initiation identifier to a process-random secret and a 30-second time bucket; current and previous buckets are accepted. The cookie is itself carried in a PSK-authenticated request. This prevents moving recorded requests to a new source without knowing the PSK. New DH handshakes are limited to one per second per configured tunnel. State remains bounded; no unbounded source-address map is created for unauthenticated traffic. After Noise completes, its two directional split keys are exported solely to HKDF-SHA256, with the handshake channel binding as salt and an AeroTUN v3 data label as context. Each direction derives a distinct 32-byte XChaCha key and a 16-byte nonce prefix. Noise CipherStates are not subsequently used for data. Each data/control datagram has this authenticated 16-byte header: | Field | Bytes | | --- | ---: | | Magic | 2 | | Protocol version | 1 | | Message kind | 1 | | Receiver session index | 4 | | Counter | 8 | The full header is AEAD additional data. The 24-byte nonce is the derived 16-byte prefix followed by the counter. An atomic counter covers control and data senders, rejects zero/wrap, and triggers renewal before exhaustion. The receiver checks authentication before committing replay state. A fixed 128-byte bitmap supports a 1024-packet replay/reorder window. Unknown sessions and malformed or oversized packets are rejected without creating a data session. The client renews the handshake every 90 seconds; active traffic continues on the current keys while renewal runs. Previous receive keys have a 30-second grace period. Sessions expire after 180 seconds, pending sessions after five seconds. At most current, previous and pending session objects are retained. Go does not guarantee physical erasure of discarded heap objects, so process-memory compromise is outside any claim of secure key destruction. V3 does not negotiate down to v2. V2 remains an explicitly selected legacy mode for coordinated rollback. These are separate wire protocols. ## Packet path and resource bounds - Separate reusable TX/RX buffers, including the veth Ethernet write buffer. - In-place data encryption and caller-owned decryption/nonce buffers; zero heap allocations in the measured steady-state crypto round trip. - UDP `ReadBatch`/`WriteBatch`, up to 16 packets; available packets flush without waiting to fill a batch. - A bounded 16-packet transmit queue backed by 32 preallocated packet buffers. - A fixed number of workers; none are created per packet. - Bulk encryption/decryption runs outside the handshake mutex. Replay state has a short receive-only critical section. - Buffers scale with the validated configured MTU, not 64 KB per scratch packet. - V3 requests 1 MiB socket buffers in each direction instead of v2's 4 MiB. Kernel accounting/limits can change actual buffer memory use. - Compression defaults off for new configurations and is disabled on the deployed pair. It is accepted only when both peers explicitly negotiate support. Headers plus authentication tag shrink from 60 to 32 bytes. Including IPv4/UDP, the encapsulation overhead becomes 60 bytes instead of 88. The actual outer path later settled at MTU 1380, allowing an overlay MTU of **1320** and a UDP benchmark datagram of **1292** bytes. During an earlier phase the overlay was 1360; sizes are negotiated from route/link limits and peer advertisements. Offloads remain disabled for the raw veth backend because it does not implement GSO segmentation or completion of partial checksums. Enabling flags without that implementation would reintroduce the original TCP failure. Native TUN is preferred when available. The local container denies TUN device access; changing that needs Proxmox host configuration outside this container's access. The ARM64 peer uses native TUN successfully. ## Peer policy and secret handling V3 requires explicit allowed source prefixes for the authenticated peer. IPv4 and IPv6 source addresses are checked before injection into the host interface. The deployed pair currently allows only each other's overlay /32 address. To carry LAN traffic, administrators must add the intended LAN prefixes and corresponding routes/firewall policy; a configured peer key does not authorize arbitrary source addresses. List/save API responses omit PSKs and private identity keys, including for admin clients. `has_psk`/`has_private_key` indicate presence. An empty secret on edit retains the stored value. Only full administrators can change/start/stop tunnels or generate PSKs. Generated-key responses use `Cache-Control: no-store`. Configuration and generated identity files are mode 0600. Existing management credentials and listeners are preserved; strong authentication and HTTPS/trusted management access are still required for appliance administration. New v3 tunnels can be saved disabled to generate a local identity. Copy its public key to the other endpoint over a trusted channel, set allowed prefixes, then enable both sides. Static private keys are never returned by the UI/API. For bootstrap on the owner host: ```sh aerotun-keygen -out /root/new-aerotun-identity.json ``` The command creates the file once with mode 0600 and prints only the public key. The deployed identities were generated separately on their owner hosts and their public keys cross-pinned over the verified SSH channel. The existing PSK was kept. ## Validation and observed results Both full repository tests and `go vet` pass. API regression tests cover redaction, privilege checks and editing without revealing/erasing a key. Protocol tests cover pinned identities, incorrect PSKs, tampered counters/headers, reflection, replay, reordering, restart, rekey under concurrent traffic, cookie source binding/expiry, and in-place encryption. Fuzzing ran about 270,000 inputs per ten-second run without a crash. Isolated v2/v3 namespace tests passed with race detection, including TCP both directions, UDP, compression negotiation and constrained links. A paired 1200-byte encrypt/decrypt microbenchmark on the local i5-4570T, using Go 1.26.10, measured approximately: | Crypto path | Time | Allocated bytes | Allocations | | --- | ---: | ---: | ---: | | v2 | 2.85–2.97 μs | 2608 | 4 | | v3 | 2.23–2.30 μs | 0 | 0 | These are crypto-path measurements, not Internet throughput claims. Network batch helpers and interface I/O have their own overhead. An isolated no-compression run delivered about 856/737 Mbit/s TCP for v3 versus 635/642 Mbit/s for v2; short runs vary with host load and are not a capacity guarantee. Live application benchmark results after deployment: | Test | Confirmed TX | RX | Reported loss | | --- | ---: | ---: | ---: | | Remote Both / Auto, 10 s | 7.862 Mbit/s | 10.457 Mbit/s | 0.983% | | Remote RX-only / Auto, 8 s | 0 | 10.098 Mbit/s | 1.162% | | Local Both / Auto, 4 streams, 8 s | 7.536 Mbit/s | 10.831 Mbit/s | 0.160% | | TCP Both, fixed 2 Mbit/s target | 1.835 Mbit/s | 1.966 Mbit/s | no application-reported loss | Live telemetry showed both peers connected at MTU 1320 with no error. Automatic key renewal completed repeatedly on both hosts, with the handshake count advancing to four during validation. A transient rejected packet during the v2/v3 transition is retained in the remote authentication-failure counter; no replay drops were observed. Counts of policy/oversize drops can include startup link-local traffic and MTU adjustment, and are not equivalent to WAN packet loss. `govulncheck` found no reachable vulnerable symbols or imported vulnerable packages. It flagged the unused/unmaintained `x/crypto/openpgp` package at module level; this application does not import that package. Builds use Go 1.26.10. ## Artifacts and recovery Builds, test logs, benchmark output, deployment state and redacted runtime state: `/root/.local/share/debtik-builds/aerotun-v3/`. Backups on both hosts: `/root/.local/share/debtik-backups/aerotun-v3-20261030T130621Z/`. They contain the prior v2 executable/configuration and route/link snapshots. Rollback timers protected activation and were cancelled after validation. The `committed` marker guards the backup rollback script against accidental execution. Deliberate rollback must restore both peers' version and configuration together. Private identity files in the backup directory remain owner-only. For verification: ```sh go test ./... go test -race ./internal/aerotun ./internal/api go vet ./... go test -run '^$' -bench 'Benchmark.*ProtocolRoundTrip' -benchmem ./internal/aerotun AEROTUN_INTEGRATION=1 go test -race -run TestVethBandwidthIntegration -v ./internal/aerotun ``` The integration harness needs root, namespace privileges, ip, tc, nsenter and iperf3. It creates disposable namespaces rather than changing production links.