This project implements a full VPN tunnel between a client and a server.
The client creates a utun virtual interface, routes all its traffic through it, and sends it to the server over UDP.
The server accepts connections from multiple clients, creates a separate utun for each one, forwards traffic, and provides internet access.
The project is written in C, uses libsodium for encryption (ChaCha20-Poly1305), and runs on macOS (with minor changes required for Linux).
The client opens a utun interface, assigns it an IP address (e.g., 10.0.0.2), and sends all traffic through an encrypted UDP tunnel to the server.
The server receives a packet, identifies which client it came from (by IP and port), creates a new utun for a new client (e.g., utun5 with IP 10.0.0.1), and forwards the traffic.
- macOS
libsodium(for encryption)
brew install libsodiumOnly libsodium is required for compilation. Everything else is built-in.
make # builds both server and client
make server # builds only the server
make client # builds only the client
make clean # removes compiled filesBoth the client and the server must use the same 32-byte key.
You can generate the key using a separate program or with the following code:
#include <sodium.h>
// ...
unsigned char key[crypto_aead_xchacha20poly1305_ietf_KEYBYTES];
crypto_aead_xchacha20poly1305_ietf_keygen(key);
FILE *f = fopen("vpn.key", "wb");
fwrite(key, sizeof(key), 1, f);
fclose(f);Then copy the resulting vpn.key file to both the client and the server.
Copy the vpn_serv.conf and vpn_cli.conf files to the directory with the binaries.
UDP_PORT = 12346
TUN_IP = 10.0.0.1
MAX_CLIENTS = 256
LOG_FILE = /var/log/vpn.log
REMOTE_IP = 127.0.0.1
REMOTE_PORT = 12346
TUN_IP = 10.0.0.2
SERVER_TUN_IP = 10.0.0.1
sudo ./bin/vpn_serverThe server will listen on the UDP port specified in the configuration file and create TUN interfaces as clients connect.
sudo ./bin/vpn_clientThe client will connect to the server, create its own utun interface, and route all its traffic through the tunnel.
TCP inside TCP causes problems: packet loss triggers retransmission at both the inner and outer levels, leading to delays and confusion. UDP is simpler, faster, and does not interfere.
TUN is a virtual network interface at Layer 3 (IP). When a program writes to it, the kernel processes the IP packet as if it came from the network. Conversely, packets routed to the TUN interface can be read by the program.
After startup, the client creates a TUN interface and adds a default route through it:
route add default 10.0.0.2Now every packet not destined for the local network goes into the TUN interface.
The client sends the first UDP packet (this can be regular encrypted traffic).
The server receives a packet from an unknown address, creates a new TUN for that client, and assigns it an IP address from the internal network.
All subsequent packets from that client are directed into its dedicated TUN interface.
UDP is connectionless. Each datagram arrives independently, and the server does not need to establish a persistent connection. It only needs to remember the sender’s address.
For each client, the server creates a separate TUN interface and assigns an IP address (typically 10.0.0.1 for the server and 10.0.0.x for the client).
The kernel automatically creates a route to the client. Internet access may require masquerading (NAT), but this project focuses only on tunneling.
Network operations must not block each other. The program uses poll (or select) to monitor multiple file descriptors at once:
- TUN for incoming packets from the kernel.
- UDP socket for incoming packets from the remote peer.
Press Ctrl+C to stop the program. It will close the TUN interface and display statistics showing how many bytes and packets were transferred.
vpn/
├── vpn_server
├── vpn_client
├── vpn.key (shared secret key, 32 bytes)
├── vpn_serv.conf
├── vpn_cli.conf
├── common.h / common.c
├── server.h / server.c
└── client.h / client.c
From the client, ping the server’s tunnel IP address:
ping 10.0.0.1You should receive replies.
- Silence on the Wire by Michal Zalewski (main inspiration for passive analysis)
- Beej’s Guide to Network Programming
- Linux TUN/TAP documentation
- libsodium documentation
- To run this project on Linux, replace the
socket(PF_SYSTEM, ...)calls withopen("/dev/net/tun", ...). - The project uses ChaCha20-Poly1305 from libsodium, a modern and fast encryption algorithm.
- This is an educational implementation, not optimized for high performance or production use.