Setting routes and dealing more robustly with privileged operations

- Created commands.sh, which is now responsible for all operations that
   require root permissions: addin/deleting routes and reloading wireguard

 - Added creation and deletion of routes when creating or deleting a client.
   This *feels* like a bug in wg-quick, considering that the routes are
   created/deleted as expected on start/stop. Reload informs wireguard of a
   peer's existence but fails to set routes. For now, this is
   a suitable workaround.
This commit is contained in:
Erik Thuning 2025-02-27 14:53:35 +01:00
parent 2a3e529111
commit bc44648c30
2 changed files with 80 additions and 7 deletions

@ -2,6 +2,7 @@ from datetime import datetime
from pathlib import Path
from textwrap import dedent
from time import sleep
import configparser
import ipaddress
import json
import subprocess
@ -99,6 +100,18 @@ def run_wg(*args, input: str=None):
text=True)
return result.stdout
def run_command(command, *args) -> None:
command_path = Path().join('command.sh')
subprocess.run(['sudo', command_path.absolute(), command, *args])
def create_route(client_ip: ipaddress) -> None:
run_command('add', f'{client_ip}/32')
def delete_route(client_ip: str) -> None:
# We don't add /32 to the ip because it is already included
# from the client config
run_command('del', client_ip)
class WireGuard:
def __init__(self, config: dict):
@ -185,13 +198,16 @@ class WireGuard:
in self.user_base.glob(f'*{confsuffix}')]
def generate_config_files(self, *args) -> None:
call_with_lock(self._unsafe_generate_config_files, args, 10)
client_ip = call_with_lock(self._unsafe_generate_config_files,
args,
10)
create_route(client_ip)
def _unsafe_generate_config_files(self,
config_id: str,
name: str,
description: str,
creation_time: datetime) -> None:
creation_time: datetime) -> ipaddress:
client_privkey, client_pubkey = generate_keypair()
client_ip = self.get_free_ip()
with open(self.config_filepath(config_id), 'x') as cf, \
@ -216,6 +232,7 @@ class WireGuard:
client_ip,
client_pubkey))
self.wg_updated = True
return client_ip
def update_config(self,
config_id: str,
@ -241,13 +258,21 @@ class WireGuard:
'data': configdata}
def delete_config(self, config_id: str) -> None:
paths = [self.config_filepath(config_id),
config_path = self.config_filepath(config_id)
paths = [config_path,
self.serverconfig_filepath(config_id),
self.meta_filepath(config_id)]
for path in paths:
if not path.exists():
raise FileNotFoundError(path)
config = configparser.ConfigParser()
config.read(config_path)
client_ip = config['Interface']['Address']
[path.unlink() for path in paths]
delete_route(client_ip)
self.wg_updated = True
def update(self) -> None:
@ -257,8 +282,5 @@ class WireGuard:
sf.write(self.generate_server_config())
# Sync updated settings to interface
subprocess.run(['sudo',
'/usr/bin/systemctl',
'reload',
f'wg-quick@{self.tunnel_id}.service'])
run_command('reload')
return

51
commands.sh Normal file

@ -0,0 +1,51 @@
#!/bin/bash
set -eu
set -o pipefail
running_dir="$(dirname "$(readlink -f "$0")")"
wg_interface="$(grep 'tunnel_id' "$running_dir/config.ini" \
| sed -r 's/[^=]+=\s*//')"
assist() {
cat <<EOF
Usage: $0 <action> [<ip>]
Actions:
add <ip> Add a route to the tunnel for this ip
del <ip> Remove a route to the tunnel for this ip
reload Sync running tunnel config with configuration on disk
EOF
exit "$1"
}
add() {
ip route add "$1" dev "$wg_interface" scope link
}
del() {
ip route del "$1" dev "$wg_interface" scope link
}
reload() {
systemctl reload "wg-quick@$wg_interface.service"
}
if [ "$#" = 0 ]; then
assist 0
fi
action="$1"
shift
case "$action" in
add )
add "$1"
;;
del )
del "$1"
;;
reload )
reload
;;
esac