import type { BUN_SQLITE_WGUI_CLIENTS, BUN_SQLITE_WGUI_HOSTS, BUN_SQLITE_WGUI_VARIABLES, } 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"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { TableType } from "@/src/types"; import checkPrivateIPAvailability from "./check-private-ip-availability"; 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, } = grabDirNames(); type Params = { host?: BUN_SQLITE_WGUI_HOSTS; wg_subnet_ip?: string; }; export default async function setupWireguardHost({ host, wg_subnet_ip, }: Params): Promise { 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 >({ table: "variables", }); const variables = variables_res.payload; const host_clients_res = await BunSQLite.select< BUN_SQLITE_WGUI_CLIENTS, TableType >({ table: "clients", query: { query: { host_id: { value: host_id, }, }, }, }); const clients = host_clients_res.payload || []; const TARGET_INTERFACE = await grabHostNetworkInterface(); const HOST_WG_IP = host?.wg_ip_address || variables?.find((v) => v.key == "main_host_wg_ip_address")?.value || wg_subnet_ip; if (!HOST_WG_IP) { return { success: false, msg: `No Host Private IP address provided`, }; } const is_ip_available = await checkPrivateIPAvailability({ ip_address: HOST_WG_IP, }); if (!is_ip_available.success) { return { success: false, msg: `IP not available`, }; } 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 += `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, msg: error.message, }; } 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)}`, ); 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`, ); 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 ${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 ${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`; sh += `\n`; sh += `cat > ${POST_DOWN_PATH} << EOF\n`; sh += `#!/bin/bash\n\n`; sh += `# Remove WireGuard INPUT/OUTPUT rules\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 ${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 > ${INTERFACE_NAME}.conf << EOF\n`; sh += `[Interface]\n`; sh += `Address = ${HOST_WG_IP}/24\n`; sh += `ListenPort = 51820\n`; sh += `PrivateKey = ${HOST_PRIVATE_KEY}\n`; sh += `PostUp = ${POST_UP_PATH}\n`; sh += `PostDown = ${POST_DOWN_PATH}\n`; sh += `\n`; if (clients[0]) { for (let i = 0; i < clients.length; i++) { const client = clients[i]; if (!client?.id || !client.public_key) continue; sh += `[Peer]\n`; sh += `PublicKey = ${client.public_key}\n`; sh += `AllowedIPs = ${client.allowed_ips}\n`; sh += `\n`; } } sh += `EOF\n`; sh += `\n`; 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}`; 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, msg: error.message, }; } return { success: true, }; }