Wireguard

De The Linux Craftsman
Aller à la navigation Aller à la recherche
Protocole udp
Port 51820
Configuration Iptables iptables -I INPUT 2 -p udp --dport 51820 -j ACCEPT

Préparation

Les basiques

Dans un premier temps, il faudra avoir une connexion à Internet, utiliser un serveur DNS et désactiver SELinux.

Pour ceux qui auraient manqué des étapes, les voici:

Assurez-vous d'avoir installé le dépôt EPEL car Wireguard vient de cette source.

Paramétrage du pare-feu

Le serveur Wireguard utilise une interface interne pour connecter les clients distants et router le trafique. Pour que le trafique des clients puissent sortir du serveur, il faut plusieurs ingrédients :

  • activer le routage :
# sysctl -w net.ipv4.ip_forward=1
# sysctl -p
  • installer iptables et le démarrer :
# dnf -y install iptables-services
# systemctl enable iptables --now

N'oubliez pas de retirer firewalld le cas échéant

  • Supprimer la règle qui interdit le routage des paquets :
# iptables -F FORWARD
  • activer le NAT sur votre interface de sortie (ici eth0) :
# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
  • ouvrir le port utilisé par Wireguard (ici 51820) :
# iptables -I INPUT 2 -p udp --dport 51820 -j ACCEPT
  • sauvegarder la configuration du pare-feu :
# service iptables save

Une fois ces étapes effectuées, entrons dans le vif du sujet !

Installation

Le noyau Linux intègre le module Wireguard et il nous faut installer les outils Wireguard pour pouvoir configurer ce module :

# dnf -y install wireguard-tools

Ce paquetage installe deux commandes :

  • wg : cette commande permet de gérer les tunnels, générer les paires de clés, etc...
  • wg-quick : cette commande permet la gestion de l'interface tun utilisé pour la connexion interne des clients VPN

Configuration serveur

Les clés

Dans le répertoire /etc/wireguard il n'y a pour l'instant rien et la première étape consiste à créer le fichier contenant la clé privée du serveur :

# wg genkey > /etc/wireguard/server.key

Cette commande génère le message suivant :

Warning: writing to world accessible file.
Consider setting the umask to 077 and trying again.

Wireguard recommande de modifier les permissions du fichier pour que tout le monde n'y ai pas accès... ce que nous allons faire :

# chmod 0400 /etc/wireguard/server.key

Maintenant que la clé privée est généré et stocké dans son fichier, nous allons générer la clé publique associée :

# cat /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub

Stockage des clés des clients

Pour stocker les clés des clients nous allons créer un répertoire clients :

# mkdir /etc/wireguard/clients

Le fichier wg0.conf

Il ne reste plus qu'a définir la configuration de notre serveur Wireguard à proprement parlé. Cela ce fait dans le fichier /etc/wireguard/wg0.conf:

[Interface]
# La clé privée contenue dans le fichier server.key
PrivateKey = iHaI5bkEbW8wqTQDbU/KITGhxtKoiezWVohVZsKwj3w=

# L'adresse privée du serveur Wireguard, les clients auront des adresses dans cette plage
Address = 10.8.0.1/24

# Le port d'écoute UDP, remplacé par ce que vous voulez
ListenPort = 51820

Démarrage du serveur

Pour démarrer notre serveur Wireguard nous allons utiliser la commande systemctl :

# systemctl enable wg-quick@wg0 --now

Vous devriez être en mesure de voir l'interface wg0 avec l'adresse IP spécifiée précédement :

# ip a sh dev wg0
3: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none 
    inet 10.8.0.1/24 scope global wg0
       valid_lft forever preferred_lft forever

On peut également voir le service en écoute sur le port UDP 51820 :

# ss -aunp
State     Recv-Q    Send-Q         Local Address:Port          Peer Address:Port    Process    
UNCONN    0         0                    0.0.0.0:51820              0.0.0.0:*                  
UNCONN    0         0                       [::]:51820                 [::]:*

Gardez en tête qu'à ce stade nous n'avons pas encore créé de client ce qui rend notre serveur inutile... pour le moment :)

Création d'un client sur le serveur

A chaque fois que vous voudrez ajouter un client à votre serveur Wireguard vous devrez :

  • générer la clé privé
# wg genkey > /etc/wireguard/clients/client1.key
  • modifier ces droits :
# chmod 0400 /etc/wireguard/clients/client1.key
  • générer la clé publique
# cat /etc/wireguard/clients/client1.key | wg pubkey > /etc/wireguard/clients/client1.pub
  • ajouter la ligne suivante dans le fichier /etc/wireguard/wg0.conf (à la fin) :
[Peer]
# La clé de client1 - client1.pub
PublicKey = iWskvxyj+QK4185xO6Z2Fb0XYo1jGyiyI3iKbyGTkT8=
# L'IP du client
AllowedIPs = 10.8.0.8/32

Il ne faut pas oublier de redémarrer le serveur :

# systemctl restart wg-quick@wg0

Fichier de configuration du client

Pour que le client puisse se connecter, il faut qu'il importe dans son client Wireguard le fichier suivant :

[Interface]
# L'adresse IP du client
Address = 10.8.0.8/32

# La clé privée du client - client1.key
PrivateKey = 4FsCdtKr9GrLiX7zpNEYeqodMa5oSeHwH/m9hsNNfEs=

# un serveur DNS
DNS = 1.1.1.1

[Peer]
# La clé publique du serveur Wireguard - server.pub
PublicKey = aK+MQ48PVopb8j1Vjs8Fvgz5ZIG2k6pmVZs8iVsgr1E=

# On route tout le trafique dans l'interface VPN (modifiez en fonction de votre besoin)
AllowedIPs = 0.0.0.0/0

# L'adresse IP publique du serveur Wireguard (habituellement celle de votre box)
Endpoint = 1.2.3.4:51820

# La durée des message keepalive en seconde
PersistentKeepalive = 25

Ce fichier doit se terminer par .conf, par exemple client1.conf.

Installation du client

Vos pouvez télécharger le client Wireguard et pour ajouter le tunnel il suffit d'importer le fichier précédent ! Sur MacOS vous pouvez envoyer votre fichier via airdrop :

Airdrop-wireguard-macos.jpg

Sur l'iPhone ou l'iPad vous aurez le prompt suivant, il faudra sélectionner Wireguard :

Prompt-import-wireguard.png

Une fois Wireguard autorisé à modifier sa configuration :

Prompt-allow-wireguard.png

Le tunnel apparait :

Wireguard-tunnel-display.png

Vérification

Une fois le client connecté, vous devriez être en mesure de le joindre depuis le serveur Wireguard :

# ping 10.8.0.8
PING 10.8.0.8 (10.8.0.8) 56(84) bytes of data.
64 bytes from 10.8.0.8: icmp_seq=1 ttl=64 time=47.0 ms
64 bytes from 10.8.0.8: icmp_seq=2 ttl=64 time=42.1 ms
64 bytes from 10.8.0.8: icmp_seq=3 ttl=64 time=156 ms

Dépannage

Handshake

Si ce n'est pas le cas vérifiez d'abord que le tunnel monte et que la poignée de main entre le serveur et le client est faite :

# wg show
interface: wg0
  public key: aK+MQ48PVopb8j1Vjs8Fvgz5ZIG2k6pmVZs8iVsgr1E=
  private key: (hidden)
  listening port: 51820

peer: iWskvxyj+QK4185xO6Z2Fb0XYo1jGyiyI3iKbyGTkT8=
  endpoint: 78.242.216.147:42525
  allowed ips: 10.8.0.1/32
  latest handshake: 1 minute, 40 seconds ago
  transfer: 8.01 KiB received, 17.34 KiB sent

...

Vous devez voir la ligne :

peer: iWskvxyj+QK4185xO6Z2Fb0XYo1jGyiyI3iKbyGTkT8=
...
latest handshake: 1 minute, 40 seconds ago
...

Si la rubrique peer ne contient pas la ligne latest handshake c'est surement à cause des clés dans le fichier de configuration du client (mauvais copié / collé)

Routage côté client

Si le tunnel est monté mais aucun trafique ne circule, vérifiez d'abord que la section allowed ips n'a pas la valeur (none) :

# wg show
interface: wg0
  ...

peer: iWskvxyj+QK4185xO6Z2Fb0XYo1jGyiyI3iKbyGTkT8=
  ...
  allowed ips: (none)

Si c'est le cas, vérifiez le AllowedIPS dans le fichier wg0.conf du serveur !

Routage côté serveur

Lorsque Wireguard est démarré, il est possible que la machine n'est plus accès au réseau :

# ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
From 10.8.0.254 icmp_seq=1 Destination Host Unreachable
ping: sendmsg: Destination address required
...

La raison est la suivante, lorsque Wireguard démarre, il modifie la table de routage de la machine pour inclure l'interface interne du VPN (wg0) :

# journalctl -fu wg-quick@wg0
...
Nov 12 15:02:05 wireguard wg-quick[533]: [#] ip -6 route add ::/0 dev wg0 table 51820
Nov 12 15:02:05 wireguard wg-quick[533]: [#] ip -6 rule add not fwmark 51820 table 51820
Nov 12 15:02:05 wireguard wg-quick[533]: [#] ip -6 rule add table main suppress_prefixlength 0
Nov 12 15:02:05 wireguard wg-quick[570]: [#] ip6tables-restore -n
Nov 12 15:02:05 wireguard wg-quick[533]: [#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820
Nov 12 15:02:05 wireguard wg-quick[533]: [#] ip -4 rule add not fwmark 51820 table 51820
Nov 12 15:02:05 wireguard wg-quick[533]: [#] ip -4 rule add table main suppress_prefixlength 0
Nov 12 15:02:05 wireguard wg-quick[533]: [#] sysctl -q net.ipv4.conf.all.src_valid_mark=1
...

La ligne

ip -4 route add 0.0.0.0/0 dev wg0 table 51820

et

ip -6 route add ::/0 dev wg0 table 51820

Ajoute une route pour utiliser l'interface wg0 et elle arrive avant la route par défaut... ce qui ne peut évidement pas fonctionner. On peut vérifier cela avec la commande suivante :

# ip route get 1.1.1.1
1.1.1.1 dev wg0 table 51820 src 10.8.0.254 uid 0 
    cache 

La commande précédente permet de voir par quelle interface le paquet sort. Un paquet en direction d'Internet devrait sortir par l'interface physique de la machine (eth0) et non par l'interface virtuelle wg0. Pour empêcher Wireguard d'altérer les tables de routage au démarrage, nous allons ajouter la ligne suivante dans le fichier /etc/wireguard/wg0.conf :

[Interface]
...
Table = off
...

Après redémarrage du service, vous devriez pouvoir pinger des adresses sur Internet !

Automatisation

On peut aller plus loin en créant un petit script en PHP pour automatiser les actions d'administration sur Wireguard :

#!/usr/bin/php
<?php

const WG_ENDPOINT = "ADRESSE_IP_PUBLIQUE_SERVEUR:51820";
const WG_DNS = '1.1.1.1';
const WG_DIR = '/etc/wireguard';
const WG_TUN = 'wg0';
const WG_CNF = WG_DIR . '/' . WG_TUN . '.conf';
const WG_PUB = WG_DIR . '/server.pub';
const WG_CLT = WG_DIR . '/clients';

main($argv);

function main(array $argv): void
{
    $action = getAction($argv);
    if ($action == 'add') {
        addPeer($argv);
    } else if ($action == 'del') {
        deletePeer($argv);
    } else if ($action == 'list') {
        listPeers(loadWgConfig());
    } else if ($action == 'conf') {
        displayPeerConfiguration($argv);
    }
    if ($action == 'add' || $action == 'del') {
        restartWg($argv);
    }
}

function getAction(array $argv): string
{
    $actions = ['add', 'del', 'list', 'conf'];
    if (sizeof($argv) < 2 || !in_array($argv[1], $actions)) {
        displayHelp();
        exit(1);
    }
    if (in_array('-h', $argv)) {
        if ($argv[1] == 'add') {
            displayAddHelp();
        } else if ($argv[1] == 'del') {
            displayDelHelp();
        } else if ($argv[1] == 'conf') {
            displayConfHelp();
        }
        exit(1);
    }
    return $argv[1];
}

/**
 * Create a peer in the present configuration
 * @param array $argv The array of arguments
 * @return void
 */
function addPeer(array $argv): void
{
    $config = loadWgConfig();
    $name = getParameter('-n', $argv);
    if (!$name) {
        $name = readline('Enter client name: ');
    }
    $allowedIPs = getParameter('-a', $argv);
    if (!$allowedIPs) {
        $allowedIPs = readline('Enter allowed IPs: ');
    }
    $privKeyFile = WG_CLT . '/' . $name . '.key';
    $pubKeyFile = WG_CLT . '/' . $name . '.pub';
    $reuse = '';
    if (in_array('-r', $argv)) {
        $reuse = 'y';
    } elseif (in_array('-d', $argv)) {
        $reuse = 'n';
    }
    if (empty($reuse)) {
        if (is_file($pubKeyFile) || is_file($privKeyFile)) {
            while (1) {
                $r = strtolower(readline("Client with this name already have keys, reuse them ? [Y/n]"));
                if (empty($r) || $r == 'y') {
                    $reuse = 'y';
                    break;
                } else if ($r == 'n') {
                    $reuse = 'n';
                    break;
                }
            }
        }
    }
    $privKey = '';
    $pubKey = '';
    if ($reuse == 'y') {
        $privKey = trim(file_get_contents($privKeyFile));
        $pubKey = trim(file_get_contents($pubKeyFile));
    } else {
        $privKey = trim(shell_exec('wg genkey'));
        $pubKey = trim(shell_exec('echo ' . $privKey . ' | wg pubkey'));
    }
    file_put_contents(WG_CLT . '/' . $name . '.key', $privKey);
    file_put_contents(WG_CLT . '/' . $name . '.pub', $pubKey);
    $raw = '# ' . $name . "\n";
    $raw .= 'PublicKey = ' . $pubKey . "\n";
    $raw .= 'AllowedIPs = ' . $allowedIPs . "\n";
    $peer['raw'] = $raw;
    $peer['name'] = $name;
    $peer['parameters']['AllowedIPs'] = $allowedIPs;
    $peer['parameters']['PublicKey'] = $pubKey;
    // Search peer with same key and replace it
    $found = false;
    foreach ($config['Peer'] as $index => $value) {
        if ($value['parameters']['PublicKey'] == $pubKey) {
            $config['Peer'][$index] == $peer;
            $found = true;
            break;
        }
    }
    // Search peer with same name and replace it
    foreach ($config['Peer'] as $index => $value) {
        if ($value['name'] == $name) {
            $config['Peer'][$index] == $peer;
            $found = true;
            break;
        }
    }
    // Peer does not exist, adding it add the end of the list
    if (!$found) {
        $config['Peer'][] = $peer;
    }
    saveConfig($config);
    echo "\n\n" . getClientConfig($peer) . "\n\n";
}

/**
 * Delete peer from present configuration
 * @param array $argv
 * @return void
 */
function deletePeer(array $argv): void
{
    $config = loadWgConfig();
    $name = getParameter('-n', $argv);
    if (!$name) {
        listPeers($config);
        $name = readline('Enter client name: ');
    }
    $deleteKey = '';
    if (in_array('-k', $argv)) {
        $deleteKey = 'y';
    } elseif (in_array('-d', $argv)) {
        $deleteKey = 'n';
    }
    if (!$deleteKey) {
        while (1) {
            $d = strtolower(readline("Delete client keys ? [Y/n]"));
            if (empty($d) || $d == 'y') {
                $deleteKey = 'y';
                break;
            } else if ($d == 'n') {
                $deleteKey = 'n';
                break;
            }
        }
    }
    $found = false;
    foreach ($config['Peer'] as $index => $peer) {
        if ($peer['name'] == $name) {
            array_splice($config['Peer'], $index, 1);
            $found = true;
            break;
        }
    }
    if (!$found) {
        echo 'Peer ' . $name . ' does not exists !' . "\n";
        exit(1);
    }
    if ($deleteKey == 'y') {
        $privKeyFile = WG_CLT . '/' . $name . '.key';
        if (is_file($privKeyFile)) {
            unlink($privKeyFile);
        }
        $pubKeyFile = WG_CLT . '/' . $name . '.pub';
        if (is_file($pubKeyFile)) {
            unlink($pubKeyFile);
        }
    }
    saveConfig($config);
}

/**
 * List all peers present in the given configuration array
 * @param array $config
 * @return void
 */
function listPeers(array $config): void
{
    if (sizeof($config['Peer']) == 0) {
        echo "No client\n";
        return;
    }
    echo "Following client(s) found: \n";
    for ($i = 0; $i < sizeof($config['Peer']); $i++) {
        echo "- " . $config['Peer'][$i]['name'] . ' @ ' . $config['Peer'][$i]['parameters']['AllowedIPs'] . "\n";
    }
}

/**
 * Display peer configuration in console
 * @param array $argv
 * @return void
 */
function displayPeerConfiguration(array $argv): void
{
    $config = loadWgConfig();
    $name = getParameter('-n', $argv);
    if (!$name) {
        listPeers($config);
        $name = readline('Enter client name: ');
    }
    foreach ($config['Peer'] as $index => $peer) {
        if ($peer['name'] == $name) {
            echo "\n\n" . getClientConfig($peer) . "\n\n";
            return;
        }
    }
    echo 'Peer ' . $name . 'does not exist !' . "\n";
    exit(1);
}

/**
 * Display peer configuration
 * @param array $peer the peer array configuration
 * @return string the client configuration
 */
function getClientConfig(array $peer): string
{
    $str = '[Interface]' . "\n";
    $str .= 'PrivateKey = ' . trim(file_get_contents(WG_CLT . '/' . $peer['name'] . '.key'));
    $str .= "\n";
    $str .= 'Address = ' . $peer['parameters']['AllowedIPs'];
    $str .= "\n";
    $str .= 'DNS = ' . WG_DNS;
    $str .= "\n\n" . '[Peer]' . "\n";
    $str .= 'PublicKey = ' . trim(file_get_contents(WG_PUB));
    $str .= "\n";
    $str .= 'AllowedIPs = 0.0.0.0/0';
    $str .= "\n";
    $str .= 'Endpoint = ' . WG_ENDPOINT;
    $str .= "\n";
    $str .= 'PersistentKeepalive = 25';
    return $str;
}

function restartWg(array $argv): void
{
    if (getParameter('-x', $argv) != FALSE) {
        shell_exec('systemctl restart wg-quick@' . WG_TUN);
    }
}

function loadWgConfig(): array
{
    if (!is_file(WG_CNF)) {
        echo 'Configuration file ' . WG_CNF . 'does not exists !';
        exit(1);
    }
    $configStr = file_get_contents(WG_CNF);
    $configArr = explode("\n", $configStr);
    $result = ['Interface' => [], 'Peer' => []];
    $currentSection = '';
    $currentPeer = -1;
    foreach ($configArr as $line) {
        if (str_starts_with($line, '[')) {
            $currentSection = trim($line, '[]');
            if ($currentSection == 'Peer') {
                $currentPeer++;
                $result[$currentSection][$currentPeer] = [];
            }
            continue;
        }
        if ($currentSection == 'Peer') {
            $result[$currentSection][$currentPeer][] = $line;
            continue;
        }
        $result[$currentSection][] = $line;
    }
    // Parse Interface
    $result['Interface'] = parseInterface($result['Interface']);
    // Parse Peers
    foreach ($result['Peer'] as $index => $value) {
        $result['Peer'][$index] = parsePeer($value);
    }
    return $result;
}

function saveConfig(array $config): void
{
    $str = '[Interface]';
    $str .= "\n";
    $str .= trim($config['Interface']['raw']);
    $str .= "\n\n";
    foreach ($config['Peer'] as $peer) {
        $str .= '[Peer]';
        $str .= "\n";
        $str .= trim($peer['raw']);
        $str .= "\n\n";
    }
    file_put_contents(WG_CNF, $str);
}

function parseInterface(array $interface): array
{
    $parsedInterface = [];
    $parsedInterface['raw'] = implode("\n", $interface);
    preg_match_all('@^(.*)? =? (.*)@m', $parsedInterface['raw'], $matches);
    for ($i = 0; $i < sizeof($matches[0]); $i++) {
        $parsedInterface['parameters'][$matches[1][$i]] = $matches[2][$i];
    }
    return $parsedInterface;
}

function parsePeer(array $peer): array
{
    $parsedPeer = [];
    $parsedPeer['raw'] = implode("\n", $peer);
    preg_match_all('@^(.*)? =? (.*)@m', $parsedPeer['raw'], $matches);
    for ($i = 0; $i < sizeof($matches[0]); $i++) {
        $parsedPeer['parameters'][$matches[1][$i]] = $matches[2][$i];
    }
    // Check if first line is commented and assume it is client's name else use ip
    if (str_starts_with($peer[0], '#')) {
        $parsedPeer['name'] = trim($peer[0], '# ');
    } else if (isset($parsedPeer['parameters']['AllowedIPs'])) {
        $parsedPeer['name'] = $parsedPeer['parameters']['AllowedIPs'];
    }
    return $parsedPeer;
}

function displayHelp(): void
{
    echo 'Wireguard manager is a script to add, delete or list clients and more';
    echo "\n\t";
    echo 'use : add, del, list or conf';
    echo "\n\t";
    echo '-h with a previous action will display a more detailed help : ';
    echo "\n\t\t";
    echo __FILE__ . ' add -h';
    echo "\n";
}

function displayAddHelp(): void
{
    echo 'You can add client silently by giving the following parameters';
    echo "\n";
    echo '-n : for the name';
    echo "\n";
    echo '-a : for the allowed IPs';
    echo "\n";
    echo '[-r|-d]: to reuse or delete keys if the client was previously created';
    echo "\n";
    echo '-qr : to display qrcode (optional)';
    echo "\n";
    echo '-x : to reboot Wireguard service (optional)';
    echo "\n\n\t";
    echo __FILE__ . ' add -n client1 -a 10.8.0.3/32 -r -x';
    echo "\n\n";
}

function displayDelHelp(): void
{
    echo 'You can delete client silently by giving the following parameters';
    echo "\n";
    echo '-n : for the name';
    echo "\n";
    echo '[-k|-d]: to keep keys for later reuse or delete them';
    echo "\n";
    echo '-x : to reboot Wireguard service';
    echo "\n\n\t";
    echo __FILE__ . ' del -n client1 -k -x';
    echo "\n\n";
}

function displayConfHelp(): void
{
    echo 'You can display client configuration by giving the following parameters';
    echo "\n";
    echo '-n : for the name';
    echo "\n";
    echo '-qr : to display qrcode (optional)';
    echo "\n\n\t";
    echo __FILE__ . ' conf -n client1 -qr';
    echo "\n\n";
}

function getParameter(string $name, array $argv): string|bool
{
    foreach ($argv as $index => $value) {
        if ($value == $name) {
            if (isset($argv[$index + 1])) {
                return $argv[$index + 1];
            }
            return TRUE;
        }
    }
    return FALSE;
}