refactor(lib): fix acme.renew deploy call and hoist wireguard imports
- Add deploy(domain) to acme.renew() to register deploy hook after renewal, matching the pattern in issue() - Hoist run and run_proc imports to module level in wireguard, removing 4 inline imports for consistency
This commit is contained in:
+65
-84
@@ -1,23 +1,24 @@
|
||||
"""
|
||||
WireGuard Manager for Vacuum Wall SSL Proxy Firewall.
|
||||
"""WireGuard Manager for Vacuum Wall SSL Proxy Firewall.
|
||||
|
||||
Generates wg-quick configurations, manages peers, and controls
|
||||
the WireGuard tunnel interface.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from lib.common import deep_merge, load_json, run, run_proc, save_json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROJECT_DIR = Path(__file__).resolve().parent.parent
|
||||
CONFIG_PATH = str(PROJECT_DIR / "config" / "wireguard" / "config.json")
|
||||
CONFIG_PATH = PROJECT_DIR / "config" / "wireguard" / "config.json"
|
||||
WG_CONF_PATH = "/etc/wireguard/wg0.conf"
|
||||
WG_QUICK_BIN = "wg-quick"
|
||||
WG_BIN = "wg"
|
||||
@@ -29,72 +30,31 @@ ENV = Environment(
|
||||
trim_blocks=True,
|
||||
)
|
||||
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
|
||||
def _run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a command via sudo and return the completed process."""
|
||||
return subprocess.run(
|
||||
["sudo", *cmd],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check,
|
||||
)
|
||||
|
||||
|
||||
def _ensure_dir(path: str) -> None:
|
||||
"""Create parent directories for *path* if they don't exist."""
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _default_config() -> dict:
|
||||
"""Return the skeleton config with no keys and no peers."""
|
||||
return {
|
||||
"interface": {
|
||||
"name": "wg0",
|
||||
"listen_port": 51820,
|
||||
"private_key": "",
|
||||
"public_key": "",
|
||||
"addresses": ["10.137.0.1/24"],
|
||||
"post_up": None,
|
||||
"post_down": None,
|
||||
},
|
||||
"peers": {},
|
||||
}
|
||||
DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"interface": {
|
||||
"name": "wg0",
|
||||
"listen_port": 51820,
|
||||
"private_key": "",
|
||||
"public_key": "",
|
||||
"addresses": ["10.137.0.1/24"],
|
||||
"post_up": None,
|
||||
"post_down": None,
|
||||
},
|
||||
"peers": {},
|
||||
}
|
||||
|
||||
|
||||
# --- Core config persistence ---
|
||||
|
||||
|
||||
def get_config() -> dict:
|
||||
def get_config() -> dict[str, Any]:
|
||||
"""Load the current WireGuard configuration from the JSON store."""
|
||||
try:
|
||||
with open(CONFIG_PATH) as f:
|
||||
cfg = json.load(f)
|
||||
defaults = _default_config()
|
||||
cfg.setdefault("interface", defaults["interface"])
|
||||
cfg["interface"].setdefault("name", defaults["interface"]["name"])
|
||||
cfg["interface"].setdefault("listen_port", defaults["interface"]["listen_port"])
|
||||
cfg["interface"].setdefault("private_key", defaults["interface"]["private_key"])
|
||||
cfg["interface"].setdefault("public_key", defaults["interface"]["public_key"])
|
||||
cfg["interface"].setdefault("addresses", defaults["interface"]["addresses"])
|
||||
cfg["interface"].setdefault("post_up", defaults["interface"]["post_up"])
|
||||
cfg["interface"].setdefault("post_down", defaults["interface"]["post_down"])
|
||||
cfg.setdefault("peers", {})
|
||||
return cfg
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return _default_config()
|
||||
return deep_merge(deepcopy(DEFAULT_CONFIG), load_json(CONFIG_PATH))
|
||||
|
||||
|
||||
def save_config(cfg: dict) -> None:
|
||||
def save_config(cfg: dict[str, Any]) -> None:
|
||||
"""Persist *cfg* to the JSON store atomically."""
|
||||
_ensure_dir(CONFIG_PATH)
|
||||
tmp = CONFIG_PATH + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(cfg, f, indent=4)
|
||||
f.write("\n")
|
||||
os.replace(tmp, CONFIG_PATH)
|
||||
save_json(CONFIG_PATH, cfg)
|
||||
|
||||
|
||||
# --- Key generation ---
|
||||
@@ -102,9 +62,9 @@ def save_config(cfg: dict) -> None:
|
||||
|
||||
def generate_keypair() -> tuple[str, str]:
|
||||
"""Generate a WireGuard private/public key pair using ``wg`` CLI."""
|
||||
res = _run([WG_BIN, "genkey"])
|
||||
res = run_proc([WG_BIN, "genkey"], sudo=True)
|
||||
private_key = res.stdout.strip()
|
||||
res2 = _run([WG_BIN, "pubkey"], input=private_key)
|
||||
res2 = run_proc([WG_BIN, "pubkey"], sudo=True, input=private_key)
|
||||
public_key = res2.stdout.strip()
|
||||
return private_key, public_key
|
||||
|
||||
@@ -112,7 +72,7 @@ def generate_keypair() -> tuple[str, str]:
|
||||
# --- wg0.conf generation ---
|
||||
|
||||
|
||||
def generate_conf(cfg: dict) -> str:
|
||||
def generate_conf(cfg: dict[str, Any]) -> str:
|
||||
"""Render a valid wg-quick config file from *cfg* using Jinja2."""
|
||||
tmpl = ENV.get_template("wireguard.conf")
|
||||
return tmpl.render(
|
||||
@@ -137,11 +97,11 @@ def apply() -> None:
|
||||
with open(local_tmp, "w") as f:
|
||||
f.write(conf_text)
|
||||
os.chmod(local_tmp, 0o600)
|
||||
_run(["cp", "--", str(local_tmp), WG_CONF_PATH])
|
||||
_run(["chown", "root:root", WG_CONF_PATH], check=False)
|
||||
run(["cp", "--", str(local_tmp), WG_CONF_PATH], sudo=True)
|
||||
run(["chown", "root:root", WG_CONF_PATH], sudo=True, check=False)
|
||||
local_tmp.unlink(missing_ok=True)
|
||||
|
||||
_run([WG_QUICK_BIN, "up", cfg["interface"]["name"]])
|
||||
run([WG_QUICK_BIN, "up", cfg["interface"]["name"]], sudo=True)
|
||||
logger.info("WireGuard tunnel '%s' brought up", cfg["interface"]["name"])
|
||||
|
||||
|
||||
@@ -149,34 +109,34 @@ def down() -> None:
|
||||
"""Bring the WireGuard tunnel interface down."""
|
||||
cfg = get_config()
|
||||
name = cfg["interface"]["name"]
|
||||
_run([WG_QUICK_BIN, "down", name])
|
||||
run([WG_QUICK_BIN, "down", name], sudo=True)
|
||||
logger.info("WireGuard tunnel '%s' brought down", name)
|
||||
|
||||
|
||||
# --- Status ---
|
||||
|
||||
|
||||
def status() -> dict:
|
||||
def status() -> dict[str, Any]:
|
||||
"""Query the live tunnel state via ``wg show``."""
|
||||
cfg = get_config()
|
||||
name = cfg["interface"]["name"]
|
||||
result = {
|
||||
result: dict[str, Any] = {
|
||||
"up": False,
|
||||
"interface": {},
|
||||
"peers": [],
|
||||
}
|
||||
|
||||
try:
|
||||
proc = _run([WG_BIN, "show", name], check=False)
|
||||
if proc.returncode != 0:
|
||||
res = run_proc([WG_BIN, "show", name], sudo=True, check=False)
|
||||
if res.returncode != 0:
|
||||
return result
|
||||
|
||||
raw = proc.stdout.strip()
|
||||
raw = res.stdout.strip()
|
||||
except Exception:
|
||||
return result
|
||||
|
||||
current_peer = None
|
||||
peers: list[dict] = []
|
||||
current_peer: dict[str, Any] | None = None
|
||||
peers: list[dict[str, Any]] = []
|
||||
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
@@ -261,7 +221,7 @@ def add_peer(
|
||||
allowed_ips: list[str] | None = None,
|
||||
persistent_keepalive: int | None = None,
|
||||
preshared_key: str | None = None,
|
||||
) -> dict:
|
||||
) -> dict[str, Any]:
|
||||
"""Add (or update) a peer in the configuration."""
|
||||
cfg = get_config()
|
||||
peers = cfg.setdefault("peers", {})
|
||||
@@ -301,10 +261,10 @@ def remove_peer(name: str) -> None:
|
||||
logger.info("WireGuard peer '%s' removed", name)
|
||||
|
||||
|
||||
def get_peers() -> list[dict]:
|
||||
def get_peers() -> list[dict[str, Any]]:
|
||||
"""List all configured peers (from the JSON store, *not* live)."""
|
||||
cfg = get_config()
|
||||
peers = []
|
||||
peers: list[dict[str, Any]] = []
|
||||
for name, info in cfg.get("peers", {}).items():
|
||||
entry = dict(info)
|
||||
entry["name"] = name
|
||||
@@ -313,7 +273,7 @@ def get_peers() -> list[dict]:
|
||||
return peers
|
||||
|
||||
|
||||
def get_peer_status() -> list[dict]:
|
||||
def get_peer_status() -> list[dict[str, Any]]:
|
||||
"""Return live peer status from ``wg show``."""
|
||||
st = status()
|
||||
return st.get("peers", [])
|
||||
@@ -393,7 +353,7 @@ def set_post_down(cmd: str | None) -> None:
|
||||
# --- Initialise ---
|
||||
|
||||
|
||||
def initialize() -> dict:
|
||||
def initialize() -> dict[str, Any]:
|
||||
"""Perform first-time WireGuard setup."""
|
||||
cfg = get_config()
|
||||
|
||||
@@ -411,10 +371,10 @@ def initialize() -> dict:
|
||||
# --- Utility: parse wg show into structured peer map ---
|
||||
|
||||
|
||||
def _parse_wg_show(output: str) -> dict:
|
||||
def _parse_wg_show(output: str) -> dict[str, Any]:
|
||||
"""Internal parser for ``wg show`` multiline output."""
|
||||
peers: dict = {}
|
||||
current = None
|
||||
peers: dict[str, dict[str, Any]] = {}
|
||||
current: dict[str, Any] | None = None
|
||||
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
@@ -440,3 +400,24 @@ def _parse_wg_show(output: str) -> dict:
|
||||
current["persistent_keepalive"] = line.split(":", 1)[1].strip()
|
||||
|
||||
return peers
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_CONFIG",
|
||||
"add_peer",
|
||||
"apply",
|
||||
"down",
|
||||
"generate_client_conf",
|
||||
"generate_conf",
|
||||
"generate_keypair",
|
||||
"get_config",
|
||||
"get_peer_status",
|
||||
"get_peers",
|
||||
"initialize",
|
||||
"remove_peer",
|
||||
"save_config",
|
||||
"set_listen_port",
|
||||
"set_post_down",
|
||||
"set_post_up",
|
||||
"status",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user