A lightweight, blazing-fast VPN implementation based on dsvpn. twice establishes secure network tunnels using UDP for minimal overhead, with optional HiAE authenticated encryption for maximum security when needed.
Performance: twice is significantly faster than WireGuard, offering superior throughput and lower latency for VPN connections.
Future Development: twice can be easily extended to support multi-link traffic redundancy, allowing simultaneous transmission over multiple network paths for increased reliability. This feature will be implemented if there is sufficient user interest.
- UDP-based transport (no TCP overhead)
- Optional HiAE authenticated encryption
- Automatic MTU discovery for optimal performance
- Packet deduplication and reordering handling
- Non-blocking I/O everywhere
- Cross-platform support (Linux, macOS, BSD variants)
- Simple client/server architecture
- Automatic routing and firewall configuration
By default (without a key file), twice provides authentication but NO encryption:
- All packets include a 128-bit authentication tag using HiAE_mac with an all-zero key
- Rejects unrelated UDP traffic and provides basic packet integrity
- Traffic is NOT encrypted - data is sent in plaintext
- Only use this mode on trusted internal networks or for testing
When configured with a key file, twice uses HiAE for full authenticated encryption:
- AES-based AEAD cipher with 256-bit keys
- 128-bit authentication tags prevent tampering and replay attacks
- Unique nonce per packet ensures security
- Hardware acceleration on modern CPUs (AES-NI, ARM Crypto Extensions)
- Full confidentiality and authenticity of all traffic
makeStart a UDP VPN server listening on all interfaces, port 41194:
sudo ./twice server autoOr specify a specific interface and port:
sudo ./twice server 0.0.0.0 41194Connect to a UDP VPN server:
sudo ./twice client <server-ip>Or specify a custom port:
sudo ./twice client <server-ip> 41194twice genkey [keyfile] # Generate 256-bit encryption key
twice [-k <keyfile>] [-m <mtu>] [-d] server [bind-ip|auto] [port|auto] [tun-interface|auto] [local-tun-ip|auto] [remote-tun-ip|auto] [external-ip|auto]
twice [-k <keyfile>] [-m <mtu>] [-d] client <server-ip> [port|auto] [tun-interface|auto] [local-tun-ip|auto] [remote-tun-ip|auto] [gateway-ip|auto]
Options:
-k, --key <file>: Path to 256-bit key file for encryption-m, --mtu <mtu>: Set TUN interface MTU in bytes (default: 1420, range: 68-9000)-d, --discover-mtu: Automatically discover optimal MTU (client only)
Arguments:
bind-ip(server only): IP address to bind to (default: all interfaces)server-ip(client only): VPN server IP addressport: UDP port number (default: 41194)tun-interface: TUN interface name (default: auto-assigned)local-tun-ip: Local tunnel IP address (default: 192.168.192.254 for server, 192.168.192.1 for client)remote-tun-ip: Remote tunnel IP address (default: 192.168.192.1 for server, 192.168.192.254 for client)external-ip: External interface IP (server) or gateway IP (client) for routing
Use "auto" for any parameter to use default values.
Create a 256-bit (32-byte) random key file that both server and client will use:
# Method 1: Use the built-in command (recommended)
./twice genkey # Creates vpn.key
./twice genkey mykey.key # Creates mykey.key with custom name
# Method 2: Generate manually with dd
dd if=/dev/urandom of=vpn.key bs=32 count=1
# Method 3: Generate with openssl
openssl rand -out vpn.key 32
# Verify key size (should be exactly 32 bytes)
ls -l vpn.key
wc -c vpn.key # Should output: 32 vpn.keyImportant:
- The key file must be exactly 32 bytes (256 bits)
- Use the same key file on both server and client
- Keep the key file secure - anyone with this file can decrypt your traffic
- Transfer the key file securely to the client (use SSH, not email or unencrypted methods)
Server with encryption:
sudo ./twice -k vpn.key server autoClient with encryption:
sudo ./twice -k vpn.key client <server-ip>Basic encrypted VPN:
# Server
sudo ./twice --key /path/to/vpn.key server auto
# Client
sudo ./twice --key /path/to/vpn.key client 10.0.1.100Encrypted VPN with custom MTU:
# Server (MTU adjusted for encryption overhead)
sudo ./twice -k vpn.key -m 1404 server auto
# Client
sudo ./twice -k vpn.key -m 1404 client 10.0.1.100Encrypted VPN with MTU discovery:
# Client with automatic MTU discovery (accounts for encryption overhead)
sudo ./twice -k vpn.key -d client 10.0.1.100- Overhead: All packets include a 16-byte authentication tag
- MTU: The effective TUN MTU is always reduced by 16 bytes to account for the authentication tag
- Performance: HiAE uses hardware AES acceleration when available (AES-NI on x86, ARM Crypto Extensions on ARM)
- Compatibility: Both server and client must use the same key file, or both must run without a key file
Server:
sudo ./twice serverClient:
sudo ./twice client 10.0.1.100Server with custom settings:
sudo ./twice server 10.0.1.100 2194 tun0 10.8.0.1 10.8.0.2Client with custom settings:
sudo ./twice client 10.0.1.100 2194 tun1 10.8.0.2 10.8.0.1Automatic MTU discovery:
# Client automatically discovers optimal MTU
sudo ./twice -d client 10.0.1.100Conservative MTU for problematic networks:
# Server
sudo ./twice -m 1380 server auto
# Client
sudo ./twice -m 1380 client 10.0.1.100PPPoE-friendly MTU:
# Server
sudo ./twice --mtu 1412 server auto
# Client
sudo ./twice --mtu 1412 client 10.0.1.100- TUN Interface: Creates a virtual network interface for routing packets
- UDP Encapsulation: Wraps TUN frames in UDP packets with 16-byte header
- Packet Management: Handles packet reordering, deduplication, and buffering
- MTU Discovery: Automatically finds optimal MTU using binary search probing
- Non-blocking I/O: Uses poll() for efficient event handling
- Server: Listens on UDP port and tracks the latest client address
- Client: Connects to server and maintains the connection
- Routing: Automatically configures system routing tables and firewall rules
All UDP packets include an authentication tag for packet validation:
+--------+--------+--------+--------+
| Random Value (8 bytes) |
+--------+--------+--------+--------+
| Counter (8 bytes) |
+--------+--------+--------+--------+
| TUN frame data |
| (variable length, plaintext) |
+--------+--------+--------+--------+
| Authentication Tag (16 bytes) |
+--------+--------+--------+--------+
+--------+--------+--------+--------+
| Random Value (8 bytes) |
+--------+--------+--------+--------+
| Counter (8 bytes) |
+--------+--------+--------+--------+
| Encrypted TUN frame data |
| (variable length, ciphertext) |
+--------+--------+--------+--------+
| Authentication Tag (16 bytes) |
+--------+--------+--------+--------+
The packet header (16 bytes) serves dual purposes:
- Packet identification: Random value (64-bit) + Counter (64-bit)
- Nonce for HiAE: Used for both encryption (when enabled) and authentication
Authentication is always enforced:
- Without key file: HiAE_mac with all-zero key validates packets
- With key file: Full HiAE encryption + authentication
- Invalid packets (failed authentication) are silently dropped
Note: No explicit length field is needed since UDP provides the packet length.
twice includes an automatic MTU discovery feature that finds the optimal Maximum Transmission Unit for your network path. This ensures maximum performance without packet fragmentation.
When enabled with the -d flag, the client:
- Sends probe packets of various sizes to the server
- Uses binary search between 576 (minimum) and 9000 (maximum) bytes
- Identifies probe packets using a special magic header (0xFFFFFFFFFFFFFFFF)
- Server echoes probe packets back unchanged
- Determines optimal MTU based on successful round-trips
- Automatically configures the TUN interface with the discovered MTU
# Enable automatic MTU discovery on client
sudo ./twice -d client <server-ip>The discovery process typically takes 5-10 seconds and happens during initial connection. The discovery automatically accounts for the 16-byte authentication tag overhead (present in all packets).
twice implements sophisticated packet management to handle unreliable network conditions:
- Duplicate Detection: Uses 64-bit packet counters to identify and drop duplicate packets
- Statistics Tracking: Monitors duplicate packet rates for diagnostics
- Zero Overhead: No additional memory or CPU cost for normal operation
- Sliding Window: Maintains a 1024-packet reordering buffer
- Out-of-Order Buffering: Temporarily stores future packets until missing ones arrive
- Timeout Recovery: Delivers buffered packets after 2-second timeout to prevent stalls
- Automatic Recovery: Handles peer restarts by detecting random value changes
- Resilient to packet loss: Continues operating smoothly even with 5-10% packet loss
- Handles burst reordering: Can reorder up to 1024 out-of-sequence packets
- No performance impact: Preallocated buffers ensure consistent memory usage
- Automatic recovery: Self-heals from network disruptions without manual intervention
twice implements comprehensive MTU handling to prevent packet fragmentation, which can severely impact VPN performance.
The default TUN interface MTU is 1420 bytes, which provides:
- 80 bytes headroom for encapsulation overhead
- Compatibility with most internet paths (even PPPoE links with 1492 MTU)
- Prevention of fragmentation on standard Ethernet networks (1500 MTU)
You can customize the TUN MTU using the -m or --mtu option:
# Set custom MTU for both server and client
sudo ./twice -m 1380 server auto
sudo ./twice --mtu 1380 client 10.0.1.100-
TCP MSS Clamping: Automatically modifies TCP SYN packets to clamp the Maximum Segment Size (MSS) to
MTU - 40bytes, preventing TCP from generating oversized segments. -
Packet Size Monitoring: Logs warnings for packets larger than 1500 bytes and drops packets that would definitely cause fragmentation.
-
Encapsulation Overhead Calculation: Accounts for UDP (8 bytes) + IP (20+ bytes) + packet header (16 bytes) + authentication tag (16 bytes, always present).
To find the optimal MTU for your network path:
-
Test with ping and DF (Don't Fragment) flag:
# Test from client side through the VPN tunnel ping -M do -s 1372 <remote-ip> # 1372 + 28 (IP+ICMP) = 1400 ping -M do -s 1392 <remote-ip> # 1392 + 28 = 1420
-
Gradually increase packet size until fragmentation occurs:
# If this works, try larger sizes ping -M do -s 1432 <remote-ip> # 1460 total ping -M do -s 1452 <remote-ip> # 1480 total
-
Set MTU based on largest successful ping:
- If 1392-byte ping works: use MTU 1420
- If 1372-byte ping works but 1392 fails: use MTU 1400
- If 1352-byte ping works but 1372 fails: use MTU 1380
Symptoms of MTU issues:
- Slow file transfers or web browsing
- Some websites load partially or not at all
- SSH/HTTPS connections hang during authentication
- Large ping packets fail
Solutions:
-
Reduce TUN MTU:
sudo ./twice -m 1380 client <server-ip>
-
Check path MTU:
# Test path to VPN server ping -M do -s 1472 <vpn-server-ip> # 1500 total ping -M do -s 1452 <vpn-server-ip> # 1480 total
-
Common MTU values for different scenarios:
- 1420: Default, safe for most networks
- 1400: Conservative, good for problematic links
- 1380: Very conservative, for heavily encapsulated networks
- 1280: Minimum IPv6 MTU, maximum compatibility
Network-specific considerations:
- PPPoE connections: Use 1412 or lower (1492 - 80 overhead)
- Mobile/cellular: Use 1400 or lower
- Corporate networks: May require 1380 or lower due to additional encapsulation
- Cloud providers: Check documentation for recommended values
- Linux: Full support with iptables integration
- macOS: Full support with route command integration
- BSD variants: Full support (OpenBSD, FreeBSD, NetBSD, DragonFly)
- Uses UDP instead of TCP transport
- Optional HiAE encryption instead of Charm
- Always includes authentication tags (HiAE_mac with zero key when no encryption)
- No key exchange - uses pre-shared keys when encryption is enabled
- Single client per server (latest client wins)
- Default port is 41194 (avoids ISP/WiFi blocking of common VPN ports)
make installThis installs twice to /usr/local/sbin/. You can change the prefix:
make install PREFIX=/usrtwice requires root privileges to create TUN interfaces and modify routing tables:
sudo ./twice serverIf TUN interface creation fails on Linux, ensure the TUN module is loaded:
sudo modprobe tuntwice automatically configures firewall rules. If connection fails:
- Check if iptables/pf is blocking UDP traffic
- Verify the server is listening:
netstat -un | grep 41194 - Test UDP connectivity:
nc -u server-ip 41194
If packets aren't routed correctly:
- Check routing table:
ip route(Linux) orroute -n get default(macOS/BSD) - Verify TUN interface is up:
ip link show(Linux) orifconfig(macOS/BSD) - Check if IP forwarding is enabled on server:
sysctl net.ipv4.ip_forward
If experiencing slow connections or partial website loading:
-
Check current MTU:
# Linux ip link show <tun-interface> # macOS/BSD ifconfig <tun-interface>
-
Test with smaller MTU:
sudo ./twice -m 1380 client <server-ip>
-
Verify no fragmentation:
# Test through tunnel with DF flag ping -M do -s 1372 <remote-ip>
-
Check for MSS clamping:
# Monitor TCP SYN packets to verify MSS is being clamped tcpdump -i <tun-interface> -n tcp[tcpflags] \& 0x02 != 0