Files
wireguard-ui/src/functions/backend/setup/setup-wireguard-host.ts
T
2026-09-20 17:26:11 +01:00

286 lines
8.5 KiB
TypeScript

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<APIResponseObject> {
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,
};
}
}