From 76679aabe7ae26c74f2e8c49ce58bb824c3de8da Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sun, 13 Sep 2026 08:31:10 +0100 Subject: [PATCH] Implement wireguard client setup with auto key generation and host discovery --- .../setup/grab-host-network-interface.ts | 19 ++- .../setup/grab-host-public-ip-address.ts | 30 +++- .../backend/setup/setup-wireguard-client.ts | 153 +++++++++++++++++- src/scripts/setup-wireguard.sh | 10 +- src/utils/grab-dir-names.ts | 2 + 5 files changed, 199 insertions(+), 15 deletions(-) diff --git a/src/functions/backend/setup/grab-host-network-interface.ts b/src/functions/backend/setup/grab-host-network-interface.ts index a916dd7..a708521 100644 --- a/src/functions/backend/setup/grab-host-network-interface.ts +++ b/src/functions/backend/setup/grab-host-network-interface.ts @@ -1,9 +1,24 @@ +import { execSync } from "node:child_process"; + /** * Function to grab the host's network interface * eg `eth0` * @param param0 */ export default async function grabHostNetworkInterface() { - // Placeholder + try { + const route = execSync(`ip -4 route show default`, { + encoding: "utf-8", + }) + .trim() + .split(/\s+/); + + const deviceIndex = route.indexOf(`dev`); + + if (deviceIndex > -1 && route[deviceIndex + 1]) { + return route[deviceIndex + 1]; + } + } catch (error) {} + return `eth0`; -} +} \ No newline at end of file diff --git a/src/functions/backend/setup/grab-host-public-ip-address.ts b/src/functions/backend/setup/grab-host-public-ip-address.ts index 761f317..8b1de5c 100644 --- a/src/functions/backend/setup/grab-host-public-ip-address.ts +++ b/src/functions/backend/setup/grab-host-public-ip-address.ts @@ -1,9 +1,33 @@ +import { execSync } from "node:child_process"; + /** * Function to grab the host's public * IP address * @param param0 */ export default async function grabHostPublicIPAddress() { - // Placeholder - return `102.34.765.43`; -} + try { + const public_ip = execSync(`curl -sS --max-time 15 https://api.ipify.org`, { + encoding: "utf-8", + }) + .trim(); + + if (/^\d{1,3}(\.\d{1,3}){3}$/.test(public_ip)) { + return public_ip; + } + } catch (error) {} + + try { + const local_ips = execSync(`hostname -I`, { + encoding: "utf-8", + }) + .trim() + .split(/\s+/); + + if (local_ips[0]) { + return local_ips[0]; + } + } catch (error) {} + + return ``; +} \ No newline at end of file diff --git a/src/functions/backend/setup/setup-wireguard-client.ts b/src/functions/backend/setup/setup-wireguard-client.ts index 4c5a58e..c849870 100644 --- a/src/functions/backend/setup/setup-wireguard-client.ts +++ b/src/functions/backend/setup/setup-wireguard-client.ts @@ -7,14 +7,19 @@ import { AppData } from "@/src/data/app-data"; import grabDirNames from "@/src/utils/grab-dir-names"; import { execSync } from "node:child_process"; import path from "node:path"; -import grabHostNetworkInterface from "./grab-host-network-interface"; +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"; const { - WGUI_LIB_IP_TABLES_DIR, + WGUI_LIB_CLIENTS_CONFIGS_DIR, WIREGUARD_HOST_CONFIG_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME, + WGUI_LIB_KEYS_DIR, + WIREGUARD_CLIENT_CONFIG_FILE_NAME, } = grabDirNames(); type Params = { @@ -35,7 +40,145 @@ export default async function setupWireguardClient({ host, variables, }: Params): Promise { - return { - success: true, - }; + const host_id = host?.id || AppData["WireguardHostID"]; + + const CLIENT_WG_IP = client?.wg_ip_address; + + if (!CLIENT_WG_IP) { + return { + success: false, + msg: `No Client Private IP address provided`, + }; + } + + const HOST_WG_IP = + host?.wg_ip_address || + variables?.find((v) => v.key == "main_host_wg_ip_address")?.value; + + if (!HOST_WG_IP) { + return { + success: false, + msg: `No Host Private IP address provided`, + }; + } + + 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, + ); + const CLIENT_PUBLIC_KEY_FILE = path.join( + CLIENT_DIR, + WIREGUARD_PUBLIC_KEY_FILE_NAME, + ); + const CLIENT_CONFIG_FILE = path.join( + CLIENT_DIR, + WIREGUARD_CLIENT_CONFIG_FILE_NAME, + ); + + let pre_sh = ``; + + pre_sh += `mkdir -p ${CLIENT_DIR}\n`; + pre_sh += `if [ ! -f ${CLIENT_PRIVATE_KEY_FILE} ]; then\n`; + pre_sh += ` wg genkey | tee ${CLIENT_PRIVATE_KEY_FILE} | wg pubkey > ${CLIENT_PUBLIC_KEY_FILE}\n`; + pre_sh += `fi\n`; + + try { + const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" }); + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } + + try { + const CLIENT_PRIVATE_KEY = + 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 = + 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( + WIREGUARD_HOST_CONFIG_DIR, + WIREGUARD_PUBLIC_KEY_FILE_NAME, + )}`, + { encoding: "utf-8" }, + ).trim(); + + const ALLOWED_IPS = client?.allowed_ips || `0.0.0.0/0`; + + const PUBLIC_IP_ADDRESS = + (await grabHostPublicIPAddress()) || + client?.public_ip_address || + HOST_WG_IP; + + const sh = ` +cd ${CLIENT_DIR} + +cat > ${WIREGUARD_CLIENT_CONFIG_FILE_NAME} << EOF +[Interface] +Address = ${CLIENT_WG_IP}/32 +PrivateKey = ${CLIENT_PRIVATE_KEY} +DNS = 1.1.1.1 + +[Peer] +PublicKey = ${HOST_PUBLIC_KEY} +Endpoint = ${PUBLIC_IP_ADDRESS}:51820 +AllowedIPs = ${ALLOWED_IPS} +EOF +`; + + const exec = execSync(sh, { encoding: "utf-8" }); + + const CLIENT_CONFIG = execSync(`cat ${CLIENT_CONFIG_FILE}`, { + encoding: "utf-8", + }); + + const host_setup_res = await setupWireguardHost({ host, variables }); + + if (!host_setup_res.success) { + return { + success: false, + msg: `Client config created but Host update failed: ${host_setup_res.msg}`, + }; + } + + return { + success: true, + msg: `Client ${client.name} setup complete`, + stringRes: CLIENT_CONFIG, + }; + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } } diff --git a/src/scripts/setup-wireguard.sh b/src/scripts/setup-wireguard.sh index 18118ed..55ae970 100755 --- a/src/scripts/setup-wireguard.sh +++ b/src/scripts/setup-wireguard.sh @@ -12,7 +12,7 @@ set -euo pipefail -REQUIRED_TOOLS=(wg wg-quick) +REQUIRED_TOOLS=(wg wg-quick ip curl) HAVE_ALL_TOOLS=true for tool in "${REQUIRED_TOOLS[@]}"; do @@ -49,19 +49,19 @@ case "$DISTRO" in echo "detected Debian-family distro: $DISTRO" export DEBIAN_FRONTEND=noninteractive apt-get update -y - apt-get install -y wireguard + apt-get install -y wireguard iproute2 curl ;; fedora | rhel | centos | rocky | almalinux) echo "detected Fedora-family distro: $DISTRO" - dnf install -y wireguard-tools + dnf install -y wireguard-tools iproute2 curl ;; arch | manjaro | endeavouros) echo "detected Arch-family distro: $DISTRO" - pacman -Syu --noconfirm --needed wireguard-tools + pacman -Syu --noconfirm --needed wireguard-tools iproute2 curl ;; alpine) echo "detected Alpine distro: $DISTRO" - apk add --no-cache wireguard-tools + apk add --no-cache wireguard-tools curl ;; *) echo "error: unsupported distro: $DISTRO" >&2 diff --git a/src/utils/grab-dir-names.ts b/src/utils/grab-dir-names.ts index f34ed17..d07110e 100644 --- a/src/utils/grab-dir-names.ts +++ b/src/utils/grab-dir-names.ts @@ -37,6 +37,7 @@ export default function grabDirNames(params?: Params) { 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`; return { ROOT_DIR, @@ -55,5 +56,6 @@ export default function grabDirNames(params?: Params) { WIREGUARD_HOST_CONFIG_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME, + WIREGUARD_CLIENT_CONFIG_FILE_NAME, }; }