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-tools

This gives you access to a cli tool:

sudo wg

And to a service:

sudo systemctl enable wg-quick@wg0.service
sudo systemctl start wg-quick@wg0.service

The 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/32

In 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 = 15

This 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 wg

Which 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 seconds

Semi-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()