Files
wireguard-ui/src/functions/backend/setup/setup-wireguard-client.ts
T
tbenandClaude Opus 5.5 cd707465f4 Harden admin API routes and shell usage
- Require admin auth on interface-local-ip, network-interfaces,
  next-listen-port and public-ip (interface-local-ip and public-ip
  allowed unauthenticated command injection as root)
- Validate interface names and call ip/curl via execFileSync
- Write host/client WireGuard configs with fs instead of shell
  heredocs, validating IPs, AllowedIPs, endpoint and listen port
- Replace rm -rf/cat/mkdir/chmod/test shell calls with fs APIs
- Refuse private media requested through the public media route

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 21:09:27 +01:00

213 lines
6.1 KiB
TypeScript

import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_HOSTS,
} from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import grabDirNames from "@/src/utils/grab-dir-names";
import { execSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import validateIpv4 from "@/src/utils/validate-ipv4";
import validateAllowedIps from "@/src/utils/validate-allowed-ips";
import validateEndpointHost from "@/src/utils/validate-endpoint-host";
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, User } from "@/src/types";
import grabClientDirnames from "./grab-client-dir-names";
const { WIREGUARD_PUBLIC_KEY_FILE_NAME, WIREGUARD_CLIENT_CONFIG_FILE_NAME } =
grabDirNames();
type Params = {
client: BUN_SQLITE_WGUI_CLIENTS;
host?: BUN_SQLITE_WGUI_HOSTS;
user: User;
/**
* Skip the host setup/restart at the end. Use when
* regenerating multiple clients and restarting the
* host once afterwards instead.
*/
skip_host_setup?: boolean;
};
/**
* Function to setup a wireguard client. This
* function will eventually call the setupWireguardHost
* function afterwards to update it
* @param param0
* @returns
*/
export default async function setupWireguardClient({
client,
host,
user,
skip_host_setup,
}: Params): Promise<APIResponseObject> {
const host_id = host?.id || AppData["WireguardMainHostID"];
let resolved_host = host;
if (!resolved_host) {
const host_res = await BunSQLite.select<
BUN_SQLITE_WGUI_HOSTS,
TableType
>({
table: "hosts",
targetId: host_id,
});
resolved_host = host_res.singleRes || undefined;
}
const {
CLIENT_WG_IP,
CLIENT_CONFIG_FILE,
CLIENT_DIR,
CLIENT_PRIVATE_KEY_FILE,
CLIENT_PUBLIC_KEY_FILE,
HOST_CONFIG_DIR,
} = grabClientDirnames({ client, host_id });
if (!CLIENT_WG_IP) {
return {
success: false,
msg: `No Client Private IP address provided`,
};
}
if (!validateIpv4({ ip: CLIENT_WG_IP })) {
return {
success: false,
msg: `Invalid Client Private IP address: ${CLIENT_WG_IP}`,
};
}
if (
client?.allow_all_ips != 1 &&
client?.allowed_ips &&
!validateAllowedIps({ value: client.allowed_ips })
) {
return {
success: false,
msg: `Invalid Allowed IPs: ${client.allowed_ips}`,
};
}
const HOST_WG_IP = resolved_host?.wg_ip_address;
if (!HOST_WG_IP) {
return {
success: false,
msg: `No Host Private IP address provided`,
};
}
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 PUBLIC_IP_ADDRESS =
resolved_host?.public_ip_address ||
(await grabHostPublicIPAddress()) ||
client?.public_ip_address ||
HOST_WG_IP;
if (!validateEndpointHost({ value: PUBLIC_IP_ADDRESS })) {
return {
success: false,
msg: `Invalid Host public IP address: ${PUBLIC_IP_ADDRESS}`,
};
}
const LISTEN_PORT = Number(
resolved_host?.listen_port || AppData["DefaultWGListenPort"],
);
if (!Number.isInteger(LISTEN_PORT)) {
return {
success: false,
msg: `Invalid Host listen port: ${resolved_host?.listen_port}`,
};
}
const CLIENT_PRIVATE_KEY = readFileSync(
CLIENT_PRIVATE_KEY_FILE,
"utf-8",
).trim();
const HOST_PUBLIC_KEY = readFileSync(
path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME),
"utf-8",
).trim();
let CLIENT_CONFIG = ``;
CLIENT_CONFIG += `\n`;
CLIENT_CONFIG += `[Interface]\n`;
CLIENT_CONFIG += `Address = ${CLIENT_WG_IP}/32\n`;
CLIENT_CONFIG += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`;
if (client?.allow_all_ips == 1) {
CLIENT_CONFIG += `DNS = 1.1.1.1\n`;
}
CLIENT_CONFIG += `\n`;
CLIENT_CONFIG += `[Peer]\n`;
CLIENT_CONFIG += `PublicKey = ${HOST_PUBLIC_KEY}\n`;
CLIENT_CONFIG += `Endpoint = ${PUBLIC_IP_ADDRESS}:${LISTEN_PORT}\n`;
if (client?.allow_all_ips == 1) {
CLIENT_CONFIG += `AllowedIPs = 0.0.0.0/0, ::/0\n`;
} else if (client?.allowed_ips) {
CLIENT_CONFIG += `AllowedIPs = ${client.allowed_ips}\n`;
}
CLIENT_CONFIG += `PersistentKeepalive = 25\n`;
writeFileSync(CLIENT_CONFIG_FILE, CLIENT_CONFIG, { mode: 0o600 });
const host_setup_res = skip_host_setup
? { success: true }
: await setupWireguardHost({
host: resolved_host,
user,
is_update_after_client_setup: true,
});
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) {
console.log(
`Error setting up wireguard client ${client.id} for host ${host_id} =>`,
error.message,
);
return {
success: false,
msg: error.message,
};
}
}