UDP-WG Implementation
|
This repository contains an emulated implementation of both the UDP network protocol, alongside the WireGuard VPN protocol, as a means to better understand not only these protocols in isolation, but how they might be used in practice. The intention of this repository’s creation is primarily as a learning aid to explore Virtual Private Networks through a heavily documented codebase outlining how WireGuard works.
There’s three primary ways to approach this repository:
udp.h
contains our UDP packet implementation, which is used directly by our WireGuard implementation to send packets across the network!wireguard.h
contains our WireGuard implementation, and all the cryptographic algorithms used by it are contained in crypto.h
. We use both OpenSSL and Sodium for cryptography, allowing you to get a taste of how you might work with both libraries.main.cpp
contains the code for the main application, with shared.h
containing the functions needed for thread-safe input and output, and network.h
containing the code for a threaded, Dynamic Network Thread.main
uses our implementations to allow peers to communicate across the network using UDP packets, alongside connecting to peers acting as WireGuard servers to securely communicate with peers while staying anonymous and avoiding eavesdroppers!docs
folder which provides rendered HTML for all the various namespaces, functions, and classes.>[!tip] >Feeling overwhelmed? Start out with the application! There, you can get a feel on how the code actually comes together and understand the general flow of logic. When you then look into the code itself, or the Doxygen site, you’ll have a better appreciation of what the various parts are used for.
This codebase compiles into a network application where peers across the network can communicate with each other using UDP packets. We use WireGuard in a similar fashion to how you might be familiar with VPNs: A private tunnel that masks your IP address and protects your traffic within the tunnel via strong encryption. All peers are capable of becoming a WireGuard server for another peer, which involves the two peers completing a handshake to derive shared transport keys, and then the establishment of a end point for the client on the server’s device.
UDP-WG relies on OpenSSL and Sodium for its cryptographic operations. You’ll need to install both if you want to run the application, or build it from source. Due to the nature of shared libraries, the pre-compiled application may be linked against versions of these two libraries that aren’t currently on your system. If you see the following error, or something similar:
Then you’ll need to compile the application using your own versions. You can build the application by typing make
within the UDP-WG directory. We use the g++
compiler from the GNU Compiler Collection (GCC), and use the aforementioned OpenSSL and Sodium libraries. You should be able to find all of these in whatever package manager your distribution uses; below is a few of the most popular distributions:
sudo apt-get install g++ libssl-dev libsodium-dev
sudo dnf install gnu-c++ openssl-devel libsodium
sudo pacman -Syu gcc openssl libsodium
We provide two pre-compiled binaries, based on the version of Sodium. Sodium 23 is the current version used by Ubuntu, and this version is accordingly named main23
. The latest version of Sodium, Sodium 26, has also been built for rolling release distributions, and is named main26
. If both executables have errors in dynamic linking, you’ll need to compile the application so that it links to your specific versions of OpenSSL and Sodium.
Upon the start of program execution, UDP-WG tests its cryptographic implementations to ensure that they work as expected. You should see output like the following:
If any of the text is in red, or do not specify “Success,” then there’s an issue with the application that will cause errors should you continue the application. If this happens, try compiling the application for your system by running make
. The output binary main
, should work correctly.
To run the application simply run ./mainXX
from your terminal, where XX
is the pre-compiled version. If you’re using a self-compiled version, the executable should just be main
. To see what options are available on the command line, use the --help
flag:
--port
specifies the port that the application will bind to for network communication. If you launch multiple instances of the program on the same computer, you’ll need to provide a unique port for each. If the port is already in use, the application will report such an error and close; by default, the application binds to port $5000$.--addr
specifies the address that should be used by the application. If you’re only talking to instances of the program on a single computer, leave this as the localhost address; if you want to talk across a network, use the public IP of the computer.--verbose
prints verbose information to the console.--packet
will print the entire packet in a neatly visualized block, rather than simply printing the source and data contained.--log
will write out status messages to the provided log file, such as --log=server.txt
--help
displays the help message.Once you’ve confirmed that the cryptographic operations work correctly, you should be brought to the home screen:
For sending UDP packets, we’re only interested in option one and two. This home page constantly updates, and the (0) value in option two contains the amount of unread messages that you have received. When a new message is received, the value will turn green to alert you! Let’s send a message to another computer; to do this, we’ll need to have the program running on both machines, and ensure that firewalls and other security systems will permit the traffic to our chosen port. First, we choose option $1$, which will bring us to the following dialog:
Here, you can either provide the raw IP:Port combination, or an Alias if you’ve already sent a message to this peer before. For our case, we’re going to send a packet to the instance of this program running on the machine located at 192.168.101.221
, bound to the default port of $5000$:
mydomain.com:5000
will not work! If you aren’t sure what the IP of your peer is, use ping
!Here, we specify our machine, and provide an alias. This way, when we want to send another message to the peer, we can simply provide the alias:
You should then be brought back to the home screen, and your data will be packaged into a UDP packet, and sent across to the other peer. Let’s take a look at their home screen!
Notice the new message (Unfortunately, this document doesn’t allow us to have color in the code blocks). Let’s see what the peer sent by selecting 2!
The IP address might look a little usual, but this is simply the internal representation of the 127.0.0.1
0xFF
or 255), and putting them together in reverse order. For 127.0.0.1
, we’d convert it into Hex as 7F.00.00.01
. Since network addresses are stored in Big-Endian format, we put them in reverse order 0100007F
, which in decimal is $16777343$!Here, we see the source, alongside the data that we sent. If you want slightly more information, turn on the --packet
argument!
Here, we see the contents of the Psuedo-Header, including the source address, destination address ($3714427072$ = 0xDD65A8C0
= C0.A8.65.DD
= 192.168.101.221
), UDP identifier = $17$, and the length of the whole packet $26$. In the Header, we see the source and destination port, both $5000$, the length again, and the checksum value of $41472$. Finally, we have the data. You may notice that the source address is the local address. You may expect an attempt to reply to be send back to itself, since it would be pointed to localhost:5000
, or the running program, but you can actually reply! Let’s reply:
Sure enough the message gets routed, let’s look back at first instance:
[!info] Why does this work? The Network Thread that facilitates all network communications, upon discovering a new destination, automatically gets into contact with the remote Network Thread, and establishes a File Descriptor that the two can communicate over. Both Network Threads associate this FD with the claimed source of the other peer, which means the first network thread associates this FD with the destination
192.168.101.221
, whereas the second peer associates the FD with the first peer’s claimed source IP, which defaults to127.0.0.1
. Therefore, all packets sent to127.0.0.1:5000
will be sent to the other peer. To fix this use the--addr
command line argument to provide your public facing IP! For example:./main --addr=192.168.101.1
for the first program, and./main --addr=192.168.101.221
.
Let’s say we have three users of the program, Alice at 192.168.101.1:3000
, Bob at 192.168.100.1:4000
, and Carol at 192.168.100.193:5000
. Alice wants to communicate with Carol, but wants to employ the WireGuard protocol to secure the communication. Why might we want to do this?
192.168.101.0/24
subnet, and has set her network interface card to promiscuous mode to capture all packets on the network. Alice cannot trust her local subnet, but 192.168.100.0/24
is a trusted network. If she can create a tunnel into the 192.168.100.0/24
network, she can safely communicate within it. She can use WireGuard to create a secure tunnel into this secure network, thwarting Eve by encrypting her traffic within the insecure network.In this application, WireGuard is used to connect a Client with a Server. Once a Handshake has been performed, the Server creates an End-Point for the Client on their machine. The Client can then encrypt their packets and send them to the End Point. Once received, the Server will decrypt the packets, read the embedded UDP packet, and then send that packet to its intended target. Likewise, other peers on the network can send plaintext packets to the End Point, and the Server will encrypt them and send them to the client. Let’s consider the two example situations from before:
192.168.101.0/24
subnet. When Bob receives the packets, he will decrypt the content and send the original packet, designated for Carol, in plaintext across the trusted 192.168.100.0/24
subnet. Trying to eavesdrop upon the connection, Eve will be only able to capture encrypted WireGuard packets, and her attempts to read what Alice is sending will be thwarted.So, how does Alice establish a WireGuard connection with Bob? It’s actually quite easy. First, let’s have each peer start an instance of the program with the correct arguments:
./main --addr=192.168.101.1 --port=3000
./main --addr=192.168.100.1 --port=4000
./main --addr=192.168.100.193 --port=5000
Then, Alice selects 3. Connect to a WireGuard Server
:
Bob will then receive a notification:
In this application, public keys are sent across the wire in plaintext. This presents a possible vulnerability in which Eve could intercept the packets and initiate her own Handshake, one with Alice, and another Bob. Then, she could transparently encrypt the packets using Transport Keys derived from both peers. This vulnerability does not exist in the official WireGuard implementation, as both the public key and configuration is stored in a configuration file, rather than be communicated across the network; rather than require users to generate and point to these configurations, UDP-WG simplifies the exchange by sending the public keys across the wire to initiate the handshake.
With that said, we presume that Bob and Alice have some means to verify the authenticity of the public keys, as the first 16 bytes of the truncated key are displayed both on the home screen, and during the key exchange. If Bob recognizes the key as belonging to Alice, he has three options on how to respond:
Bob isn’t currently under load, and recognizes Alice’s public key, so accepts the Handshake. He then sends his own public key over to Alice, who will get her own notification:
Again, Alice needs to confirm that she is truly speaking with Bob, and confirms that the public key matches what she is expecting. Once she confirms, UDP-WG will perform the WireGuard handshake, and both Alice and Bob will derive a set of common transport keys that they can use to securely encrypt data to send to one another. These keys are cycled every two minutes by using a set of Ephemeral Keys on top of their Static Keys (The public portion of which was sent across the wire).
Alice’s home screen will then update to reflect this new WireGuard connection:
Let’s go over her new options:
Send a UDP message over WireGuard
will function identically to the original UDP Message option, where Alice can provide the address and port of Carol, give her an alias, and provide the data to send. However, rather than sending this packet directly to Carol, UDP-WG will instead encrypt this packet using the shared Transport Key and send it to the End Point Bob created for her. When this packet receives Bob, he will decrypt it using his own copy of the Transport Keys, get the UDP packet placed within the WireGuard Packet, and send that packet to Carol as the intended destination. By spoofing the source to the End Point, Carol will receive the message as having been sent from the End Point, and her plaintext reply will be sent back to Bob, who will use his shared Transport Key to encrypt the packet before sending it back to Alice, who can then decrypt it and read Carol’s reply, again formatted such that source appears as Carol’s IP address, rather than the End Point, and the destination is Alice’s actual IP address, not the End Point. This ensures a seamless communication, where neither Alice or Carol need to be aware of the WireGuard server routing their packets.Disconnect from the WireGuard Server
terminates the connection between Alice and Bob. When Alice closes the connection, Bob will notice and immediately close the End Point to prevent any further traffic from being processed.In the above example, what if Bob was under load and handling numerous WireGuard connections simultaneously? In this case, Bob would want to send a cookie to defer the Handshake process with Alice until a later date, which is mandated to at least five seconds after the initial attempt to handshake. Fortunately, this process is entirely automatic. Recall the prompt that Bob received when Alice first initialized the connection:
By providing C
or c
, Bob will not send a Response Packet, but instead a Cookie. The Cookie is a random value that changes every two minutes, hashed with Alice’s IP+Port, and further encrypted with the Bob’s public key using the original mac1
value as Additional Authenticated Data. In essence, Bob returns a cryptographic value tied to the initial Handshake between him and Alice, that Alice will promptly decrypt and store. After the timeout period, when Alice requests a second handshake, she will automatically pass that value by calculating a second MAC, stored in the mac2
section of the Packet, that is a MAC using the cookie value as the key, and the rest of the Packet as data. Bob can then verify that this mac2
address is valid, both in the context of Alice’s previous handshake, and also in the context of the random secret (As if Alice took longer than two minutes, then random secret on Bob’s server will have changed and thus invalidated the cookie).
mac2
, (IE no cookie has been sent or the sent cookie expired), they can send a cookie in response. However, if the mac2
is valid, they will continue the handshake process. This ensures that Alice will only need to wait at a maximum of five seconds, plus the time to actually complete the handshake itself.The codebase is broken up into logical files, each containing a namespace sharing the same name as the file itself. Therefore, if you see functions like wireguard::Handshake
, you know the file that this function belongs to is wireguard.h
:
wireguard.h
file contains all the functions and variables used by the WireGuard implementation, including the functions for initiating a handshake, and the structure of the various packets sent between peers.udp.h
file contains the udp::packet
, our implementation of the UDP protocol.main.cpp
file contains the main application.If you’re interested in looking at some of the auxiliary files:
crypto.h
contains the implementations of the cryptographic algorithms needed by WireGuard. They use implementations from both OpenSSL and Sodium.shared.h
contains various shared objects and functions, particularly mediated input and output.shared.h
, which provides the common definitions used by all the subsequent files. Then, move to crypto.h
to get an understanding of the underlying cryptographic functions and data structures. With that, you’ll be equipped to understand udp.h
and wireguard.h
, in that order, and can finally finish with network.h
and main.cpp
. That said, feel free to jump to whichever section most interests you, the documentation should be more the sufficient such that you don’t need to read the entire codebase to understand a particular part! The WireGuard Protocol passes data over the UDP protocol, and as such this repository provides a sample implementation of UDP for these packets to be sent over. The udp::packet
consists of three major components:
All the code related to the WireGuard implementation are available in the wireguard
namespace, and can be broadly classified into the following groups:
CONSTRUCTION
constant used to create the Chaining Key Value used to eventually derive the Transport Keys and the LABEL_MAC1
and LABEL_COOKIE
that are to create the mac1
value of the handshake packets, and the cookie respectively. Another type of constant is the REKEY
and RJECT
constants, which specify either the amount of time in seconds, or messages sent, that require a REKEY
, or an outright rejection of the connection.[!info] According to the WireGuard reference, a WireGuard server will continue to handle packets after the
REKEY
threshold has been reached, although it will continually prompt the client to re-key. Once theRJECT
threshold has exceeded, the server will refuse to handle packets until a successful re-key occurs.
config
structure that contains all the relevant information needed to communicate with the peer. For a client, this value is stored in the wireguard_server
variable in main.cpp
. For the server, this value is passed onto the newly create WireGuard Thread that manages the End Point.Rm
is a class that implements this behavior, with the cookie_random
an instance of this object that is used when creating a cookie.>[!info] >These packets are implemented as derivatives of an abstract Packet
class. This is due to the need to convert the cryto::string
used to store secrets and other values in WireGuard, to raw bytes that can be sent across the network. The Packet
class contains two methods: Packet::Serialize()
, which convert a crypto::string
packet into a string of bytes, and Packet::Expand()
which performs the inverse of Serialize
.
Handshake1
, the Initiator generates their set of ephemeral keys, ties the eventual Transport Keys to these values and their Static Keys, before sending it across to the Responder who inverses the process using the Initial Packet sent by the Initiator to arrive at a shared value C
and H
. In Handshake2
the Responder generates their own set of Ephemeral Keys, and ties both them and their Static Keys to these shared values. The Initiator preforms the inverse of the process using the Response Packet sent by the Responder, and both arrive at a set of shared Transport Keys. This communication is controlled by the Handshake
function. Handshake
also manages generation and parsing of cookies.The auxiliary cryptographic functions used by the WireGuard implementation relies on two libraries:
HASH
using BLAKE2s hashing algorithmMAC
using BLAKE2s’ keyed-hash functionalityHMAC
using HMAC-BLAKE2s.DH
and DH_GENERATE
using the Curve25519 ECC algorithm.ENCRYPT
and DECRYPT
using the ChaCha20-Poly1305 stream cipher and message authentication code.XENCRYPT
and XDECRYPT
using the XChaCha20-Poly1305 stream cipher and message authentication code.AEAD
(Authenticated Encryption with Associated Data) and XAEAD
to refer to the encryption and decryption functions using the standard ChaCha20, and extended XChaCha20 functions respectively. The Difference between ChaCha20 and its extended variant come from the increased size of the nonce value. ChaCha20 uses a 96 bit nonce, whereas XChaCha20 uses a 192 bit nonce; when a nonce is chosen at random, the latter is a stronger mode of encryption. ChaCha’s name is a delightful reference to the algorithm that it was based upon: Salsa.Additionally, there are other members of importance:
KDF
function implements the HKDF
key derivation algorithm as outlined in the WireGuard reference, utilizing the above functions.crypto::string
is a class for storing unsigned cryptographic bytes that are wiped when leaving scope.crypto::keypair
is a general purpose pair of crypto::strings
that are used both in a private/public configuration, such as the Static and Ephemeral keys, and as a unrelated collection of two values, such as the ciphertext and nonce returned by XENCRYPT
.These cryptographic functions exist within the crypto
namespace.
Another part of the WireGuard standard is the use of timestamps, to which the TAI64N format is mandated. The implementation of this format, and auxiliary functions for working with it, exist in the shared
namespace.
Code pertaining to the creation of the actual application utilizing the UDP and WG implementations is available in:
shared
namespace contains various functions and structures used throughout the program, particularly handling thread-safe input and output.main.cpp
file contains the code that drives the application.network
namespace contains the Network Thread and associated network::queue
used for sending packets across the network, discovering new peers, and maintaining existing connections.