Interested in understanding the inner workings of a VPN? Thinking about setting up your own VPN server? Today, we’re taking a straightforward look at how to set up a basic VPN server using Rust.
In this article, we’ll break down the core concepts and guide you through the process with some clear-cut Rust code. By the end, you’ll have a foundational understanding and a basic VPN server to show for it.
Let’s get started!
How a VPN Works
A VPN is essentially a private tunnel between your computer (or another device) and a remote server, usually operated by a VPN service. Here’s how it all breaks down:
- Initiation: When you launch a VPN client on your device, it reaches out to a VPN server to establish a secure connection.
- Authentication: The client and server go through a handshake process. They exchange credentials, ensure they’re talking to the right entities, and set up encryption protocols for the session.
- Tunneling: Once authenticated, a secure tunnel forms between the client and server. This tunnel ensures that data passing through remains confidential and intact.
- Data Transfer:
- Encryption: Before data leaves your device, the VPN client encrypts it. This turns readable data into scrambled code.
- Transit: The encrypted data travels through the internet, passing through various routers and servers. But to any prying eyes, the data looks like gibberish.
- Decryption: Once the data reaches the VPN server, it’s decrypted, turning it back into a readable format.
- Exiting to the Internet: The VPN server then sends the data out to the internet to reach its destination (like a website). Importantly, to the outside world (like the website or your ISP), it appears as though the data is coming from the VPN server, not your device. This masks your real IP address.
6. Receiving Data: When data is sent back from the internet, the process reverses. The VPN server receives the data, encrypts it, sends it through the tunnel to your device, where it’s then decrypted for you to see.
7. Disconnection: Once you’re done, the VPN client will close the connection with the server, shutting down the secure tunnel.
The Crates we will use
- clap (v2.33): A popular crate that aids in creating command-line interfaces for Rust applications. It assists with parsing arguments, displaying help messages, and managing configurations.
- aes-gcm (v0.10.3) and aes-soft (v0.6): These crates relate to encryption.
aes-gcmprovides authenticated encryption using the AES-GCM mode, offering both confidentiality and data integrity.aes-softis a software-based (as opposed to hardware-accelerated) implementation of the AES algorithm. - tokio (v1): A runtime for writing asynchronous applications with Rust. The
fullandsignalfeatures suggest a full suite of asynchronous features, including support for handling signals (like shutting down gracefully). - tun (v0.6.1) and tun-tap (v0.1.4): Both these crates pertain to virtual networking. The
tuncrate provides a platform-agnostic interface for creating and managing TUN devices, whereastun-tapis a library to manage TUN and TAP devices, allowing for network-layer (TUN) and link-layer (TAP) traffic respectively. - serde (v1.0) and serde_derive (v1.0.190): Serialization and deserialization are at the heart of these crates.
serdeis a generic serialization/deserialization framework, whileserde_deriveenables the use of procedural macros to automatically generate the necessary code for serialization and deserialization. - bincode (v1.3): A binary serialization and deserialization strategy. When combined with Serde, it allows you to encode and decode Rust data structures in a compact binary format.
- rand (v0.8): This crate provides a suite of randomization utilities, whether you’re generating random numbers, picking random elements, or shuffling data.
- anyhow (v1.0): A flexible and easy-to-use crate for error handling,
anyhowallows for creating and managing custom errors while also ensuring nice error reporting. - ctrlc (v3.1): Handling interruptions is essential, especially for long-running tasks. The
ctrlccrate offers functionality to gracefully handle the Ctrl-C signal, allowing for clean shutdowns or specific actions upon interruption. - aes (v0.7): A crate dedicated to the AES (Advanced Encryption Standard) symmetric encryption algorithm. It serves as the foundation for various encryption-related tasks.
- env_logger (v0.9): Logging is a critical component of many applications. The
env_loggercrate is a logger which is configured via an environment variable, allowing dynamic control over log output.
Project Setup
Add the following dependencies to your Cargo.toml:
[dependencies]
clap = "2.33"
aes-gcm = "0.10.3"
aes-soft = "0.6"
tokio = { version = "1", features = ["full", "signal"] }
tun = "0.6.1"
tun-tap = "0.1.4"
serde = "1.0"
serde_derive = "1.0.190"
bincode = "1.3"
rand = "0.8"
anyhow = "1.0"
ctrlc = "3.1"
aes = "0.7"
block-modes = "0.8"
block-padding = "0.2"
generic-array = "0.14"
socket2 = "0.4"
env_logger = "0.9"
log = "0.4.20"
The Server Logic
This function server_mode represents the primary logic for a server that listens for incoming TCP connections, manages a TUN virtual network interface, and shuffles data between the TUN interface and its clients. Let's break down the code to understand each part:
Initialization:
let listener = TcpListener::bind("0.0.0.0:12345").unwrap();
This sets up a TCP listener that listens on all available interfaces (0.0.0.0) on port 12345.
let clients: Arc<Mutex<HashMap<usize, TcpStream>>> = Arc::new(Mutex::new(HashMap::new()));
Here, an atomic reference-counted (Arc) HashMap is initialized to keep track of connected clients. This HashMap maps client IDs (of type usize) to their respective TcpStream. The use of Arc and Mutex ensures that this map can be safely accessed and modified from multiple threads.
Setting up the TUN interface:
let mut config = tun::Configuration::default();
config.name("tun0");
let tun_device = tun::create(&config).unwrap();
This initializes the configuration for a TUN device named “tun0” and creates it.
if let Err(e) = setup_tun_interface() {
eprintln!("Failed to set up TUN interface: {}", e);
return;
}
Here, the TUN interface “tun0” is being set up using the setup_tun_interface function. If there's an error, the function will print an error message and return early.

Share the TUN device among threads:
let shared_tun = Arc::new(Mutex::new(tun_device));
This wraps the TUN device in an Arc and Mutex so it can be shared and safely accessed among multiple threads.
Start server and handle data flow:
info!("Server started on 0.0.0.0:12345");
This logs the message indicating that the server has started.
The block below spawns a new thread. Within this thread, the server attempts to fetch the client with the key 0 from the clients map and read data from the TUN device, sending it to this client.
let tun_device_clone = shared_tun.clone();
let clients_clone = clients.clone();
thread::spawn(move || {
...
});
Listen for incoming connections:
for (client_id, stream) in listener.incoming().enumerate() {
...
}
The server continuously listens for incoming client connections. For each client, it assigns an ID (client_id), which is just an enumeration of the incoming connection.
Inside the loop:
- The server spawns a new thread for each client to handle data exchange between the TUN device and the client.
- It adds the client’s
TcpStreamto theclientsmap. - Another thread is spawned to handle the client’s overall functionality, presumably reading data from the client and writing it to the TUN device.
Error Handling: Throughout the function, there are several error checks using either pattern matching or if let. These error checks handle cases like:
- Failure to set up the TUN interface.
- Failure to clone a client’s
TcpStream. - A client with a specific ID not found in the
clientsmap. - Connection failures.
Cleanup:
let _ = destroy_tun_interface();
Before the function ends, the server attempts to destroy or clean up the “tun0” interface using the destroy_tun_interface function.
To summarize, the server_mode function sets up a server that listens for incoming TCP client connections and manages data exchange between these clients and a TUN virtual network interface. The server uses multithreading to handle multiple clients and TUN device operations concurrently.
The VPN Client Logic
Here, we’re looking at the client-side logic for connecting to a VPN server and managing data exchange between the client and a TUN virtual network interface. Let’s break down and explain each part:
Function: client_mode:
This asynchronous function represents the primary logic for the VPN client.
- Connect to the VPN Server:
let mut stream = TcpStream::connect(vpn_server_ip).unwrap();
Here, the client establishes a TCP connection to the VPN server using its IP address.
- Clone the Stream:
let mut stream_clone = stream.try_clone().unwrap();
The TCP stream is cloned so that it can be used in multiple places, particularly in the subsequent loops where data will be read from the server and written to the TUN device.
- Initialize the TUN Interface:
let mut config = tun::Configuration::default();
config.name(TUN_INTERFACE_NAME);
let mut tun_device = tun::create(&config).unwrap();
The client sets up its TUN interface using a predefined interface name (TUN_INTERFACE_NAME).
- Setup the Client’s IP Address and Routing:
set_client_ip_and_route();
This function call presumably sets the IP address for the client’s TUN interface and configures the necessary routes.
- Data Exchange Loop:
let mut buffer = [0; 1024]; loop { ... }- Inside this loop, the client continuously reads data from the VPN server and writes it to the TUN interface. If there’s any error while reading from the server, the loop terminates.
- Function:
read_from_client_and_write_to_tun: - This asynchronous function handles the data exchange between the client’s TCP stream (from the VPN server) and the TUN interface.
- Data Reading Loop:
let mut buffer = [0u8; 1500];
loop {
...
}
Within this loop, the client:
Reads from the VPN server:
match client.read(&mut buffer) {
...
}
The client attempts to read data packets from the VPN server into a buffer.
Deserialize and Decrypt the Packet:
let vpn_packet: VpnPacket = bincode::deserialize(&buffer[..n]).unwrap();
let decrypted_data = decrypt(&vpn_packet.data);
The received data is then deserialized into a VpnPacket structure. This structure presumably encapsulates the VPN data packet, which is then decrypted to retrieve the original data.
Write to TUN Interface:
tun.write(&decrypted_data).unwrap();
The decrypted data is then written to the TUN interface.
- Error Handling: If there’s an error while reading from the VPN server, an error message is logged, and the loop continues to the next iteration.
Networking and Tunneling
Let’s now see the two essential functions that set up and configure the VPN interfaces on both the client and server ends, ensuring proper communication and routing through the VPN tunnel.
set_client_ip_and_route()
This function sets up the client’s end of a VPN connection.



