import type { BUN_SQLITE_WGUI_CLIENTS, BUN_SQLITE_WGUI_CLIENT_RULES, BUN_SQLITE_WGUI_HOSTS, } from "@/db/types/db"; import grabDirNames from "@/src/utils/grab-dir-names"; import { execSync } from "node:child_process"; import grabHostNetworkInterface from "./grab-host-network-interface"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { TableType, User } from "@/src/types"; import checkPrivateIPAvailability from "./check-private-ip-availability"; import manageWireguardHost from "./manage-wireguard-host"; import grabHostDirnames from "./grab-host-dir-names"; import grabClientDirnames from "./grab-client-dir-names"; import buildHostIptablesScripts from "./build-host-iptables-scripts"; import { AppData } from "@/src/data/app-data"; const { WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME } = grabDirNames(); type Params = { host?: BUN_SQLITE_WGUI_HOSTS; wg_subnet_ip?: string; user: User; is_update_after_client_setup?: boolean; }; export default async function setupWireguardHost({ host, wg_subnet_ip, user, is_update_after_client_setup, }: Params): Promise { const { HOST_CLIENTS_DIR, HOST_CONFIG_DIR, HOST_CONFIG_FILE, HOST_ID, HOST_IPTABLES_DIR, HOST_PRIVATE_KEY_FILE, HOST_PUBLIC_KEY_FILE, INTERFACE_NAME, POST_DOWN_PATH, POST_UP_PATH, } = grabHostDirnames({ host }); const host_clients_res = await BunSQLite.select< BUN_SQLITE_WGUI_CLIENTS, TableType >({ table: "clients", query: { query: { host_id: { value: String(HOST_ID), }, }, }, }); const clients = host_clients_res.payload || []; const client_rules_res = await BunSQLite.select< BUN_SQLITE_WGUI_CLIENT_RULES, TableType >({ table: "client_rules", }); const client_ids = new Set( clients .map((client) => client.id) .filter((id): id is number => Boolean(id)), ); const rules_by_client_id = new Map< number, BUN_SQLITE_WGUI_CLIENT_RULES[] >(); for (let i = 0; i < (client_rules_res.payload || []).length; i++) { const rule = client_rules_res.payload?.[i]; if (!rule?.client_id || !client_ids.has(rule.client_id)) { continue; } const existing_rules = rules_by_client_id.get(rule.client_id) || []; existing_rules.push(rule); rules_by_client_id.set(rule.client_id, existing_rules); } const TARGET_INTERFACE = host?.interface || (await grabHostNetworkInterface()); const HOST_WG_IP = host?.wg_ip_address || wg_subnet_ip; if (!HOST_WG_IP) { return { success: false, msg: `No Host Private IP address provided`, }; } const is_ip_available = is_update_after_client_setup ? { success: true } : host?.wg_ip_address ? { success: true } : await checkPrivateIPAvailability({ ip_address: HOST_WG_IP, }); if (!is_ip_available.success && !is_update_after_client_setup) { return { success: false, msg: `IP not available`, }; } if (!is_update_after_client_setup) { let pre_sh = ``; pre_sh += `set -e\n`; pre_sh += `mkdir -p ${HOST_CONFIG_DIR}\n`; pre_sh += `mkdir -p ${HOST_CLIENTS_DIR}\n`; pre_sh += `mkdir -p ${HOST_IPTABLES_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" }); } catch (error: any) { return { success: false, msg: error.message, }; } } try { const HOST_PUBLIC_KEY = execSync(`cat ${HOST_PUBLIC_KEY_FILE}`, { encoding: "utf-8", }).trim(); const HOST_PRIVATE_KEY = execSync(`cat ${HOST_PRIVATE_KEY_FILE}`, { encoding: "utf-8", }).trim(); if (!is_update_after_client_setup) { 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`; sh += `cd ${HOST_CONFIG_DIR}\n`; const HOST_SUBNET = `${HOST_WG_IP.split(".").slice(0, 3).join(".")}.0/24`; const iptables_scripts = buildHostIptablesScripts({ host_id: Number(HOST_ID), interface_name: INTERFACE_NAME, target_interface: TARGET_INTERFACE || "eth0", host_subnet: HOST_SUBNET, clients: clients.map((client) => ({ id: client.id, wg_ip_address: client.wg_ip_address, rules: client.id && typeof client.id == "number" ? rules_by_client_id.get(client.id) || [] : [], })), }); if ( !iptables_scripts.success || !iptables_scripts.post_up || !iptables_scripts.post_down ) { return { success: false, msg: iptables_scripts.msg || `Could not build iptables scripts`, }; } execSync(`mkdir -p ${HOST_IPTABLES_DIR}`, { encoding: "utf-8" }); await Bun.write(POST_UP_PATH, iptables_scripts.post_up); await Bun.write(POST_DOWN_PATH, iptables_scripts.post_down); execSync(`chmod +x ${POST_UP_PATH} ${POST_DOWN_PATH}`, { encoding: "utf-8", }); sh += `cat > ${INTERFACE_NAME}.conf << EOF\n`; sh += `[Interface]\n`; sh += `Address = ${HOST_WG_IP}/24\n`; sh += `ListenPort = ${host?.listen_port || AppData["DefaultWGListenPort"]}\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) continue; const { CLIENT_PUBLIC_KEY_FILE } = grabClientDirnames({ client, host_id: HOST_ID, }); const CLIENT_PUBLIC_KEY = execSync( `cat ${CLIENT_PUBLIC_KEY_FILE}`, { encoding: "utf-8", }, ).trim(); sh += `[Peer]\n`; sh += `PublicKey = ${CLIENT_PUBLIC_KEY}\n`; sh += `AllowedIPs = ${client.wg_ip_address}/32\n`; sh += `\n`; } } sh += `EOF\n`; sh += `chmod 600 ${INTERFACE_NAME}.conf\n`; sh += `\n`; const exec = execSync(sh, { encoding: "utf-8" }); if (!is_update_after_client_setup) { execSync( `ip link set ${INTERFACE_NAME} down 2>/dev/null; ip link del ${INTERFACE_NAME} 2>/dev/null; true`, { encoding: "utf-8" }, ); } const manage_res = manageWireguardHost({ host_id: HOST_ID, action: "restart", }); if (!manage_res.success) { return { success: false, 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) { console.log( `Error setting up wireguard host ${HOST_ID} =>`, error.message, ); return { success: false, msg: error.message, }; } }