erplibre/script/install/install_vpn.sh
Mathieu Benoit 197d19d61e [ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.

Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.

--- EN ---

The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.

What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.

Assisted-by: Claude Opus 5
2026-09-04 03:42:49 +00:00

189 lines
6.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Paquets client VPN, par pilote de script/vpn/drivers/.
#
# sudo bash script/install/install_vpn.sh l2tp_ipsec
#
# Un groupe de paquets par pilote : le nom passé en argument est le `name` du
# pilote, et non un nom de paquet. C'est le CLI (script/vpn/vpn.py install)
# qui appelle ce script, et il ne connaît que les noms de pilotes.
#
# Ce script INSTALLE et ne configure rien : la configuration est rendue au
# montage du tunnel, dans un tmpfs, par le pilote. Il désactive tout de même
# le démarrage automatique des services — un strongSwan ou un xl2tpd lancé au
# boot tiendrait UDP 500/1701 et empêcherait l'instance dédiée au profil de
# s'attacher.
set -euo pipefail
log() { echo "[VPN] $*"; }
die() { echo "[VPN] ERREUR: $*" >&2; exit 1; }
usage() {
cat <<'USAGE'
Usage : sudo bash script/install/install_vpn.sh <pilote>
Pilotes connus :
l2tp_ipsec strongSwan + xl2tpd + pppd (L2TP/IPsec à clé pré-partagée)
wireguard wireguard-tools
openvpn openvpn
openconnect openconnect + vpnc-scripts
sshuttle sshuttle (sur RHEL/Rocky/Alma : dépôt EPEL requis)
Sans argument : tous les pilotes.
USAGE
}
check_root() {
[ "$(id -u)" -eq 0 ] || die "à lancer en root : sudo bash $0 $*"
}
detect_os() {
[ -f /etc/os-release ] || die "OS indéterminable (pas de /etc/os-release)"
# shellcheck disable=SC1091
. /etc/os-release
OS="${ID}"
OS_LIKE="${ID_LIKE:-}"
log "OS détecté : ${OS}"
}
family() {
case "$OS" in
ubuntu|debian|linuxmint|pop|elementary|raspbian) echo debian; return ;;
arch|manjaro|endeavouros|artix|garuda) echo arch; return ;;
fedora|rhel|centos|almalinux|rocky) echo rhel; return ;;
opensuse*|sles|sled) echo suse; return ;;
esac
case "$OS_LIKE" in
*debian*|*ubuntu*) echo debian; return ;;
*arch*) echo arch; return ;;
*rhel*|*fedora*) echo rhel; return ;;
*suse*) echo suse; return ;;
esac
die "famille de distribution inconnue : ${OS} (ID_LIKE=${OS_LIKE})"
}
# Les paquets, par pilote puis par famille. `strongswan-starter` fournit la
# commande `ipsec` et le démon starter, que le pilote L2TP utilise ; les
# paquets `charon-systemd`/`swanctl` seuls ne la fournissent PAS.
packages_for() {
local driver="$1" fam="$2"
case "${driver}:${fam}" in
# libstrongswan-standard-plugins apporte le greffon openssl, et
# avec lui 3DES. Sans ce paquet, charon ANNONCE 3DES, le
# concentrateur le choisit — c'est souvent le seul qu'il connaisse —
# et la négociation meurt sur « ENCRYPTION_ALGORITHM 3DES_CBC not
# supported! ». Mesuré sur Ubuntu 24.04 : greffons chargés sans lui,
# « aes md5 rc2 sha1 », donc pas de 3DES.
l2tp_ipsec:debian)
echo "strongswan strongswan-starter libstrongswan-standard-plugins libcharon-extra-plugins xl2tpd ppp" ;;
l2tp_ipsec:arch) echo "strongswan xl2tpd ppp" ;;
l2tp_ipsec:rhel) echo "strongswan xl2tpd ppp" ;;
l2tp_ipsec:suse) echo "strongswan xl2tpd ppp" ;;
# wireguard-tools fournit wg ET wg-quick. Le module noyau est dans
# Linux depuis 5.6 : rien à compiler sur les distributions visées.
wireguard:*) echo "wireguard-tools" ;;
openvpn:*) echo "openvpn" ;;
# vpnc-scripts porte le script que openconnect appelle pour poser
# les routes et le DNS. Sans lui, la session s'ouvre et la machine
# ne voit rien passer.
openconnect:debian) echo "openconnect vpnc-scripts" ;;
openconnect:*) echo "openconnect" ;;
sshuttle:*) echo "sshuttle" ;;
*) die "pilote inconnu : ${driver}. Voir --help." ;;
esac
}
install_packages() {
local fam="$1"; shift
log "Installation : $*"
case "$fam" in
debian)
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
# shellcheck disable=SC2086
apt-get install -y --no-install-recommends $*
;;
arch)
# shellcheck disable=SC2086
pacman -Sy --needed --noconfirm $*
;;
rhel)
# shellcheck disable=SC2086
{ command -v dnf >/dev/null && dnf install -y $*; } \
|| yum install -y $*
;;
suse)
# shellcheck disable=SC2086
zypper --non-interactive install $*
;;
esac
}
disable_autostart() {
# Un service lancé au boot tient le port et fait échouer l'instance
# dédiée au profil. On les arrête et on les désactive : le pilote
# démarre ce dont il a besoin, quand il en a besoin.
command -v systemctl >/dev/null || return 0
for unit in xl2tpd strongswan-starter strongswan ipsec; do
if systemctl list-unit-files "${unit}.service" >/dev/null 2>&1 \
&& systemctl is-enabled "${unit}.service" >/dev/null 2>&1; then
log "désactivation de ${unit}.service (le pilote le pilote)"
systemctl disable --now "${unit}.service" >/dev/null 2>&1 || true
fi
done
}
# Les binaires que chaque pilote exige, en miroir de `binaries` dans
# script/vpn/drivers/. Un paquet installé sans son binaire (nom changé,
# dépôt incomplet) doit être vu ICI, pas au premier montage.
binaries_for() {
case "$1" in
l2tp_ipsec) echo "ipsec xl2tpd pppd ip" ;;
wireguard) echo "wg wg-quick ip" ;;
openvpn) echo "openvpn ip" ;;
openconnect) echo "openconnect ip" ;;
sshuttle) echo "sshuttle ssh" ;;
esac
}
verify() {
local driver="$1" missing=""
for b in $(binaries_for "$driver"); do
command -v "$b" >/dev/null || missing="${missing} ${b}"
done
if [ -n "$missing" ]; then
die "toujours absents après installation :${missing}"
fi
log "vérifié : tout est en place pour ${driver}"
}
ALL_DRIVERS="l2tp_ipsec wireguard openvpn openconnect sshuttle"
main() {
case "${1:-}" in
-h|--help) usage; exit 0 ;;
esac
check_root "$@"
detect_os
local fam drivers
fam="$(family)"
# Sans argument : tout. C'est ce que « [8] Installer les paquets
# client » demande quand on ne choisit pas de technologie.
drivers="${*:-${ALL_DRIVERS}}"
for driver in ${drivers}; do
log "── ${driver} ──"
install_packages "$fam" "$(packages_for "$driver" "$fam")"
verify "$driver"
done
disable_autostart
log "Terminé. Monter un tunnel : ./script/vpn/vpn.py up --profile <nom>"
}
main "$@"