Wireguard
Wireguard (https://www.wireguard.com/) is a tool to create a tunnel network (or Virtual Private Network) between any machine. It is available on linux, windows, macos, android, etc. Running at the kernel level in linux machines it requires very few resources and almost no overhead.
Why use it?
It allows you to create on top of another network (such as the Internet) a virtual private LAN where you can access machines via a LAN IP such as 10.10.10.0/24
It also allows to reach machines that are behind a NAT and would not be publicly available through one machine that is publicly available and will act as a server. More details here: https://ghost.dorsk.dev/network/
Installation
Setting up on a linux machine only requires to
sudo apt-get install wireguard-toolsThis gives you access to a cli tool:
sudo wgAnd to a service:
sudo systemctl enable wg-quick@wg0.service
sudo systemctl start wg-quick@wg0.serviceThe related configuration is located at /etc/wireguard/wg0.conf
Configuration
Configuration might be confusing at first but it is very simple.
If you want a fully managed solution there is https://tailscale.com/
If you want an online tool to generate the config you can use https://www.wireguardconfig.com/
Or if you prefer to do it manually, you can keep reading.
The minimum useful topology would be 2 peers. If trying to access machines through a NAT the usual basic configuration would be 1 server and 1 peer, with the server being publicly reachable and the peer not.
The config file is composed of minimum two sections, one defines the local peer or Interface and the other one defines the distant peers Peer.
Taking the example of a local peer we would define as the server:
[Interface]
# server-machine
Address = 10.10.10.1/24
ListenPort = 51820
PrivateKey = <PRIVATE_KEY_SERVER>
# PublicKey = <PUBLIC_KEY_SERVER>
[Peer]
# home-machine-1
PublicKey = <PUBLIC_KEY_1>
# PresharedKey = <PRESHARED_KEY_1>
AllowedIPs = 10.10.10.2/32In the Interface block we have:
- Address This is the IP of this server-machine on the wireguard LAN
- ListenPort The UDP port other peers will use to connect to this machine
- PrivateKey The private key that the server-machine uses to sign
The PublicKey is not necessary here but should be given to other peers so they can confirm this machine signature.
In the Peer block (repeated for as many machines as we want on this LAN) we have:
- PublicKey the public key of the remote peer home-machine-1
- PresharedKey is completely optional, it adds a layer of security. It requires to have it set in home-machine-1 configuration as well.
Now for a remote peer that would connect as a client to this server we would have an almost identical but reverse configuration:
[Interface]
# home-machine-1
Address = 10.10.10.2/24
ListenPort = 51820
PrivateKey = <PRIVATE_KEY_1>
# Table = off
# PublicKey = <PUBLIC_KEY_1>
[Peer]
# server
PublicKey = <PUBLIC_KEY_SERVER>
# PresharedKey = <PRESHARED_KEY_1>
AllowedIPs = 10.10.10.0/24
Endpoint = <PUBLIC_IPV4>:51820
PersistentKeepalive = 15This time we have:
- Address This is the IP of this home-machine-1 on the wireguard LAN
- ListenPort is optional, it defines the port the machine listens to.
- PrivateKey The private key that the home-machine-1 uses to sign
The PublicKey is not necessary here but should be given to other peers, in this example to server-machine so they can confirm this machine signature.
- Table = off is only for linux machines to disable automatically created iptable rules.
And for the Peer block:
- PublicKey of the server
- PresharedKey only if we used one for the previous configuration
- AllowedIPs all the machines that are allowed to connect to this machine and that this machine can connect to. This can be a CIDR like here to all all machines of the wireguard LAN or it can be limited to just one machine with 10.10.10.1/32
- Endpoint with the port is only necessary to connect from a client to the server or for a full peer to peer network where both discover each other.
- PersistentKeepalive is to keep the connection open from a client to the server.
Manual configuration
First thing we need is to generate some PublicKey / PrivateKey pairs as well as optionally some PresharedKey to use.
This can be done with the wg cli tool:
privatekey=$(wg genkey); echo "privatekey: $privatekey" && echo "publickey: $(echo $privatekey | wg pubkey)"
echo "optional preshared key 1: $(wg genpsk)"
echo "optional preshared key 2: $(wg genpsk)"
echo "optional preshared key 3: $(wg genpsk)"It will generate something like this (don't use those values) which we can use for our server-machine:
privatekey: qJJKBp71t5hNpmg17x4eM1wdKTBQYZmXn7K6SbsJxXE=
publickey: 2dFYzqJYry1xMlkLPAxuMBth5fM2rN4pQ0xMJM1VhGk=
optional preshared key 1: UEcLwFQNurwm47ivvTGnSWwSx0+trXu6GWouoEIDuz0=We need key pairs for each machine of the network, so to continue our example let's generate some for the home-machine-1 (again don't use those):
privatekey: cG3xzOTzCmEwgRiK2JwlW9kazOyFaOD2T3lvUEjDJEk=
publickey: /pJKtriT5pXbWYoy+TwXAsMwczEzIBizpOdP9CiDOGU=And we need to place these values in the configuration we saw above in the place of the <PLACEHOLDER> values.
Supposing the server public IPv4 is 1.1.1.1 , the end result config should be, on the server:
[Interface]
# server
Address = 10.10.10.1/24
ListenPort = 51820
PrivateKey = qJJKBp71t5hNpmg17x4eM1wdKTBQYZmXn7K6SbsJxXE=
# PublicKey = 2dFYzqJYry1xMlkLPAxuMBth5fM2rN4pQ0xMJM1VhGk=
[Peer]
# home-machine-1
PublicKey = /pJKtriT5pXbWYoy+TwXAsMwczEzIBizpOdP9CiDOGU=
PresharedKey = UEcLwFQNurwm47ivvTGnSWwSx0+trXu6GWouoEIDuz0=
AllowedIPs = 10.10.10.2/32
And for the client:
[Interface]
# home-machine-1
Address = 10.10.10.2/24
ListenPort = 51820
PrivateKey = cG3xzOTzCmEwgRiK2JwlW9kazOyFaOD2T3lvUEjDJEk=
# Table = off # Only for a linux machine where wireguard might create iptable rules
# PublicKey = /pJKtriT5pXbWYoy+TwXAsMwczEzIBizpOdP9CiDOGU=
[Peer]
# server
PublicKey = 2dFYzqJYry1xMlkLPAxuMBth5fM2rN4pQ0xMJM1VhGk=
PresharedKey = UEcLwFQNurwm47ivvTGnSWwSx0+trXu6GWouoEIDuz0=
AllowedIPs = 10.10.10.0/24
Endpoint = 1.1.1.1:51820
PersistentKeepalive = 15
Connectivity can be confirmed by running
sudo wgWhich should print some statistics like:
peer: 2dFYzqJYry1xMlkLPAxuMBth5fM2rN4pQ0xMJM1VhGk=
preshared key: (hidden)
endpoint: 1.1.1.1:51820
allowed ips: 10.10.10.0/24
latest handshake: 35 seconds ago
transfer: 80.10 MiB received, 144.52 MiB sent
persistent keepalive: every 15 secondsSemi-auto configuration
To update the configuration for multiple peers and as an alternative to https://www.wireguardconfig.com/ I wrote the below script which helps auto generate the wg0.conf files for manually generated keys as above.
Just fill the server and peer configs and run python wireguard.py or however you named the file to generate the config files.
import textwrap
from dataclasses import dataclass
from pathlib import Path
LISTEN_PORT = 51820 # Choose anything non conflicting
def cidr(ipv4: str) -> str:
return ".".join(ipv4.split(".")[:3] + ["0"])
@dataclass
class WireguardConfig:
ipv4: str
name: str
private_key: str
public_key: str
@dataclass
class WireguardPeer(WireguardConfig):
preshared_key: str
endpoint: str | None = None
def generate_peer_interface(self) -> str:
return f"""
[Interface]
# {self.name}
Address = {self.ipv4}/24
ListenPort = {LISTEN_PORT}
PrivateKey = {self.private_key}
# Table = off # Only for a linux machine where wireguard might create iptable rules
# PublicKey = {self.public_key}
"""
def generate_peer_for_server(self) -> str:
return f"""
[Peer]
# {self.name}
PublicKey = {self.public_key}
PresharedKey = {self.preshared_key}
AllowedIPs = {self.ipv4}/32
"""
def generate_peer_for_peer(self) -> str:
return f"""
[Peer]
# {self.name}
Endpoint = {self.endpoint}:{LISTEN_PORT}
PublicKey = {self.public_key}
AllowedIPs = {self.ipv4}/32
"""
@dataclass
class WireguardServer(WireguardConfig):
endpoint: str
def generate_server_interface(self) -> str:
return f"""
[Interface]
# {self.name}
Address = {self.ipv4}/24
ListenPort = {LISTEN_PORT}
PrivateKey = {self.private_key}
# PublicKey = {self.public_key}
"""
def generate_server_peer(self, preshared_key: str) -> str:
return f"""
[Peer]
# {self.name}
PublicKey = {self.public_key}
PresharedKey = {preshared_key}
AllowedIPs = {cidr(self.ipv4)}/24
Endpoint = {self.endpoint}:{LISTEN_PORT}
PersistentKeepalive = 15
"""
server = WireguardServer(
endpoint="<PUBLIC_IPV4>",
ipv4="10.10.10.1",
name="server",
private_key="<PRIVATE_KEY_SERVER>",
public_key="<PUBLIC_KEY_SERVER>",
)
peers = [
WireguardPeer(
endpoint="192.168.11.2",
ipv4="10.10.10.2",
name="home-machine-1",
preshared_key="<PRESHARED_KEY_1>",
private_key="<PRIVATE_KEY_1>",
public_key="<PUBLIC_KEY_1>",
),
WireguardPeer(
endpoint="192.168.11.3",
ipv4="10.10.10.3",
name="home-machine-2",
preshared_key="<PRESHARED_KEY_2>",
private_key="<PRIVATE_KEY_2>",
public_key="<PUBLIC_KEY_2>",
),
]
def main() -> None:
# Generate the server with all peers
with Path(f"{server.name}.conf").open("w") as f:
f.write(textwrap.dedent(server.generate_server_interface()))
for peer in peers:
f.write(textwrap.dedent(peer.generate_peer_for_server()))
# Generate each peer with only the server
for peer in peers:
with Path(f"{peer.name}.conf").open("w") as f:
f.write(textwrap.dedent(peer.generate_peer_interface()))
f.write(textwrap.dedent(server.generate_server_peer(peer.preshared_key)))
# Also add each peer to each other peer if they are in the same LAN
for other_peer in peers:
if other_peer == peer:
continue
if not peer.endpoint or not other_peer.endpoint or cidr(peer.endpoint) != cidr(other_peer.endpoint):
continue
f.write(textwrap.dedent(other_peer.generate_peer_for_peer()))
if __name__ == "__main__":
main()