diff --git a/db/schema.ts b/db/schema.ts index 60da3f9..6366290 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -174,10 +174,6 @@ const schema: BUN_SQLITE_DatabaseSchemaType = { fieldName: "public_ip_address", dataType: "TEXT", }, - { - fieldName: "private_key", - dataType: "TEXT", - }, { fieldName: "public_key", dataType: "TEXT", @@ -203,10 +199,6 @@ const schema: BUN_SQLITE_DatabaseSchemaType = { fieldName: "wg_ip_address", dataType: "TEXT", }, - { - fieldName: "private_key", - dataType: "TEXT", - }, { fieldName: "public_key", dataType: "TEXT", @@ -238,6 +230,7 @@ const schema: BUN_SQLITE_DatabaseSchemaType = { fieldName: "key", dataType: "TEXT", options: Variables.map((v) => v.value), + unique: true, }, { fieldName: "value", diff --git a/db/types/db.ts b/db/types/db.ts index 53dfafe..29f865c 100644 --- a/db/types/db.ts +++ b/db/types/db.ts @@ -111,7 +111,6 @@ export type BUN_SQLITE_WGUI_CLIENTS = { name?: string; wg_ip_address?: string; public_ip_address?: string; - private_key?: string; public_key?: string; allowed_ips?: string; notes?: string; @@ -132,7 +131,6 @@ export type BUN_SQLITE_WGUI_HOSTS = { updated_at?: number | ""; user_id?: number | ""; wg_ip_address?: string; - private_key?: string; public_key?: string; } diff --git a/package.json b/package.json index 7667809..9ef5c25 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "module", "private": true, "scripts": { - "dev": "bun src/server.ts", + "dev": "sudo bun src/server.ts", "build": "bunx bunext build", "db:schema": "bunx bun-sqlite schema -t", "deploy:prod": "./deploy/deploy-prod.sh", diff --git a/src/functions/backend/setup/manage-wireguard-host.ts b/src/functions/backend/setup/manage-wireguard-host.ts new file mode 100644 index 0000000..a52d526 --- /dev/null +++ b/src/functions/backend/setup/manage-wireguard-host.ts @@ -0,0 +1,53 @@ +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 type { APIResponseObject } from "@moduletrace/bunext/types"; +import { execSync } from "node:child_process"; +import path from "node:path"; + +const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_WG_QUICK_MANAGE_SCRIPT } = + grabDirNames(); + +export type WireguardHostAction = "up" | "down" | "restart"; + +type Params = { + action: WireguardHostAction; + host_id?: number; +}; + +export default function manageWireguardHost({ + action, + host_id = AppData["WireguardHostID"], +}: Params): APIResponseObject { + const INTERFACE_NAME = deriveWireguardInterfaceName({ host_id }); + const HOST_CONFIG_PATH = path.join( + WGUI_LIB_HOSTS_CONFIGS_DIR, + `${INTERFACE_NAME}.conf`, + ); + + const IS_ROOT = + typeof process.getuid === "function" && process.getuid() === 0; + + if (!IS_ROOT) { + return { + success: false, + msg: "wg-ui must run as root to manage wireguard tunnels", + }; + } + + const MANAGE_COMMAND = `${WGUI_WG_QUICK_MANAGE_SCRIPT} ${action} ${INTERFACE_NAME} ${HOST_CONFIG_PATH}`; + + try { + const output = execSync(MANAGE_COMMAND, { encoding: "utf-8" }).trim(); + + return { + success: true, + msg: output, + }; + } catch (error: any) { + return { + success: false, + msg: `Could not ${action} ${INTERFACE_NAME}: ${error.message}`, + }; + } +} diff --git a/src/functions/backend/setup/setup-wireguard-client.ts b/src/functions/backend/setup/setup-wireguard-client.ts index f805b01..fafead8 100644 --- a/src/functions/backend/setup/setup-wireguard-client.ts +++ b/src/functions/backend/setup/setup-wireguard-client.ts @@ -11,20 +11,20 @@ import grabHostPublicIPAddress from "./grab-host-public-ip-address"; import setupWireguardHost from "./setup-wireguard-host"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import BunSQLite from "@moduletrace/bun-sqlite"; -import type { TableType } from "@/src/types"; +import type { TableType, User } from "@/src/types"; const { - WGUI_LIB_CLIENTS_CONFIGS_DIR, WGUI_LIB_HOSTS_CONFIGS_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME, - WGUI_LIB_KEYS_DIR, + WGUI_LIB_HOST_CLIENTS_DIR_NAME, WIREGUARD_CLIENT_CONFIG_FILE_NAME, } = grabDirNames(); type Params = { client: BUN_SQLITE_WGUI_CLIENTS; host?: BUN_SQLITE_WGUI_HOSTS; + user: User; }; /** @@ -37,6 +37,7 @@ type Params = { export default async function setupWireguardClient({ client, host, + user, }: Params): Promise { const host_id = host?.id || AppData["WireguardHostID"]; @@ -58,9 +59,21 @@ export default async function setupWireguardClient({ }; } - const HOST_WG_IP = - host?.wg_ip_address || - variables?.find((v) => v.key == "main_host_wg_ip_address")?.value; + const HOST_CONFIG_DIR = path.join( + WGUI_LIB_HOSTS_CONFIGS_DIR, + String(host_id), + ); + + const HOST_CLIENTS_DIR = path.join( + WGUI_LIB_HOSTS_CONFIGS_DIR, + WGUI_LIB_HOST_CLIENTS_DIR_NAME, + ); + + const CLIENT_DIR = path.join(HOST_CLIENTS_DIR, `${client.id}`); + + const HOST_WG_IP = host?.id + ? host?.wg_ip_address + : variables?.find((v) => v.key == "main_host_wg_ip_address")?.value; if (!HOST_WG_IP) { return { @@ -69,8 +82,6 @@ export default async function setupWireguardClient({ }; } - const CLIENT_DIR = path.join(WGUI_LIB_CLIENTS_CONFIGS_DIR, `${client.id}`); - const CLIENT_PRIVATE_KEY_FILE = path.join( CLIENT_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME, @@ -101,66 +112,44 @@ export default async function setupWireguardClient({ } try { - const CLIENT_PRIVATE_KEY = - client?.private_key || - execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, { - encoding: "utf-8", - }).trim(); + const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, { + encoding: "utf-8", + }).trim(); - if (CLIENT_PRIVATE_KEY && !client.private_key && client.id) { - await BunSQLite.update({ - data: { private_key: CLIENT_PRIVATE_KEY }, - table: "clients", - targetId: client.id, - }); - } + const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, { + encoding: "utf-8", + }).trim(); - const CLIENT_PUBLIC_KEY = - client?.public_key || - execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, { - encoding: "utf-8", - }).trim(); - - if (CLIENT_PUBLIC_KEY && !client.public_key && client.id) { - await BunSQLite.update({ - data: { public_key: CLIENT_PUBLIC_KEY }, - table: "clients", - targetId: client.id, - }); - } - - const HOST_PUBLIC_KEY = - host?.public_key || - execSync( - `cat ${path.join( - WGUI_LIB_HOSTS_CONFIGS_DIR, - WIREGUARD_PUBLIC_KEY_FILE_NAME, - )}`, - { encoding: "utf-8" }, - ).trim(); - - const ALLOWED_IPS = client?.allowed_ips || `0.0.0.0/0`; + const HOST_PUBLIC_KEY = execSync( + `cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`, + { encoding: "utf-8" }, + ).trim(); const PUBLIC_IP_ADDRESS = (await grabHostPublicIPAddress()) || client?.public_ip_address || HOST_WG_IP; - const sh = ` -cd ${CLIENT_DIR} + let sh = ``; -cat > ${WIREGUARD_CLIENT_CONFIG_FILE_NAME} << EOF -[Interface] -Address = ${CLIENT_WG_IP}/32 -PrivateKey = ${CLIENT_PRIVATE_KEY} -DNS = 1.1.1.1 + sh += `cd ${CLIENT_DIR}\n`; + sh += `cat > ${WIREGUARD_CLIENT_CONFIG_FILE_NAME} << EOF\n`; + sh += `\n`; + sh += `[Interface]\n`; + sh += `Address = ${CLIENT_WG_IP}/32\n`; + sh += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`; + sh += `DNS = 1.1.1.1\n`; + sh += `\n`; + sh += `[Peer]\n`; + sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`; + sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:51820\n`; -[Peer] -PublicKey = ${HOST_PUBLIC_KEY} -Endpoint = ${PUBLIC_IP_ADDRESS}:51820 -AllowedIPs = ${ALLOWED_IPS} -EOF -`; + if (client?.allowed_ips) { + sh += `AllowedIPs = ${client.allowed_ips}\n`; + } + + sh += `EOF\n`; + sh += `\n`; const exec = execSync(sh, { encoding: "utf-8" }); @@ -168,7 +157,7 @@ EOF encoding: "utf-8", }); - const host_setup_res = await setupWireguardHost({ host }); + const host_setup_res = await setupWireguardHost({ host, user }); if (!host_setup_res.success) { return { diff --git a/src/functions/backend/setup/setup-wireguard-host.ts b/src/functions/backend/setup/setup-wireguard-host.ts index fc8cf9b..1392f5d 100644 --- a/src/functions/backend/setup/setup-wireguard-host.ts +++ b/src/functions/backend/setup/setup-wireguard-host.ts @@ -11,31 +11,50 @@ import path from "node:path"; import grabHostNetworkInterface from "./grab-host-network-interface"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import BunSQLite from "@moduletrace/bun-sqlite"; -import type { TableType } from "@/src/types"; +import type { TableType, User } from "@/src/types"; import checkPrivateIPAvailability from "./check-private-ip-availability"; +import manageWireguardHost from "./manage-wireguard-host"; const { - WGUI_LIB_IP_TABLES_DIR, WGUI_LIB_HOSTS_CONFIGS_DIR, - WGUI_WG_QUICK_SYSTEMD_SCRIPT, WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME, + WGUI_LIB_HOST_CLIENTS_DIR_NAME, + WGUI_LIB_HOST_IPTABLES_DIR_NAME, } = grabDirNames(); type Params = { host?: BUN_SQLITE_WGUI_HOSTS; wg_subnet_ip?: string; + user: User; }; export default async function setupWireguardHost({ host, wg_subnet_ip, + user, }: Params): Promise { const host_id = host?.id || AppData["WireguardHostID"]; const INTERFACE_NAME = deriveWireguardInterfaceName({ host_id }); - const HOST_CONFIG_PATH = path.join( + + const HOST_CONFIG_DIR = path.join( WGUI_LIB_HOSTS_CONFIGS_DIR, + String(host_id), + ); + + const HOST_CLIENTS_DIR = path.join( + WGUI_LIB_HOSTS_CONFIGS_DIR, + WGUI_LIB_HOST_CLIENTS_DIR_NAME, + ); + + const HOST_IPTABLES_DIR = path.join( + WGUI_LIB_HOSTS_CONFIGS_DIR, + WGUI_LIB_HOST_IPTABLES_DIR_NAME, + ); + + const HOST_CONFIG_FILE = path.join( + HOST_CONFIG_DIR, `${INTERFACE_NAME}.conf`, ); @@ -91,16 +110,14 @@ export default async function setupWireguardHost({ let pre_sh = ``; pre_sh += `set -e\n`; - pre_sh += `mkdir -p ${WGUI_LIB_HOSTS_CONFIGS_DIR}\n`; - pre_sh += `cd ${WGUI_LIB_HOSTS_CONFIGS_DIR}\n`; + pre_sh += `mkdir -p ${HOST_CONFIG_DIR}\n`; + pre_sh += `cd ${HOST_CONFIG_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`; try { const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" }); - - console.log("exec_pre_setup", exec_pre_setup); } catch (error: any) { return { success: false, @@ -109,31 +126,66 @@ export default async function setupWireguardHost({ } try { - const HOST_PUBLIC_KEY = host?.id - ? host.public_key - : execSync( - `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(WGUI_LIB_HOSTS_CONFIGS_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME)}`, - ); + const HOST_PUBLIC_KEY = execSync( + `cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`, + { encoding: "utf-8" }, + ).trim(); + + const HOST_PRIVATE_KEY = execSync( + `cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME)}`, + { encoding: "utf-8" }, + ).trim(); + + if (host_id == 0) { + const update_variables = await BunSQLite.insert< + BUN_SQLITE_WGUI_VARIABLES, + TableType + >({ + table: "variables", + data: [ + { + key: "main_host_wg_ip_address", + value: HOST_WG_IP, + }, + { + key: "main_host_wg_public_key", + value: HOST_PUBLIC_KEY, + }, + ], + update_on_duplicate: true, + }); + + console.log("update_variables", update_variables); + + if (!update_variables.success) { + throw new Error(`Couldn't update host variables`); + } + } else if (host_id) { + const update_host = await BunSQLite.insert< + BUN_SQLITE_WGUI_HOSTS, + TableType + >({ + table: "hosts", + data: [ + { + id: host_id, + user_id: user.id, + wg_ip_address: HOST_WG_IP, + public_key: HOST_PUBLIC_KEY, + }, + ], + update_on_duplicate: true, + }); + } let sh = ``; sh += `set -e\n`; - const POST_UP_PATH = path.join( - WGUI_LIB_IP_TABLES_DIR, - `${host_id}-up.sh`, - ); - const POST_DOWN_PATH = path.join( - WGUI_LIB_IP_TABLES_DIR, - `${host_id}-down.sh`, - ); + const POST_UP_PATH = path.join(HOST_IPTABLES_DIR, `up.sh`); + const POST_DOWN_PATH = path.join(HOST_IPTABLES_DIR, `down.sh`); - sh += `cd ${WGUI_LIB_HOSTS_CONFIGS_DIR}\n`; + sh += `cd ${HOST_CONFIG_DIR}\n`; sh += `cat > ${POST_UP_PATH} << EOF\n`; sh += `#!/bin/bash\n\n`; @@ -147,6 +199,7 @@ export default async function setupWireguardHost({ sh += `\n`; sh += `iptables -t nat -A POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`; sh += `EOF\n`; + sh += `chmod +x ${POST_UP_PATH}\n`; sh += `\n`; @@ -162,6 +215,7 @@ export default async function setupWireguardHost({ sh += `\n`; sh += `iptables -t nat -D POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`; sh += `EOF\n`; + sh += `chmod +x ${POST_DOWN_PATH}\n`; sh += `\n`; @@ -192,36 +246,26 @@ export default async function setupWireguardHost({ const exec = execSync(sh, { encoding: "utf-8" }); - 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}`; + const manage_res = manageWireguardHost({ + host_id, + action: "restart", + }); - 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) { + if (!manage_res.success) { 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}`, + msg: `Host config written to ${HOST_CONFIG_FILE}, but the tunnel could not be restarted: ${manage_res.msg}`, }; } + + return { + success: true, + msg: [exec.trim(), manage_res.msg].join("\n\n"), + }; } catch (error: any) { return { success: false, msg: error.message, }; } - - return { - success: true, - }; } diff --git a/src/functions/backend/setup/sync-wireguard-hosts.ts b/src/functions/backend/setup/sync-wireguard-hosts.ts new file mode 100644 index 0000000..317e7db --- /dev/null +++ b/src/functions/backend/setup/sync-wireguard-hosts.ts @@ -0,0 +1,63 @@ +import grabDirNames from "@/src/utils/grab-dir-names"; +import fs from "node:fs"; +import manageWireguardHost from "./manage-wireguard-host"; +import BunSQLite from "@moduletrace/bun-sqlite"; +import type { + BUN_SQLITE_WGUI_HOSTS, + BUN_SQLITE_WGUI_VARIABLES, +} from "@/db/types/db"; +import type { TableType } from "@/src/types"; +import { AppData } from "@/src/data/app-data"; + +const HOST_CONFIG_FILE_NAME_PATTERN = /^wgui(\d+)\.conf$/; + +export default async function syncWireguardHosts() { + const { WGUI_LIB_HOSTS_CONFIGS_DIR } = grabDirNames(); + + let host_config_file_names: string[] = []; + + try { + host_config_file_names = fs + .readdirSync(WGUI_LIB_HOSTS_CONFIGS_DIR) + .filter((file_name) => + HOST_CONFIG_FILE_NAME_PATTERN.test(file_name), + ); + + const variables = await BunSQLite.select< + BUN_SQLITE_WGUI_VARIABLES, + TableType + >({ + table: "variables", + }); + + const hosts = await BunSQLite.select({ + table: "hosts", + }); + + const main_host_id = AppData["WireguardHostID"]; + const main_host_ip = variables.payload?.find( + (v) => v.key == "main_host_wg_ip_address", + )?.value; + + if (!main_host_ip) { + throw new Error(`Main Host not set yet`); + } + + for (const host_config_file_name of host_config_file_names) { + const host_id = Number( + host_config_file_name.match(HOST_CONFIG_FILE_NAME_PATTERN)?.[1], + ); + + const res = manageWireguardHost({ action: "up", host_id }); + + console.log( + `[wgui] wireguard host ${host_config_file_name}: ${ + res.success ? `up` : `failed — ${res.msg}` + }`, + ); + } + } catch (error: any) { + console.log(`[wgui] skipping wireguard host sync — ${error.message}`); + return; + } +} diff --git a/src/pages/admin/hosts/(functions)/derive-host-config.ts b/src/pages/admin/hosts/(functions)/derive-host-config.ts index 3bfbf0c..4fc5a8b 100644 --- a/src/pages/admin/hosts/(functions)/derive-host-config.ts +++ b/src/pages/admin/hosts/(functions)/derive-host-config.ts @@ -16,11 +16,7 @@ type Params = { clients?: BUN_SQLITE_WGUI_CLIENTS[]; }; -export default function deriveHostConfig({ - host, - variables, - clients, -}: Params) { +export default function deriveHostConfig({ host, variables, clients }: Params) { const host_id = host?.id || AppData["WireguardHostID"]; const wg_ip_address = @@ -33,16 +29,14 @@ export default function deriveHostConfig({ host_id, 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`, post_down: `${IP_TABLES_DIR}/${host_id}-down.sh`, - private_key: host?.private_key, public_key: host?.public_key, - client_count: clients?.filter( - (client) => (client.host_id || 0) == host_id, - ).length || 0, + client_count: + clients?.filter((client) => (client.host_id || 0) == host_id) + .length || 0, }; -} \ No newline at end of file +} diff --git a/src/pages/admin/hosts/(sections)/interface-config-section.tsx b/src/pages/admin/hosts/(sections)/interface-config-section.tsx index 2eb97d3..2c26f84 100644 --- a/src/pages/admin/hosts/(sections)/interface-config-section.tsx +++ b/src/pages/admin/hosts/(sections)/interface-config-section.tsx @@ -30,12 +30,7 @@ export default function InterfaceConfigSection({ const rows = [ { keyName: "Address", value: config.address || "—" }, { keyName: "ListenPort", value: String(config.listen_port) }, - { - keyName: "PrivateKey", - value: config.private_key - ? "•••••••••••• (redacted)" - : "Not set", - }, + , { keyName: "PostUp", value: config.post_up }, { keyName: "PostDown", value: config.post_down }, ]; @@ -43,7 +38,9 @@ export default function InterfaceConfigSection({ function buildConfigText() { const lines = ["[Interface]"]; for (const row of rows) { - lines.push(`${row.keyName} = ${row.value}`); + if (row) { + lines.push(`${row.keyName} = ${row.value}`); + } } lines.push(""); lines.push( @@ -59,12 +56,12 @@ export default function InterfaceConfigSection({

Interface configuration

-

+

{config.config_path}

-

- {config.systemd_unit_name} -

[Interface] - {rows.map((row) => ( - - ))} + {rows.map((row) => { + if (!row) return; + + return ( + + ); + })} # Public key ·{" "} @@ -97,4 +98,4 @@ export default function InterfaceConfigSection({ ); -} \ No newline at end of file +} diff --git a/src/pages/api/admin/grab-next-available-private-ip.ts b/src/pages/api/admin/grab-next-available-private-ip.ts index f53326c..cf04613 100644 --- a/src/pages/api/admin/grab-next-available-private-ip.ts +++ b/src/pages/api/admin/grab-next-available-private-ip.ts @@ -1,8 +1,5 @@ -import checkUserAccess from "@/src/functions/backend/auth/check-user-access"; import userAuth from "@/src/functions/backend/auth/user-auth"; import grabNextAvailablePrivateIPSubnet from "@/src/functions/backend/setup/grab-next-available-private-ip-subnet"; -import setupWireguardHost from "@/src/functions/backend/setup/setup-wireguard-host"; -import type { ApiReqParams } from "@/src/types"; import type { APIResponseObject, BunextAPIRouteHandler, diff --git a/src/pages/api/admin/setup-main-host.ts b/src/pages/api/admin/setup-main-host.ts index e32adc7..4e8e84c 100644 --- a/src/pages/api/admin/setup-main-host.ts +++ b/src/pages/api/admin/setup-main-host.ts @@ -44,6 +44,7 @@ export const handler: BunextAPIRouteHandler = async ( host: { wg_ip_address: body.ip_address, }, + user, }); } catch (error: any) { return { diff --git a/src/scripts/install-wg-ui.sh b/src/scripts/install-wg-ui.sh index c6aefdf..6d987f9 100755 --- a/src/scripts/install-wg-ui.sh +++ b/src/scripts/install-wg-ui.sh @@ -12,10 +12,8 @@ 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" +WG_QUICK_HELPER="$WGUI_LIB_DIR/scripts/wg-quick-manage.sh" INSTALL_DIR="${INSTALL_DIR:-}" -SERVICE_USER="${SERVICE_USER:-}" -SERVICE_GROUP="${SERVICE_GROUP:-}" SERVICE_NAME="${SERVICE_NAME:-wgui}" REPO_URL="${REPO_URL:-}" BRANCH="${BRANCH:-main}" @@ -42,18 +40,8 @@ require_root() { fi } -resolve_service_user() { - if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] && id -u "$SUDO_USER" >/dev/null 2>&1; then - echo "$SUDO_USER" - else - echo "root" - fi -} - require_root -SERVICE_USER="${SERVICE_USER:-$(resolve_service_user)}" -SERVICE_GROUP="${SERVICE_GROUP:-$(id -gn "$SERVICE_USER")}" -log "running the wg-ui service as $SERVICE_USER (group: $SERVICE_GROUP)" +log "running the wg-ui service as root" if [ "$DEV_MODE" = true ]; then INSTALL_DIR="${INSTALL_DIR:-$REPO_ROOT}" @@ -125,10 +113,10 @@ install_bun() { log "bun: $($BUN_BIN --version)" } -setup_service_user() { +setup_lib_dirs() { 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" + chmod 700 "$WGUI_LIB_DIR/iptables" "$WGUI_LIB_DIR/keys" "$WGUI_LIB_DIR/clients" "$WGUI_LIB_DIR/hosts" + chmod 755 "$WGUI_LIB_DIR/scripts" } clone_or_update() { @@ -149,22 +137,12 @@ clone_or_update() { log "cloning wg-ui ($REPO_URL, branch $BRANCH) ..." git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR" fi - chown -R "$SERVICE_USER:$SERVICE_GROUP" "$INSTALL_DIR" -} - -run_as_service_user() { - local cmd - cmd="cd $(printf '%q' "$INSTALL_DIR") && export PATH=/usr/local/bin:/usr/bin:/bin && $(printf '%q ' "$@")" - if command -v runuser >/dev/null 2>&1; then - runuser -u "$SERVICE_USER" -- bash -c "$cmd" - else - su -s /bin/bash -c "$cmd" "$SERVICE_USER" - fi } install_dependencies() { log "installing app dependencies with bun ..." - run_as_service_user "$BUN_BIN" install + cd "$INSTALL_DIR" + "$BUN_BIN" install } ensure_env_file() { @@ -176,7 +154,6 @@ ensure_env_file() { echo "ENCRYPTION_SALT=$(openssl rand -base64 32 | tr -d '\n')" echo "DATA_DIR=$INSTALL_DIR/.data" } > "$INSTALL_DIR/.env" - chown "$SERVICE_USER:$SERVICE_GROUP" "$INSTALL_DIR/.env" chmod 600 "$INSTALL_DIR/.env" fi } @@ -190,31 +167,14 @@ setup_wireguard() { } install_wg_quick_helper() { - local helper_src="$INSTALL_DIR/src/scripts/wg-quick-systemd.sh" + local helper_src="$INSTALL_DIR/src/scripts/wg-quick-manage.sh" if [ ! -f "$helper_src" ]; then - fail "missing $helper_src — cannot install the wg-quick systemd helper" + fail "missing $helper_src — cannot install the wg-quick helper" fi - log "installing wg-quick systemd helper to $WG_QUICK_HELPER ..." + log "installing wg-quick 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" </dev/null 2>&1 || fail "invalid sudoers file $sudoers_file" -} - install_systemd_unit() { local unit="/etc/systemd/system/$SERVICE_NAME.service" log "writing systemd unit $unit ..." @@ -226,8 +186,8 @@ Wants=network-online.target [Service] Type=simple -User=$SERVICE_USER -Group=$SERVICE_GROUP +User=root +Group=root WorkingDirectory=$INSTALL_DIR Environment=NODE_ENV=production ExecStart=$BUN_BIN src/server.ts @@ -257,7 +217,7 @@ name="$SERVICE_NAME" description="Wireguard UI" command="$BUN_BIN" command_args="src/server.ts" -command_user="$SERVICE_USER" +command_user="root" directory="$INSTALL_DIR" output_log="/var/log/$SERVICE_NAME.log" error_log="/var/log/$SERVICE_NAME.log" @@ -288,7 +248,7 @@ grab_port() { install_deps install_bun -setup_service_user +setup_lib_dirs if [ "$DEV_MODE" = false ]; then clone_or_update fi @@ -296,7 +256,6 @@ install_dependencies ensure_env_file setup_wireguard install_wg_quick_helper -install_wg_quick_sudoers if [ "$DEV_MODE" = false ]; then case "$INIT_SYSTEM" in @@ -308,7 +267,7 @@ if [ "$DEV_MODE" = false ]; then ;; *) log "no supported init system found — start manually with: - su -s /bin/bash $SERVICE_USER -c 'cd $INSTALL_DIR && NODE_ENV=production $BUN_BIN src/server.ts' + cd $INSTALL_DIR && NODE_ENV=production $BUN_BIN src/server.ts (add the line above to your boot scripts)" ;; esac @@ -318,9 +277,9 @@ PORT="$(grab_port)" log "wg-ui install complete." log "webapp: $INSTALL_DIR" log "runtime: $WGUI_LIB_DIR (host configs, keys, iptables, client configs)" -log "tunnels: systemd units wg-quick@wgui.service (configs in $WGUI_LIB_DIR/hosts — /etc/wireguard is never touched)" +log "tunnels: managed by the web server via wg-quick up/down (configs in $WGUI_LIB_DIR/hosts — /etc/wireguard is never touched)" if [ "$DEV_MODE" = true ]; then - log "process: development mode — no system service installed" + log "process: development mode — no system service installed; run the dev server as root (e.g. after sudo -i)" else log "process: managed by $INIT_SYSTEM as $SERVICE_NAME" fi diff --git a/src/scripts/setup-wireguard.sh b/src/scripts/setup-wireguard.sh index 745c0a6..3fd908b 100755 --- a/src/scripts/setup-wireguard.sh +++ b/src/scripts/setup-wireguard.sh @@ -79,9 +79,10 @@ done # Configs are project-scoped under /var/lib/wgui (hosts + helper scripts), # never /etc/wireguard, so stock wg-quick setups on this machine are untouched. +# Everything runs as root: hosts holds private keys and is written by the web +# server, and the helper script is invoked directly (no sudoers entry). 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 +chown root:root /var/lib/wgui/scripts chmod 755 /var/lib/wgui/scripts echo "wireguard setup complete." diff --git a/src/scripts/wg-quick-manage.sh b/src/scripts/wg-quick-manage.sh new file mode 100755 index 0000000..83035f8 --- /dev/null +++ b/src/scripts/wg-quick-manage.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Manages a single wireguard-ui host tunnel directly with wg-quick. +# +# 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 the web server brings tunnels up/down through this +# helper instead of systemd. Interfaces follow the project convention +# wgui (e.g. wgui0), never stock names like wg0. +# +# Usage: wg-quick-manage.sh +# action up bring the tunnel up if it is not already up +# down bring the tunnel down if it is up +# restart down (if needed), then up +# interface e.g. wgui0 — must match wgui[0-9]+ (project convention) +# config-path must be /var/lib/wgui/hosts/.conf +# +# Designed to run as root only (the web server runs as root). + +set -euo pipefail + +ACTION="${1:-}" +INTERFACE="${2:-}" +CONFIG_PATH="${3:-}" + +if [ -z "$ACTION" ] || [ -z "$INTERFACE" ] || [ -z "$CONFIG_PATH" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +if [[ ! "$INTERFACE" =~ ^wgui[0-9]+$ ]]; then + echo "error: refusing to manage non-project interface '$INTERFACE' (expected wgui[0-9]+)" >&2 + exit 1 +fi + +case "$ACTION" in + up | down | restart) ;; + *) + echo "error: unknown action '$ACTION' (expected up, down, or restart)" >&2 + exit 1 + ;; +esac + +EXPECTED_CONFIG_PATH="/var/lib/wgui/hosts/${INTERFACE}.conf" +if [ "$CONFIG_PATH" != "$EXPECTED_CONFIG_PATH" ]; then + echo "error: config path must be $EXPECTED_CONFIG_PATH" >&2 + exit 1 +fi + +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 + +interface_is_up() { + ip link show dev "$INTERFACE" >/dev/null 2>&1 +} + +bring_up() { + if interface_is_up; then + echo "interface $INTERFACE is already up — nothing to do." + return 0 + fi + "$WG_QUICK_BIN" up "$CONFIG_PATH" +} + +bring_down() { + if ! interface_is_up; then + echo "interface $INTERFACE is not up — nothing to do." + return 0 + fi + "$WG_QUICK_BIN" down "$CONFIG_PATH" +} + +case "$ACTION" in + up) + bring_up + ;; + down) + bring_down + ;; + restart) + bring_down + bring_up + ;; +esac diff --git a/src/scripts/wg-quick-systemd.sh b/src/scripts/wg-quick-systemd.sh deleted file mode 100755 index 76464cc..0000000 --- a/src/scripts/wg-quick-systemd.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/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 -# (e.g. wgui0), never stock names like wg0. -# -# Usage: wg-quick-systemd.sh -# 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 " >&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" <