Script per la gestione della configurazione di una vpn wireguard

vpn

Cos'è wireguard e come si configura, lo sappiamo ma mi serviva qualcosa che mi permettesse di fare provisioning e deprovisioning dei certificati in maniera più semplice di quanto già si faccia con i tool disponibili nella userland, wg e wg-quick.

E questo è il motivo principale per cui è nato questo script.

Prende come riferimento una configurazione road warrior, il primo scenario di una topologia point-to-site, quella più comune.

Alcune considerazioni

  1. Lo script definisce una workdir dove deposita tutte le configurazioni e i certificati. Le precondizioni sono legate alla presenza dei tool userland wg-tools (ovviamente) e di ufw. Vengono comunque verificate nello script.

  2. Altra semplificazione è data dal fatto di allocare una /24 come rete wireguard. 254 host per una rete domestica sono più che sufficienti.

  3. L'allocazione degli ip viene fatta con un progressivo memorizzato nel file 'ip_renew'. Gli ip dei certificati revocati vengono mantenuti in un file, 'ip_release'. Quando si crea un nuovo certificato, si controlla prima che ci sia un ip da recuperare in ip_release. Se la lista è vuota si ricorre al progressivo.

  4. La creazione della configurazione lato server passa da due file di configurazione: una configurazione globale ottenuta tracciando l'evoluzione iniziale in termini di aggiunta o rimozione dei client peer e destinata al rilascio in esecuzione. L'altra contenente la sola configurazione del server e usata unicamente per la rigenerazione della configurazione globale a partire dai peer esistenti.

  5. L'operazione di init è propedeutica per addclient, removeclient, rebuild, deploy, share.

Le operazioni a disposizione sono:

Per tutto il resto, i commenti nel codice sono abbastanza esplicativi.

Lo script

#!/bin/bash

# Questo è il file di configurazione globale, i dati sono fittizi.
# Può essere mantenuto nello script o nella home dell'utente 
# (es. $HOME/.config/wg_man.conf) e importato 
# con 'source' all'inizio.
WG_INTERFACE=wg0
PHYSICAL_INTERFACE=eth0
NETWORK="192.168.15.0"
SUBNET_MASK="24"
SERVER_IP_ADDRESS="192.168.15.1"
DNS="1.1.1.1"
LISTEN_PORT="51820"
ENDPOINT=wireguard.nodns.net:51820"
WORKDIR="$HOME/wireguard"



check_dependency() {
    if test -e /usr/sbin/ufw; then
        if test -e /usr/bin/wg; then
            return 0
        else
            return 2
        fi
    else
        return 1
    fi
}



is_init() {
    test -e ${WORKDIR}/conf.d/server_wg.conf && return 0 || return 1
}



# Genera la chiave privata
gen_priv_key() {
    wg genkey | tee ${WORKDIR}/keys/$1_privkey
}



# Genera la pre-shared key
gen_client_psk() {
    # Evita il warning sui permessi del file che devono essere 600
    umask 077
    wg genpsk | tee ${WORKDIR}/psk/$1_psk
}



# Rilascio di un nuovo indirizzo ip. Questa funziona viene invocata da
# addclient().
#
# I primi 3 ottetti li ottengo dalla variabile globale NETWORK.
# L'ultimo ottetto, relativo all'host, lo ricavo dalla lista degli ip
# riutilizzabili ("ip_release").
#
# Se non è vuota, prelevo il primo e lo rimuovo dalla lista. Se è vuota,
# prelevo l'ultimo ottetto da "ip_renew" e lo incremento per il successivo
# assegnamento.
ip_renew() {
    # Selezioni i primi 3 ottetti del network
    SUB_IP=$(echo ${NETWORK}|awk -F "." '{print $1"."$2"."$3}')

    if [[ ! -e ${WORKDIR}/conf.d/ip_release || ! -s ${WORKDIR}/conf.d/ip_release ]]; then
        # Genero il nuovo ip (Incremento di 1 l'ultimo ottetto)
        IP=$(cat ${WORKDIR}/conf.d/ip_renew)
        echo $((++IP)) > ${WORKDIR}/conf.d/ip_renew

        # Restituisco i 3 ottetti + il quarto
        #echo -n ${SUB_IP};cat ${WORKDIR}/conf.d/ip_renew
    else
        IP=$(head -n 1 ${WORKDIR}/conf.d/ip_release)
        sed -i "1d" ${WORKDIR}/conf.d/ip_release
    fi
    echo -n ${SUB_IP}"."${IP}
}



# Riallocazione di un indirizzo ip. Questa funzione viene invocata da
# removeclient().
# Aggiungo l'ultimo ottetto alla lista degli ip riallocabili.
# @par 1: ultimo ottetto.
ip_release() {
    echo $1|cut -d"." -f 4 >> ${WORKDIR}/conf.d/ip_release
}



# Aggiunge il nuovo peer al file di configurazione del server
# @par 1: CLIENT_PUBLIC_KEY
# @par 2: CLIENT_PSK
# @par 3: CLIENT_IP_ADDRESS
# @par 4: CLIENT_NAME
add_peer_to_server() {
    cat >> ${WORKDIR}/conf.d/server_wg.conf << EOF
# $4
[Peer]
PublicKey = $1
PresharedKey = $2
AllowedIPs = $3/32
PersistentKeepalive = 25

EOF
}



# Inizializza la configurazione del server wireguard.
#
# Questa operazione può essere fatta una sola volta a meno che non si
# cancelli tutta la workdir.
#
# Verranno creati due file  di configurazione: server_wg.conf e 
# server_wg_raw.conf
#
# Il primo conterrà la configurazione completa del server e dei peer,
# verrà aggiornato ad ogni modifica dei client (aggiunta o rimozione) e
# verrà usato per il deploy della configurazione.
#
# Il secondo conterrà la configurazione del solo server e verrà usato
# solo per rigenerare il primo file di configurazione.
init() {
    # Controllo che wireguard sia almeno abilitato
    echo -n "Inserire la password di Amministratore: "; read -s PASSWORD
    sudo -k
    if echo $PASSWORD|sudo -S echo "got a root" > /dev/null; then
        if ! sudo systemctl is-enabled --quiet wg-quick@${WG_INTERFACE}; then
            echo -en "\nAttivazione servizio wireguard... "
            #sudo systemctl enable --quiet wg-quick@${WG_INTERFACE}
            echo "fatto."
        fi
        sudo -k 
        
        #Verifico che queste cartelle esistano. Se lo sono, non faccio nulla, se no le creo.
        echo -n "Creazione workdir... "
        [[ ! -e ${WORKDIR}/conf.d || ! -e ${WORKDIR}/psk  || ! -e ${WORKDIR}/keys ]] \
            && { mkdir -p ${WORKDIR}/{conf.d,keys,psk}; } \
            || { echo "inizializzazione già effettuata."; exit; }
        echo "fatto."
        
        # Genero la chiave privata per il server
        echo -n "Creazione chiave private del server..."
        SERVER_PRIV_KEY=$(gen_priv_key "server_wg")
        echo "fatto."

        # Genero il file di configurazione
        echo -n "Inizializzazione del server... "
        cat > ${WORKDIR}/conf.d/server_wg.conf << EOF
[Interface]
Address = ${SERVER_IP_ADDRESS}/${SUBNET_MASK}
# SaveConfig = true

PreUp = ufw route allow in on ${WG_INTERFACE} out on ${PHYSICAL_INTERFACE} log from ${NETWORK}/${SUBNET_MASK} to 0.0.0.0/0
PostDown = ufw route delete allow in on ${WG_INTERFACE} out on ${PHYSICAL_INTERFACE} log from ${NETWORK}/${SUBNET_MASK} to 0.0.0.0/0

# IP masquerading
PreUp = iptables -t nat -A POSTROUTING -o ${PHYSICAL_INTERFACE} -s ${NETWORK}/${SUBNET_MASK} -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o ${PHYSICAL_INTERFACE} -s ${NETWORK}/${SUBNET_MASK} -j MASQUERADE

# IP filtering
PreUp = ufw allow in on ${PHYSICAL_INTERFACE} log to 0.0.0.0/0 app Wireguard
PostDown = ufw delete allow in on ${PHYSICAL_INTERFACE} log to 0.0.0.0/0 app Wireguard

ListenPort = ${LISTEN_PORT}
PrivateKey = ${SERVER_PRIV_KEY}

EOF
        echo "fatto."
        cp ${WORKDIR}/conf.d/server_wg.conf ${WORKDIR}/conf.d/server_wg_raw.conf
        # Inizializza l'erogatore di ip
        echo 1 > ${WORKDIR}/conf.d/ip_renew
    else
        echo "Sono necessarie le credenziali di amministrazione per eseguire questo comando."
        exit 1
    fi
}



# Genera un certificato per il client composto da:
# * chiave privata del client
# * chiave pre-condivisa
# * client ip
# * chiave pubblica del server
# * nome simbolico del client
#
# Dopo la creazione e il salvataggio in <WORKDIR>/conf.d, verrà aggiunto
# il relativo peer nella configurazione del server (server_wg.conf).
# @par_1 @par_2 @par_3...: lista di nomi relativi ai certificati da creare.
add_client() {
    if is_init; then
        # Se non c'è alcun input, esco.
        if [[ $# -lt 1 ]]; then
            echo -e "Devi inserire almeno un nome."
            exit 1
        else
            # Per ogni nome presente in input, creo una configurazione 
            # client e l'aggiungo alla configurazione del server.
            for CLIENT_NAME in "$@"; do
                echo -n "Creazione certificato per il client \"${CLIENT_NAME}\"... "
                
                # Controllo se la configurazione esista già discriminando
                # in base al nome del file.
                if [[ -e ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf ]]; then
                    echo "già esistente."
                else
                    # Creo la configurazione del client
                    CLIENT_PRIV_KEY=$(gen_priv_key ${CLIENT_NAME})
                    CLIENT_PUBLIC_KEY=$(echo ${CLIENT_PRIV_KEY} | wg pubkey)
                    CLIENT_PSK=$(gen_client_psk ${CLIENT_NAME})
                    CLIENT_IP_ADDRESS=$(ip_renew)
                    SERVER_PUBLIC_KEY=$(grep PrivateKey ${WORKDIR}/conf.d/server_wg.conf | cut -d " " -f 3 | tr -d " " | wg pubkey)
                    CLIENT_NAME_UPPER=$(echo ${CLIENT_NAME} | tr '[:lower:]' '[:upper:]')
                    cat > ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf << EOF
    # ${CLIENT_NAME_UPPER}
    [Interface]
    Address = ${CLIENT_IP_ADDRESS}/${SUBNET_MASK}
    DNS = ${DNS}
    PrivateKey = ${CLIENT_PRIV_KEY}

    [Peer]
    PublicKey = ${SERVER_PUBLIC_KEY}
    PresharedKey = ${CLIENT_PSK}
    AllowedIPs = 0.0.0.0/0
    Endpoint = ${ENDPOINT}
    PersistentKeepalive = 25
EOF
                    echo "fatto."
                    echo -n "Aggiunta peer \"${CLIENT_NAME}\" al file di configurazione del server... "
                    
                    # Aggiungo il peer alla configurazione del server.
                    add_peer_to_server ${CLIENT_PUBLIC_KEY} ${CLIENT_PSK} ${CLIENT_IP_ADDRESS} ${CLIENT_NAME_UPPER}
                    echo -e "fatto.\n"                
                fi
            done
        fi
    else
        echo "È necessario inizializzare il server con 'wg_init.sh init'."
        echo "L'operazione richiesta non sarà completata"
    fi
}



# Rimuove file di configurazioni, chiavi e pre-shared key realtive a file
# dati in input.
# Rimuove inoltre il peer dal file di configurazione server_wg.conf.
# @par_1 @par_2 @par_3...: lista di nomi relativi ai certificati da rimuovere.
remove_client() {
    if is_init; then
        if [[ $# -lt 1 ]]; then
            echo -e "Devi inserire almeno un nome."
            exit 1
        else
            for CLIENT_NAME in "$@"; do
                echo -n "Rimozione client \"${CLIENT_NAME}\"... "
                if [[ ! -e ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf ]]; then
                    echo "non esistente."
                else
                    # Estraggo l'ultimo ottetto dell'ip del client.
                    IP=$(grep "Address" ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf|cut -d " " -f 3|tr -d " "|cut -d "/" -f 1)
                    
                    # Lo inserisco nella lista degli ip riallocabili.
                    ip_release ${IP}
                    
                    # Cancello file di configurazione, chiave e psk del client.
                    rm ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf ${WORKDIR}/keys/${CLIENT_NAME}_privkey ${WORKDIR}/psk/${CLIENT_NAME}_psk
                    echo "terminata con successo."
                    echo -n "Rimozione peer \"${CLIENT_NAME}\" dal file di configurazione del server... "
                    
                    # Rimuovo il peer dalla configurazione del server.
                    CLIENT_NAME_UPPER=$(echo ${CLIENT_NAME} | tr '[:lower:]' '[:upper:]')
                    sed -i "/# ${CLIENT_NAME_UPPER}/,+6d" ${WORKDIR}/conf.d/server_wg.conf
                    echo -e "terminata con successo.\n"   
                fi
            done
        fi
    else
        echo "È necessario inizializzare il server con 'wg_init.sh init'."
        echo "L'operazione richiesta non sarà completata"
    fi
}



rebuild() {
    if is_init; then
        # Rigenero il file di configurazione del server da server_wg_raw.conf.
        echo -n "Ripristino del file di configurazione del server... "
        cat ${WORKDIR}/conf.d/server_wg_raw.conf > ${WORKDIR}/conf.d/server_wg.conf
        echo "fatto."
        
        # Per ogni file di configurazione client, aggiungo il relativo peer
        # nel file di configurazione server_wg.conf.
        for FILE_CONF in ${WORKDIR}/conf.d/client_*.conf; do
            CLIENT_PUBLIC_KEY=$(grep "PrivateKey" ${FILE_CONF}|cut -d " " -f 3|tr -d " " | wg pubkey)
            CLIENT_PSK=$(grep "PresharedKey" ${FILE_CONF}|cut -d " " -f 3|tr -d " ")
            CLIENT_IP_ADDRESS=$(grep "Address" ${FILE_CONF}|cut -d " " -f 3|tr -d " "|cut -d "/" -f 1)
            CLIENT_NAME_UPPER=$(grep "#" ${FILE_CONF} | cut -d " " -f 2)
            echo -n "Aggiunta peer \"$(echo ${CLIENT_NAME_UPPER} | tr '[:upper:]' '[:lower:]')\"... "
            add_peer_to_server ${CLIENT_PUBLIC_KEY} ${CLIENT_PSK} ${CLIENT_IP_ADDRESS} ${CLIENT_NAME_UPPER}
            echo -e "fatto."
        done
    else
        echo "È necessario inizializzare il server con 'wg_init.sh init'."
        echo "L'operazione richiesta non sarà completata"
    fi
}



# Avvia il server wireguard con la nuova configurazione
deploy() {
    if is_init; then
        echo -n "Inserire la password di Amministratore: "; read -s PASSWORD
        sudo -k
        if echo $PASSWORD|sudo -S echo "got a root" > /dev/null; then
   
            # Stoppo il servizio wireguard
            echo -ne "\nStop wireguard... "
            sudo systemctl stop wg-quick@${WG_INTERFACE}
            echo "fatto."
            
            # Aggiorno la configurazione
            echo -n "Copia della nuova configurazione... "
            sudo cp ${WORKDIR}/conf.d/server_wg.conf /etc/wireguard/${WG_INTERFACE}
            echo "fatto."
            
            # Riavvio il servizio wireguard
            echo -n "Riavvio wireguard... "
            sudo systemctl start wg-quick@${WG_INTERFACE}
            echo "fatto."
            
            # Revoco i privilegi di amministrazione
            sudo -k
        else
            echo "Sono necessarie le credenziali di amministrazione per eseguire questo comando."
            exit 1
        fi
    else
        echo "È necessario inizializzare il server con 'wg_init.sh init'."
        echo "L'operazione richiesta non sarà completata"
    fi
}



# Crea il qr-code della configurazione del client nella cartella corrente.
share() {
    if is_init; then
        if [[ $# -lt 1 ]]; then
            echo -e "Devi inserire almeno un nome."
            exit 1
        else
            for CLIENT_NAME in "$@"; do
                echo -n "Creazione qr-code per il client \"${CLIENT_NAME}\"... "
                qrencode -r ${WORKDIR}/conf.d/client_${CLIENT_NAME}.conf -o qrcode_client_${CLIENT_NAME}.jpg
                echo "fatto."
            done
        fi
    else
        echo "È necessario inizializzare il server con 'wg_init.sh init'."
        echo "L'operazione richiesta non sarà completata"
    fi
}



help() {

[[ $1 != "" && $1 != "help" ]] && echo -e "Comando inesistente."

    cat<<EOF
Usa come: ./wg_man.sh [command] ARG
dove:
    [command]
        init                     : Inizializza la configurazione del server.
        addclient <lista nomi>   : Aggiunge i client indicati in <lista nomi>.
        removeclient <lista nomi>: Rimuove i client indicati in <lista nomi>.
        rebuild                  : Ricostruisce il file di configurazione
                                   del server partendo dalle configurazioni
                                   di tutti i client registrati.
        deploy                   : Riavvia il server wireguard con l'ultima
                                   configurazione disponibile.
        share <lista nomi>       : Genera i qr-code relativi ai client indicati in <lista nomi>.
        help                     : Stampa questa pagina di help.

ESEMPI:
    INiZIALIZZA IL SERVER WIREGUARD
    wg_man.sh init

    AGGIUNGE I CLIENT 'macbook_pro', 'mobile'
    wg_man.sh addclient macbook_pro mobile

    RIMUOVE I CLIENT 'iphone_luca', 'workstation'
    wg_man.sh removeclient iphone_luca workstation

    RICOSTRUISCE LA CONFIGURAZIONE DEL SERVER WIREGUARD
    wg_man.sh rebuild
    
    ATTIVA LA NUOVA CONFIGURAZIONE
    wg_man.sh deploy

    GENERA I QRCODE PER 'pc_ufficio', 'laptop'
    wg_man.sh share pc_ufficio laptop
EOF
}



main() {
    case $1 in
        init         ) init ;;
        addclient    ) shift; add_client "$@" ;;
        removeclient ) shift; remove_client "$@" ;;
        rebuild      ) rebuild ;;
        deploy       ) deploy ;;
        share        ) shift; share "$@" ;;
        *            ) help ;;
    esac
}



check_dependency
case $? in
    "0" ) main $* ;;
    "1" ) echo -e "UFW non presente. Installare UFW."; exit 1 ;;
    "2" ) echo -e "wireguard-tools non presenti. È necessario installarli."; exit 1 ;;
esac

#bash #wireguard #psk #chiavi #certificati #crittografia