Refactor wireguard to use a different config dir instead of /etc/wireguard
This commit is contained in:
@@ -15,7 +15,7 @@ import type { TableType } from "@/src/types";
|
||||
|
||||
const {
|
||||
WGUI_LIB_CLIENTS_CONFIGS_DIR,
|
||||
WIREGUARD_HOST_CONFIG_DIR,
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||
WGUI_LIB_KEYS_DIR,
|
||||
@@ -133,7 +133,7 @@ export default async function setupWireguardClient({
|
||||
host?.public_key ||
|
||||
execSync(
|
||||
`cat ${path.join(
|
||||
WIREGUARD_HOST_CONFIG_DIR,
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||
)}`,
|
||||
{ encoding: "utf-8" },
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
} from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||
import deriveWireguardInterfaceName from "@/src/utils/derive-wireguard-interface-name";
|
||||
import { execSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import grabHostNetworkInterface from "./grab-host-network-interface";
|
||||
@@ -15,7 +16,8 @@ import checkPrivateIPAvailability from "./check-private-ip-availability";
|
||||
|
||||
const {
|
||||
WGUI_LIB_IP_TABLES_DIR,
|
||||
WIREGUARD_HOST_CONFIG_DIR,
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
WGUI_WG_QUICK_SYSTEMD_SCRIPT,
|
||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||
} = grabDirNames();
|
||||
@@ -31,6 +33,12 @@ export default async function setupWireguardHost({
|
||||
}: Params): Promise<APIResponseObject> {
|
||||
const host_id = host?.id || AppData["WireguardHostID"];
|
||||
|
||||
const INTERFACE_NAME = deriveWireguardInterfaceName({ host_id });
|
||||
const HOST_CONFIG_PATH = path.join(
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
`${INTERFACE_NAME}.conf`,
|
||||
);
|
||||
|
||||
const variables_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_VARIABLES,
|
||||
TableType
|
||||
@@ -83,7 +91,8 @@ export default async function setupWireguardHost({
|
||||
let pre_sh = ``;
|
||||
|
||||
pre_sh += `set -e\n`;
|
||||
pre_sh += `cd ${WIREGUARD_HOST_CONFIG_DIR}\n`;
|
||||
pre_sh += `mkdir -p ${WGUI_LIB_HOSTS_CONFIGS_DIR}\n`;
|
||||
pre_sh += `cd ${WGUI_LIB_HOSTS_CONFIGS_DIR}\n`;
|
||||
pre_sh += `if [ ! -f ${WIREGUARD_PRIVATE_KEY_FILE_NAME} ]; then\n`;
|
||||
pre_sh += ` wg genkey | tee ${WIREGUARD_PRIVATE_KEY_FILE_NAME} | wg pubkey > ${WIREGUARD_PUBLIC_KEY_FILE_NAME}\n`;
|
||||
pre_sh += `fi\n`;
|
||||
@@ -103,12 +112,12 @@ export default async function setupWireguardHost({
|
||||
const HOST_PUBLIC_KEY = host?.id
|
||||
? host.public_key
|
||||
: execSync(
|
||||
`cat ${path.join(WIREGUARD_HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
||||
`cat ${path.join(WGUI_LIB_HOSTS_CONFIGS_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
||||
);
|
||||
const HOST_PRIVATE_KEY = host?.private_key
|
||||
? host.private_key
|
||||
: execSync(
|
||||
`cat ${path.join(WIREGUARD_HOST_CONFIG_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME)}`,
|
||||
`cat ${path.join(WGUI_LIB_HOSTS_CONFIGS_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME)}`,
|
||||
);
|
||||
|
||||
let sh = ``;
|
||||
@@ -124,17 +133,17 @@ export default async function setupWireguardHost({
|
||||
`${host_id}-down.sh`,
|
||||
);
|
||||
|
||||
sh += `cd ${WIREGUARD_HOST_CONFIG_DIR}\n`;
|
||||
sh += `cd ${WGUI_LIB_HOSTS_CONFIGS_DIR}\n`;
|
||||
|
||||
sh += `cat > ${POST_UP_PATH} << EOF\n`;
|
||||
sh += `#!/bin/bash\n\n`;
|
||||
sh += `# Allow WireGuard traffic to/from the server itself\n`;
|
||||
sh += `iptables -I INPUT 1 -i wg${host_id} -j ACCEPT\n`;
|
||||
sh += `iptables -I OUTPUT 1 -o wg${host_id} -j ACCEPT\n`;
|
||||
sh += `iptables -I INPUT 1 -i ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `iptables -I OUTPUT 1 -o ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `\n`;
|
||||
sh += `# Allow WireGuard traffic to be forwarded (insert above Docker rules)\n`;
|
||||
sh += `iptables -I FORWARD 1 -i wg${host_id} -j ACCEPT\n`;
|
||||
sh += `iptables -I FORWARD 1 -o wg${host_id} -j ACCEPT\n`;
|
||||
sh += `iptables -I FORWARD 1 -i ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `iptables -I FORWARD 1 -o ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `\n`;
|
||||
sh += `iptables -t nat -A POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`;
|
||||
sh += `EOF\n`;
|
||||
@@ -144,19 +153,19 @@ export default async function setupWireguardHost({
|
||||
sh += `cat > ${POST_DOWN_PATH} << EOF\n`;
|
||||
sh += `#!/bin/bash\n\n`;
|
||||
sh += `# Remove WireGuard INPUT/OUTPUT rules\n`;
|
||||
sh += `iptables -D INPUT -i wg${host_id} -j ACCEPT\n`;
|
||||
sh += `iptables -D OUTPUT -o wg${host_id} -j ACCEPT\n`;
|
||||
sh += `iptables -D INPUT -i ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `iptables -D OUTPUT -o ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `\n`;
|
||||
sh += `# Remove FORWARD rules\n`;
|
||||
sh += `iptables -D FORWARD -i wg${host_id} -j ACCEPT\n`;
|
||||
sh += `iptables -D FORWARD -o wg${host_id} -j ACCEPT\n`;
|
||||
sh += `iptables -D FORWARD -i ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `iptables -D FORWARD -o ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `\n`;
|
||||
sh += `iptables -t nat -D POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`;
|
||||
sh += `EOF\n`;
|
||||
|
||||
sh += `\n`;
|
||||
|
||||
sh += `cat > wg${host_id}.conf << EOF\n`;
|
||||
sh += `cat > ${INTERFACE_NAME}.conf << EOF\n`;
|
||||
sh += `[Interface]\n`;
|
||||
sh += `Address = ${HOST_WG_IP}/24\n`;
|
||||
sh += `ListenPort = 51820\n`;
|
||||
@@ -183,10 +192,28 @@ export default async function setupWireguardHost({
|
||||
|
||||
const exec = execSync(sh, { encoding: "utf-8" });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: exec,
|
||||
};
|
||||
const IS_ROOT =
|
||||
typeof process.getuid === "function" && process.getuid() === 0;
|
||||
const SUDO_PREFIX = IS_ROOT ? "" : "sudo -n ";
|
||||
const MANAGE_WG_QUICK_CMD = `${SUDO_PREFIX}${WGUI_WG_QUICK_SYSTEMD_SCRIPT} ${INTERFACE_NAME} ${HOST_CONFIG_PATH}`;
|
||||
|
||||
let exec_systemd = ``;
|
||||
|
||||
try {
|
||||
exec_systemd = execSync(MANAGE_WG_QUICK_CMD, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: [exec.trim(), exec_systemd.trim()].join("\n\n"),
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Host config written to ${HOST_CONFIG_PATH}, but could not manage the tunnel via systemd (wg-quick@${INTERFACE_NAME}.service): ${error.message}`,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import deriveWireguardInterfaceName from "@/src/utils/derive-wireguard-interface-name";
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
@@ -7,6 +8,7 @@ import type {
|
||||
|
||||
const LISTEN_PORT = 51820;
|
||||
const IP_TABLES_DIR = `/var/lib/wgui/iptables`;
|
||||
const HOSTS_CONFIG_DIR = `/var/lib/wgui/hosts`;
|
||||
|
||||
type Params = {
|
||||
host?: BUN_SQLITE_WGUI_HOSTS;
|
||||
@@ -25,10 +27,14 @@ export default function deriveHostConfig({
|
||||
host?.wg_ip_address ||
|
||||
variables?.find((v) => v.key == "main_host_wg_ip_address")?.value;
|
||||
|
||||
const interface_name = deriveWireguardInterfaceName({ host_id });
|
||||
|
||||
return {
|
||||
host_id,
|
||||
interface_name: `wg${host_id}`,
|
||||
config_path: `/etc/wireguard/wg${host_id}.conf`,
|
||||
interface_name,
|
||||
config_path: `${HOSTS_CONFIG_DIR}/${interface_name}.conf`,
|
||||
systemd_unit_name: `wg-quick@${interface_name}.service`,
|
||||
|
||||
address: wg_ip_address ? `${wg_ip_address}/24` : undefined,
|
||||
listen_port: LISTEN_PORT,
|
||||
post_up: `${IP_TABLES_DIR}/${host_id}-up.sh`,
|
||||
|
||||
@@ -62,6 +62,9 @@ export default function InterfaceConfigSection({
|
||||
<P noMargin className="font-mono text-[11.5px] text-foreground-light/40 dark:text-foreground-dark/40">
|
||||
{config.config_path}
|
||||
</P>
|
||||
<P noMargin className="font-mono text-[11.5px] text-foreground-light/40 dark:text-foreground-dark/40">
|
||||
{config.systemd_unit_name}
|
||||
</P>
|
||||
</Stack>
|
||||
<AdminButton
|
||||
Icon={Copy}
|
||||
|
||||
@@ -12,6 +12,7 @@ log() { echo "==> $*"; }
|
||||
fail() { echo "error: $*" >&2; exit 1; }
|
||||
|
||||
WGUI_LIB_DIR="/var/lib/wgui"
|
||||
WG_QUICK_HELPER="$WGUI_LIB_DIR/scripts/wg-quick-systemd.sh"
|
||||
INSTALL_DIR="${INSTALL_DIR:-}"
|
||||
SERVICE_USER="${SERVICE_USER:-}"
|
||||
SERVICE_GROUP="${SERVICE_GROUP:-}"
|
||||
@@ -125,8 +126,9 @@ install_bun() {
|
||||
}
|
||||
|
||||
setup_service_user() {
|
||||
mkdir -p "$WGUI_LIB_DIR/iptables" "$WGUI_LIB_DIR/keys" "$WGUI_LIB_DIR/clients"
|
||||
mkdir -p "$WGUI_LIB_DIR/iptables" "$WGUI_LIB_DIR/keys" "$WGUI_LIB_DIR/clients" "$WGUI_LIB_DIR/hosts" "$WGUI_LIB_DIR/scripts"
|
||||
chown -R "$SERVICE_USER:$SERVICE_GROUP" "$WGUI_LIB_DIR"
|
||||
chmod 770 "$WGUI_LIB_DIR/hosts"
|
||||
}
|
||||
|
||||
clone_or_update() {
|
||||
@@ -187,11 +189,30 @@ setup_wireguard() {
|
||||
fi
|
||||
}
|
||||
|
||||
grant_runtime_access() {
|
||||
log "granting $SERVICE_USER access to /etc/wireguard ..."
|
||||
mkdir -p /etc/wireguard
|
||||
chown "root:$SERVICE_GROUP" /etc/wireguard
|
||||
chmod 770 /etc/wireguard
|
||||
install_wg_quick_helper() {
|
||||
local helper_src="$INSTALL_DIR/src/scripts/wg-quick-systemd.sh"
|
||||
if [ ! -f "$helper_src" ]; then
|
||||
fail "missing $helper_src — cannot install the wg-quick systemd helper"
|
||||
fi
|
||||
log "installing wg-quick systemd helper to $WG_QUICK_HELPER ..."
|
||||
install -m 755 -o root -g root "$helper_src" "$WG_QUICK_HELPER"
|
||||
}
|
||||
|
||||
install_wg_quick_sudoers() {
|
||||
if [ "$SERVICE_USER" = "root" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! command -v visudo >/dev/null 2>&1; then
|
||||
log "visudo not found — skipping sudoers entry for $SERVICE_USER (tunnel management will require root)"
|
||||
return 0
|
||||
fi
|
||||
local sudoers_file="/etc/sudoers.d/wgui-wg-quick"
|
||||
log "granting $SERVICE_USER passwordless access to $WG_QUICK_HELPER ..."
|
||||
cat > "$sudoers_file" <<EOF
|
||||
$SERVICE_USER ALL=(root) NOPASSWD: $WG_QUICK_HELPER *
|
||||
EOF
|
||||
chmod 440 "$sudoers_file"
|
||||
visudo -cf "$sudoers_file" >/dev/null 2>&1 || fail "invalid sudoers file $sudoers_file"
|
||||
}
|
||||
|
||||
install_systemd_unit() {
|
||||
@@ -274,7 +295,8 @@ fi
|
||||
install_dependencies
|
||||
ensure_env_file
|
||||
setup_wireguard
|
||||
grant_runtime_access
|
||||
install_wg_quick_helper
|
||||
install_wg_quick_sudoers
|
||||
|
||||
if [ "$DEV_MODE" = false ]; then
|
||||
case "$INIT_SYSTEM" in
|
||||
@@ -295,7 +317,8 @@ fi
|
||||
PORT="$(grab_port)"
|
||||
log "wg-ui install complete."
|
||||
log "webapp: $INSTALL_DIR"
|
||||
log "runtime: $WGUI_LIB_DIR (keys, iptables, client configs)"
|
||||
log "runtime: $WGUI_LIB_DIR (host configs, keys, iptables, client configs)"
|
||||
log "tunnels: systemd units wg-quick@wgui<host_id>.service (configs in $WGUI_LIB_DIR/hosts — /etc/wireguard is never touched)"
|
||||
if [ "$DEV_MODE" = true ]; then
|
||||
log "process: development mode — no system service installed"
|
||||
else
|
||||
|
||||
@@ -77,8 +77,12 @@ for tool in "${REQUIRED_TOOLS[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p /etc/wireguard
|
||||
chmod 700 /etc/wireguard
|
||||
# Configs are project-scoped under /var/lib/wgui (hosts + helper scripts),
|
||||
# never /etc/wireguard, so stock wg-quick setups on this machine are untouched.
|
||||
mkdir -p /var/lib/wgui/hosts /var/lib/wgui/scripts
|
||||
chown root:root /var/lib/wgui/hosts /var/lib/wgui/scripts
|
||||
chmod 700 /var/lib/wgui/hosts
|
||||
chmod 755 /var/lib/wgui/scripts
|
||||
|
||||
echo "wireguard setup complete."
|
||||
wg --version
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Manages the wg-quick systemd unit for a single wireguard-ui host tunnel.
|
||||
#
|
||||
# This project deliberately avoids /etc/wireguard entirely so it can never
|
||||
# collide with an existing WireGuard setup. Host configs live under
|
||||
# /var/lib/wgui/hosts and are wired into the stock wg-quick@ template unit
|
||||
# via a per-instance drop-in that points ExecStart/ExecStop at the project
|
||||
# config path. Interfaces follow the project convention wgui<host_id>
|
||||
# (e.g. wgui0), never stock names like wg0.
|
||||
#
|
||||
# Usage: wg-quick-systemd.sh <interface> <config-path>
|
||||
# interface e.g. wgui0 — must match wgui[0-9]* (project convention)
|
||||
# config-path absolute path to the host config file
|
||||
#
|
||||
# Designed to run as root (directly or via a NOPASSWD sudoers entry).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INTERFACE="${1:-}"
|
||||
CONFIG_PATH="${2:-}"
|
||||
|
||||
if [ -z "$INTERFACE" ] || [ -z "$CONFIG_PATH" ]; then
|
||||
echo "usage: $0 <interface> <config-path>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$INTERFACE" in
|
||||
wgui[0-9]*)
|
||||
;;
|
||||
*)
|
||||
echo "error: refusing to manage non-project interface '$INTERFACE' (expected wgui[0-9]*)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
WG_QUICK_BIN="$(command -v wg-quick)"
|
||||
if [ -z "$WG_QUICK_BIN" ]; then
|
||||
echo "error: wg-quick not found — run setup-wireguard.sh first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DROP_IN_DIR="/etc/systemd/system/wg-quick@${INTERFACE}.service.d"
|
||||
mkdir -p "$DROP_IN_DIR"
|
||||
|
||||
cat > "$DROP_IN_DIR/override.conf" <<EOF
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=$WG_QUICK_BIN up $CONFIG_PATH
|
||||
ExecReload=
|
||||
ExecReload=$WG_QUICK_BIN strip $CONFIG_PATH
|
||||
ExecStop=
|
||||
ExecStop=$WG_QUICK_BIN down $CONFIG_PATH
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "wg-quick@${INTERFACE}.service"
|
||||
systemctl restart "wg-quick@${INTERFACE}.service"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import deriveWireguardInterfaceName from "./derive-wireguard-interface-name";
|
||||
|
||||
describe("deriveWireguardInterfaceName", () => {
|
||||
test("derives interface name from host id", () => {
|
||||
expect(deriveWireguardInterfaceName({ host_id: 0 })).toBe("wgui0");
|
||||
expect(deriveWireguardInterfaceName({ host_id: 1 })).toBe("wgui1");
|
||||
expect(deriveWireguardInterfaceName({ host_id: 7 })).toBe("wgui7");
|
||||
});
|
||||
|
||||
test("uses a project-scoped prefix, never stock wgN", () => {
|
||||
expect(deriveWireguardInterfaceName({ host_id: 0 })).not.toBe("wg0");
|
||||
expect(deriveWireguardInterfaceName({ host_id: 1 })).not.toBe("wg1");
|
||||
});
|
||||
|
||||
test("prefix matches wg-quick interface name requirements (<= 15 chars)", () => {
|
||||
expect(deriveWireguardInterfaceName({ host_id: 0 }).length).toBeLessThanOrEqual(
|
||||
15,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
const WIREGUARD_INTERFACE_NAME_PREFIX = `wgui`;
|
||||
|
||||
type Params = {
|
||||
host_id: number;
|
||||
};
|
||||
|
||||
export default function deriveWireguardInterfaceName({ host_id }: Params) {
|
||||
return `${WIREGUARD_INTERFACE_NAME_PREFIX}${host_id}`;
|
||||
}
|
||||
@@ -33,8 +33,13 @@ export default function grabDirNames(params?: Params) {
|
||||
const WGUI_LIB_IP_TABLES_DIR = path.join(WGUI_LIB_DIR, `iptables`);
|
||||
const WGUI_LIB_KEYS_DIR = path.join(WGUI_LIB_DIR, `keys`);
|
||||
const WGUI_LIB_CLIENTS_CONFIGS_DIR = path.join(WGUI_LIB_DIR, `clients`);
|
||||
const WGUI_LIB_HOSTS_CONFIGS_DIR = path.join(WGUI_LIB_DIR, `hosts`);
|
||||
const WGUI_LIB_SCRIPTS_DIR = path.join(WGUI_LIB_DIR, `scripts`);
|
||||
const WGUI_WG_QUICK_SYSTEMD_SCRIPT = path.join(
|
||||
WGUI_LIB_SCRIPTS_DIR,
|
||||
`wg-quick-systemd.sh`,
|
||||
);
|
||||
|
||||
const WIREGUARD_HOST_CONFIG_DIR = `/etc/wireguard`;
|
||||
const WIREGUARD_PRIVATE_KEY_FILE_NAME = `private.key`;
|
||||
const WIREGUARD_PUBLIC_KEY_FILE_NAME = `public.key`;
|
||||
const WIREGUARD_CLIENT_CONFIG_FILE_NAME = `wg.conf`;
|
||||
@@ -52,8 +57,10 @@ export default function grabDirNames(params?: Params) {
|
||||
WGUI_LIB_IP_TABLES_DIR,
|
||||
WGUI_LIB_KEYS_DIR,
|
||||
WGUI_LIB_CLIENTS_CONFIGS_DIR,
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
WGUI_LIB_SCRIPTS_DIR,
|
||||
WGUI_WG_QUICK_SYSTEMD_SCRIPT,
|
||||
|
||||
WIREGUARD_HOST_CONFIG_DIR,
|
||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
||||
|
||||
Reference in New Issue
Block a user