Major updates
This commit is contained in:
+1
-8
@@ -174,10 +174,6 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
|
|||||||
fieldName: "public_ip_address",
|
fieldName: "public_ip_address",
|
||||||
dataType: "TEXT",
|
dataType: "TEXT",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldName: "private_key",
|
|
||||||
dataType: "TEXT",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldName: "public_key",
|
fieldName: "public_key",
|
||||||
dataType: "TEXT",
|
dataType: "TEXT",
|
||||||
@@ -203,10 +199,6 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
|
|||||||
fieldName: "wg_ip_address",
|
fieldName: "wg_ip_address",
|
||||||
dataType: "TEXT",
|
dataType: "TEXT",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldName: "private_key",
|
|
||||||
dataType: "TEXT",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldName: "public_key",
|
fieldName: "public_key",
|
||||||
dataType: "TEXT",
|
dataType: "TEXT",
|
||||||
@@ -238,6 +230,7 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
|
|||||||
fieldName: "key",
|
fieldName: "key",
|
||||||
dataType: "TEXT",
|
dataType: "TEXT",
|
||||||
options: Variables.map((v) => v.value),
|
options: Variables.map((v) => v.value),
|
||||||
|
unique: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: "value",
|
fieldName: "value",
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ export type BUN_SQLITE_WGUI_CLIENTS = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
wg_ip_address?: string;
|
wg_ip_address?: string;
|
||||||
public_ip_address?: string;
|
public_ip_address?: string;
|
||||||
private_key?: string;
|
|
||||||
public_key?: string;
|
public_key?: string;
|
||||||
allowed_ips?: string;
|
allowed_ips?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
@@ -132,7 +131,6 @@ export type BUN_SQLITE_WGUI_HOSTS = {
|
|||||||
updated_at?: number | "";
|
updated_at?: number | "";
|
||||||
user_id?: number | "";
|
user_id?: number | "";
|
||||||
wg_ip_address?: string;
|
wg_ip_address?: string;
|
||||||
private_key?: string;
|
|
||||||
public_key?: string;
|
public_key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun src/server.ts",
|
"dev": "sudo bun src/server.ts",
|
||||||
"build": "bunx bunext build",
|
"build": "bunx bunext build",
|
||||||
"db:schema": "bunx bun-sqlite schema -t",
|
"db:schema": "bunx bun-sqlite schema -t",
|
||||||
"deploy:prod": "./deploy/deploy-prod.sh",
|
"deploy:prod": "./deploy/deploy-prod.sh",
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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 type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_WG_QUICK_MANAGE_SCRIPT } =
|
||||||
|
grabDirNames();
|
||||||
|
|
||||||
|
export type WireguardHostAction = "up" | "down" | "restart";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
action: WireguardHostAction;
|
||||||
|
host_id?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function manageWireguardHost({
|
||||||
|
action,
|
||||||
|
host_id = AppData["WireguardHostID"],
|
||||||
|
}: Params): APIResponseObject {
|
||||||
|
const INTERFACE_NAME = deriveWireguardInterfaceName({ host_id });
|
||||||
|
const HOST_CONFIG_PATH = path.join(
|
||||||
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
|
`${INTERFACE_NAME}.conf`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const IS_ROOT =
|
||||||
|
typeof process.getuid === "function" && process.getuid() === 0;
|
||||||
|
|
||||||
|
if (!IS_ROOT) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: "wg-ui must run as root to manage wireguard tunnels",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const MANAGE_COMMAND = `${WGUI_WG_QUICK_MANAGE_SCRIPT} ${action} ${INTERFACE_NAME} ${HOST_CONFIG_PATH}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const output = execSync(MANAGE_COMMAND, { encoding: "utf-8" }).trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
msg: output,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Could not ${action} ${INTERFACE_NAME}: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,20 +11,20 @@ import grabHostPublicIPAddress from "./grab-host-public-ip-address";
|
|||||||
import setupWireguardHost from "./setup-wireguard-host";
|
import setupWireguardHost from "./setup-wireguard-host";
|
||||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
import type { TableType } from "@/src/types";
|
import type { TableType, User } from "@/src/types";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
WGUI_LIB_CLIENTS_CONFIGS_DIR,
|
|
||||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||||
WGUI_LIB_KEYS_DIR,
|
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
|
||||||
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
||||||
} = grabDirNames();
|
} = grabDirNames();
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
client: BUN_SQLITE_WGUI_CLIENTS;
|
client: BUN_SQLITE_WGUI_CLIENTS;
|
||||||
host?: BUN_SQLITE_WGUI_HOSTS;
|
host?: BUN_SQLITE_WGUI_HOSTS;
|
||||||
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,6 +37,7 @@ type Params = {
|
|||||||
export default async function setupWireguardClient({
|
export default async function setupWireguardClient({
|
||||||
client,
|
client,
|
||||||
host,
|
host,
|
||||||
|
user,
|
||||||
}: Params): Promise<APIResponseObject> {
|
}: Params): Promise<APIResponseObject> {
|
||||||
const host_id = host?.id || AppData["WireguardHostID"];
|
const host_id = host?.id || AppData["WireguardHostID"];
|
||||||
|
|
||||||
@@ -58,9 +59,21 @@ export default async function setupWireguardClient({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOST_WG_IP =
|
const HOST_CONFIG_DIR = path.join(
|
||||||
host?.wg_ip_address ||
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
variables?.find((v) => v.key == "main_host_wg_ip_address")?.value;
|
String(host_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const HOST_CLIENTS_DIR = path.join(
|
||||||
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
|
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
const CLIENT_DIR = path.join(HOST_CLIENTS_DIR, `${client.id}`);
|
||||||
|
|
||||||
|
const HOST_WG_IP = host?.id
|
||||||
|
? host?.wg_ip_address
|
||||||
|
: variables?.find((v) => v.key == "main_host_wg_ip_address")?.value;
|
||||||
|
|
||||||
if (!HOST_WG_IP) {
|
if (!HOST_WG_IP) {
|
||||||
return {
|
return {
|
||||||
@@ -69,8 +82,6 @@ export default async function setupWireguardClient({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLIENT_DIR = path.join(WGUI_LIB_CLIENTS_CONFIGS_DIR, `${client.id}`);
|
|
||||||
|
|
||||||
const CLIENT_PRIVATE_KEY_FILE = path.join(
|
const CLIENT_PRIVATE_KEY_FILE = path.join(
|
||||||
CLIENT_DIR,
|
CLIENT_DIR,
|
||||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||||
@@ -101,66 +112,44 @@ export default async function setupWireguardClient({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const CLIENT_PRIVATE_KEY =
|
const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
|
||||||
client?.private_key ||
|
encoding: "utf-8",
|
||||||
execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
|
}).trim();
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
if (CLIENT_PRIVATE_KEY && !client.private_key && client.id) {
|
const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
|
||||||
await BunSQLite.update<BUN_SQLITE_WGUI_CLIENTS, TableType>({
|
encoding: "utf-8",
|
||||||
data: { private_key: CLIENT_PRIVATE_KEY },
|
}).trim();
|
||||||
table: "clients",
|
|
||||||
targetId: client.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const CLIENT_PUBLIC_KEY =
|
const HOST_PUBLIC_KEY = execSync(
|
||||||
client?.public_key ||
|
`cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
||||||
execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
|
{ encoding: "utf-8" },
|
||||||
encoding: "utf-8",
|
).trim();
|
||||||
}).trim();
|
|
||||||
|
|
||||||
if (CLIENT_PUBLIC_KEY && !client.public_key && client.id) {
|
|
||||||
await BunSQLite.update<BUN_SQLITE_WGUI_CLIENTS, TableType>({
|
|
||||||
data: { public_key: CLIENT_PUBLIC_KEY },
|
|
||||||
table: "clients",
|
|
||||||
targetId: client.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const HOST_PUBLIC_KEY =
|
|
||||||
host?.public_key ||
|
|
||||||
execSync(
|
|
||||||
`cat ${path.join(
|
|
||||||
WGUI_LIB_HOSTS_CONFIGS_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 =
|
const PUBLIC_IP_ADDRESS =
|
||||||
(await grabHostPublicIPAddress()) ||
|
(await grabHostPublicIPAddress()) ||
|
||||||
client?.public_ip_address ||
|
client?.public_ip_address ||
|
||||||
HOST_WG_IP;
|
HOST_WG_IP;
|
||||||
|
|
||||||
const sh = `
|
let sh = ``;
|
||||||
cd ${CLIENT_DIR}
|
|
||||||
|
|
||||||
cat > ${WIREGUARD_CLIENT_CONFIG_FILE_NAME} << EOF
|
sh += `cd ${CLIENT_DIR}\n`;
|
||||||
[Interface]
|
sh += `cat > ${WIREGUARD_CLIENT_CONFIG_FILE_NAME} << EOF\n`;
|
||||||
Address = ${CLIENT_WG_IP}/32
|
sh += `\n`;
|
||||||
PrivateKey = ${CLIENT_PRIVATE_KEY}
|
sh += `[Interface]\n`;
|
||||||
DNS = 1.1.1.1
|
sh += `Address = ${CLIENT_WG_IP}/32\n`;
|
||||||
|
sh += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`;
|
||||||
|
sh += `DNS = 1.1.1.1\n`;
|
||||||
|
sh += `\n`;
|
||||||
|
sh += `[Peer]\n`;
|
||||||
|
sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`;
|
||||||
|
sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:51820\n`;
|
||||||
|
|
||||||
[Peer]
|
if (client?.allowed_ips) {
|
||||||
PublicKey = ${HOST_PUBLIC_KEY}
|
sh += `AllowedIPs = ${client.allowed_ips}\n`;
|
||||||
Endpoint = ${PUBLIC_IP_ADDRESS}:51820
|
}
|
||||||
AllowedIPs = ${ALLOWED_IPS}
|
|
||||||
EOF
|
sh += `EOF\n`;
|
||||||
`;
|
sh += `\n`;
|
||||||
|
|
||||||
const exec = execSync(sh, { encoding: "utf-8" });
|
const exec = execSync(sh, { encoding: "utf-8" });
|
||||||
|
|
||||||
@@ -168,7 +157,7 @@ EOF
|
|||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
});
|
});
|
||||||
|
|
||||||
const host_setup_res = await setupWireguardHost({ host });
|
const host_setup_res = await setupWireguardHost({ host, user });
|
||||||
|
|
||||||
if (!host_setup_res.success) {
|
if (!host_setup_res.success) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,31 +11,50 @@ import path from "node:path";
|
|||||||
import grabHostNetworkInterface from "./grab-host-network-interface";
|
import grabHostNetworkInterface from "./grab-host-network-interface";
|
||||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
import type { TableType } from "@/src/types";
|
import type { TableType, User } from "@/src/types";
|
||||||
import checkPrivateIPAvailability from "./check-private-ip-availability";
|
import checkPrivateIPAvailability from "./check-private-ip-availability";
|
||||||
|
import manageWireguardHost from "./manage-wireguard-host";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
WGUI_LIB_IP_TABLES_DIR,
|
|
||||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
WGUI_WG_QUICK_SYSTEMD_SCRIPT,
|
|
||||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||||
|
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
|
||||||
|
WGUI_LIB_HOST_IPTABLES_DIR_NAME,
|
||||||
} = grabDirNames();
|
} = grabDirNames();
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
host?: BUN_SQLITE_WGUI_HOSTS;
|
host?: BUN_SQLITE_WGUI_HOSTS;
|
||||||
wg_subnet_ip?: string;
|
wg_subnet_ip?: string;
|
||||||
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function setupWireguardHost({
|
export default async function setupWireguardHost({
|
||||||
host,
|
host,
|
||||||
wg_subnet_ip,
|
wg_subnet_ip,
|
||||||
|
user,
|
||||||
}: Params): Promise<APIResponseObject> {
|
}: Params): Promise<APIResponseObject> {
|
||||||
const host_id = host?.id || AppData["WireguardHostID"];
|
const host_id = host?.id || AppData["WireguardHostID"];
|
||||||
|
|
||||||
const INTERFACE_NAME = deriveWireguardInterfaceName({ host_id });
|
const INTERFACE_NAME = deriveWireguardInterfaceName({ host_id });
|
||||||
const HOST_CONFIG_PATH = path.join(
|
|
||||||
|
const HOST_CONFIG_DIR = path.join(
|
||||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
|
String(host_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const HOST_CLIENTS_DIR = path.join(
|
||||||
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
|
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
const HOST_IPTABLES_DIR = path.join(
|
||||||
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
|
WGUI_LIB_HOST_IPTABLES_DIR_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
const HOST_CONFIG_FILE = path.join(
|
||||||
|
HOST_CONFIG_DIR,
|
||||||
`${INTERFACE_NAME}.conf`,
|
`${INTERFACE_NAME}.conf`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,16 +110,14 @@ export default async function setupWireguardHost({
|
|||||||
let pre_sh = ``;
|
let pre_sh = ``;
|
||||||
|
|
||||||
pre_sh += `set -e\n`;
|
pre_sh += `set -e\n`;
|
||||||
pre_sh += `mkdir -p ${WGUI_LIB_HOSTS_CONFIGS_DIR}\n`;
|
pre_sh += `mkdir -p ${HOST_CONFIG_DIR}\n`;
|
||||||
pre_sh += `cd ${WGUI_LIB_HOSTS_CONFIGS_DIR}\n`;
|
pre_sh += `cd ${HOST_CONFIG_DIR}\n`;
|
||||||
pre_sh += `if [ ! -f ${WIREGUARD_PRIVATE_KEY_FILE_NAME} ]; then\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 += ` wg genkey | tee ${WIREGUARD_PRIVATE_KEY_FILE_NAME} | wg pubkey > ${WIREGUARD_PUBLIC_KEY_FILE_NAME}\n`;
|
||||||
pre_sh += `fi\n`;
|
pre_sh += `fi\n`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" });
|
const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" });
|
||||||
|
|
||||||
console.log("exec_pre_setup", exec_pre_setup);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -109,31 +126,66 @@ export default async function setupWireguardHost({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const HOST_PUBLIC_KEY = host?.id
|
const HOST_PUBLIC_KEY = execSync(
|
||||||
? host.public_key
|
`cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
||||||
: execSync(
|
{ encoding: "utf-8" },
|
||||||
`cat ${path.join(WGUI_LIB_HOSTS_CONFIGS_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
).trim();
|
||||||
);
|
|
||||||
const HOST_PRIVATE_KEY = host?.private_key
|
const HOST_PRIVATE_KEY = execSync(
|
||||||
? host.private_key
|
`cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME)}`,
|
||||||
: execSync(
|
{ encoding: "utf-8" },
|
||||||
`cat ${path.join(WGUI_LIB_HOSTS_CONFIGS_DIR, WIREGUARD_PRIVATE_KEY_FILE_NAME)}`,
|
).trim();
|
||||||
);
|
|
||||||
|
if (host_id == 0) {
|
||||||
|
const update_variables = await BunSQLite.insert<
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "variables",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
key: "main_host_wg_ip_address",
|
||||||
|
value: HOST_WG_IP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "main_host_wg_public_key",
|
||||||
|
value: HOST_PUBLIC_KEY,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
update_on_duplicate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("update_variables", update_variables);
|
||||||
|
|
||||||
|
if (!update_variables.success) {
|
||||||
|
throw new Error(`Couldn't update host variables`);
|
||||||
|
}
|
||||||
|
} else if (host_id) {
|
||||||
|
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 = ``;
|
let sh = ``;
|
||||||
|
|
||||||
sh += `set -e\n`;
|
sh += `set -e\n`;
|
||||||
|
|
||||||
const POST_UP_PATH = path.join(
|
const POST_UP_PATH = path.join(HOST_IPTABLES_DIR, `up.sh`);
|
||||||
WGUI_LIB_IP_TABLES_DIR,
|
const POST_DOWN_PATH = path.join(HOST_IPTABLES_DIR, `down.sh`);
|
||||||
`${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 += `cd ${HOST_CONFIG_DIR}\n`;
|
||||||
|
|
||||||
sh += `cat > ${POST_UP_PATH} << EOF\n`;
|
sh += `cat > ${POST_UP_PATH} << EOF\n`;
|
||||||
sh += `#!/bin/bash\n\n`;
|
sh += `#!/bin/bash\n\n`;
|
||||||
@@ -147,6 +199,7 @@ export default async function setupWireguardHost({
|
|||||||
sh += `\n`;
|
sh += `\n`;
|
||||||
sh += `iptables -t nat -A POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`;
|
sh += `iptables -t nat -A POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`;
|
||||||
sh += `EOF\n`;
|
sh += `EOF\n`;
|
||||||
|
sh += `chmod +x ${POST_UP_PATH}\n`;
|
||||||
|
|
||||||
sh += `\n`;
|
sh += `\n`;
|
||||||
|
|
||||||
@@ -162,6 +215,7 @@ export default async function setupWireguardHost({
|
|||||||
sh += `\n`;
|
sh += `\n`;
|
||||||
sh += `iptables -t nat -D POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`;
|
sh += `iptables -t nat -D POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`;
|
||||||
sh += `EOF\n`;
|
sh += `EOF\n`;
|
||||||
|
sh += `chmod +x ${POST_DOWN_PATH}\n`;
|
||||||
|
|
||||||
sh += `\n`;
|
sh += `\n`;
|
||||||
|
|
||||||
@@ -192,36 +246,26 @@ export default async function setupWireguardHost({
|
|||||||
|
|
||||||
const exec = execSync(sh, { encoding: "utf-8" });
|
const exec = execSync(sh, { encoding: "utf-8" });
|
||||||
|
|
||||||
const IS_ROOT =
|
const manage_res = manageWireguardHost({
|
||||||
typeof process.getuid === "function" && process.getuid() === 0;
|
host_id,
|
||||||
const SUDO_PREFIX = IS_ROOT ? "" : "sudo -n ";
|
action: "restart",
|
||||||
const MANAGE_WG_QUICK_CMD = `${SUDO_PREFIX}${WGUI_WG_QUICK_SYSTEMD_SCRIPT} ${INTERFACE_NAME} ${HOST_CONFIG_PATH}`;
|
});
|
||||||
|
|
||||||
let exec_systemd = ``;
|
if (!manage_res.success) {
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
success: false,
|
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}`,
|
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) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
msg: error.message,
|
msg: error.message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import manageWireguardHost from "./manage-wireguard-host";
|
||||||
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
|
import type {
|
||||||
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
|
} from "@/db/types/db";
|
||||||
|
import type { TableType } from "@/src/types";
|
||||||
|
import { AppData } from "@/src/data/app-data";
|
||||||
|
|
||||||
|
const HOST_CONFIG_FILE_NAME_PATTERN = /^wgui(\d+)\.conf$/;
|
||||||
|
|
||||||
|
export default async function syncWireguardHosts() {
|
||||||
|
const { WGUI_LIB_HOSTS_CONFIGS_DIR } = grabDirNames();
|
||||||
|
|
||||||
|
let host_config_file_names: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
host_config_file_names = fs
|
||||||
|
.readdirSync(WGUI_LIB_HOSTS_CONFIGS_DIR)
|
||||||
|
.filter((file_name) =>
|
||||||
|
HOST_CONFIG_FILE_NAME_PATTERN.test(file_name),
|
||||||
|
);
|
||||||
|
|
||||||
|
const variables = await BunSQLite.select<
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "variables",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hosts = await BunSQLite.select<BUN_SQLITE_WGUI_HOSTS, TableType>({
|
||||||
|
table: "hosts",
|
||||||
|
});
|
||||||
|
|
||||||
|
const main_host_id = AppData["WireguardHostID"];
|
||||||
|
const main_host_ip = variables.payload?.find(
|
||||||
|
(v) => v.key == "main_host_wg_ip_address",
|
||||||
|
)?.value;
|
||||||
|
|
||||||
|
if (!main_host_ip) {
|
||||||
|
throw new Error(`Main Host not set yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const host_config_file_name of host_config_file_names) {
|
||||||
|
const host_id = Number(
|
||||||
|
host_config_file_name.match(HOST_CONFIG_FILE_NAME_PATTERN)?.[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = manageWireguardHost({ action: "up", host_id });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[wgui] wireguard host ${host_config_file_name}: ${
|
||||||
|
res.success ? `up` : `failed — ${res.msg}`
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(`[wgui] skipping wireguard host sync — ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,7 @@ type Params = {
|
|||||||
clients?: BUN_SQLITE_WGUI_CLIENTS[];
|
clients?: BUN_SQLITE_WGUI_CLIENTS[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function deriveHostConfig({
|
export default function deriveHostConfig({ host, variables, clients }: Params) {
|
||||||
host,
|
|
||||||
variables,
|
|
||||||
clients,
|
|
||||||
}: Params) {
|
|
||||||
const host_id = host?.id || AppData["WireguardHostID"];
|
const host_id = host?.id || AppData["WireguardHostID"];
|
||||||
|
|
||||||
const wg_ip_address =
|
const wg_ip_address =
|
||||||
@@ -33,16 +29,14 @@ export default function deriveHostConfig({
|
|||||||
host_id,
|
host_id,
|
||||||
interface_name,
|
interface_name,
|
||||||
config_path: `${HOSTS_CONFIG_DIR}/${interface_name}.conf`,
|
config_path: `${HOSTS_CONFIG_DIR}/${interface_name}.conf`,
|
||||||
systemd_unit_name: `wg-quick@${interface_name}.service`,
|
|
||||||
|
|
||||||
address: wg_ip_address ? `${wg_ip_address}/24` : undefined,
|
address: wg_ip_address ? `${wg_ip_address}/24` : undefined,
|
||||||
listen_port: LISTEN_PORT,
|
listen_port: LISTEN_PORT,
|
||||||
post_up: `${IP_TABLES_DIR}/${host_id}-up.sh`,
|
post_up: `${IP_TABLES_DIR}/${host_id}-up.sh`,
|
||||||
post_down: `${IP_TABLES_DIR}/${host_id}-down.sh`,
|
post_down: `${IP_TABLES_DIR}/${host_id}-down.sh`,
|
||||||
private_key: host?.private_key,
|
|
||||||
public_key: host?.public_key,
|
public_key: host?.public_key,
|
||||||
client_count: clients?.filter(
|
client_count:
|
||||||
(client) => (client.host_id || 0) == host_id,
|
clients?.filter((client) => (client.host_id || 0) == host_id)
|
||||||
).length || 0,
|
.length || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,12 +30,7 @@ export default function InterfaceConfigSection({
|
|||||||
const rows = [
|
const rows = [
|
||||||
{ keyName: "Address", value: config.address || "—" },
|
{ keyName: "Address", value: config.address || "—" },
|
||||||
{ keyName: "ListenPort", value: String(config.listen_port) },
|
{ keyName: "ListenPort", value: String(config.listen_port) },
|
||||||
{
|
,
|
||||||
keyName: "PrivateKey",
|
|
||||||
value: config.private_key
|
|
||||||
? "•••••••••••• (redacted)"
|
|
||||||
: "Not set",
|
|
||||||
},
|
|
||||||
{ keyName: "PostUp", value: config.post_up },
|
{ keyName: "PostUp", value: config.post_up },
|
||||||
{ keyName: "PostDown", value: config.post_down },
|
{ keyName: "PostDown", value: config.post_down },
|
||||||
];
|
];
|
||||||
@@ -43,7 +38,9 @@ export default function InterfaceConfigSection({
|
|||||||
function buildConfigText() {
|
function buildConfigText() {
|
||||||
const lines = ["[Interface]"];
|
const lines = ["[Interface]"];
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
lines.push(`${row.keyName} = ${row.value}`);
|
if (row) {
|
||||||
|
lines.push(`${row.keyName} = ${row.value}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(
|
lines.push(
|
||||||
@@ -59,12 +56,12 @@ export default function InterfaceConfigSection({
|
|||||||
<H2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60 mb-0!">
|
<H2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60 mb-0!">
|
||||||
Interface configuration
|
Interface configuration
|
||||||
</H2>
|
</H2>
|
||||||
<P noMargin className="font-mono text-[11.5px] text-foreground-light/40 dark:text-foreground-dark/40">
|
<P
|
||||||
|
noMargin
|
||||||
|
className="font-mono text-[11.5px] text-foreground-light/40 dark:text-foreground-dark/40"
|
||||||
|
>
|
||||||
{config.config_path}
|
{config.config_path}
|
||||||
</P>
|
</P>
|
||||||
<P noMargin className="font-mono text-[11.5px] text-foreground-light/40 dark:text-foreground-dark/40">
|
|
||||||
{config.systemd_unit_name}
|
|
||||||
</P>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<AdminButton
|
<AdminButton
|
||||||
Icon={Copy}
|
Icon={Copy}
|
||||||
@@ -79,13 +76,17 @@ export default function InterfaceConfigSection({
|
|||||||
<Span className="px-5 py-3 font-mono text-[12.5px] font-semibold text-secondary dark:text-secondary block">
|
<Span className="px-5 py-3 font-mono text-[12.5px] font-semibold text-secondary dark:text-secondary block">
|
||||||
[Interface]
|
[Interface]
|
||||||
</Span>
|
</Span>
|
||||||
{rows.map((row) => (
|
{rows.map((row) => {
|
||||||
<InterfaceConfigRow
|
if (!row) return;
|
||||||
key={row.keyName}
|
|
||||||
keyName={row.keyName}
|
return (
|
||||||
value={row.value}
|
<InterfaceConfigRow
|
||||||
/>
|
key={row.keyName}
|
||||||
))}
|
keyName={row.keyName}
|
||||||
|
value={row.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<Row className="justify-between gap-4 px-5 py-2.5 border-t border-slate-200/60 dark:border-white/5">
|
<Row className="justify-between gap-4 px-5 py-2.5 border-t border-slate-200/60 dark:border-white/5">
|
||||||
<Span className="font-mono text-[12px] text-foreground-light/35 dark:text-foreground-dark/35">
|
<Span className="font-mono text-[12px] text-foreground-light/35 dark:text-foreground-dark/35">
|
||||||
# Public key ·{" "}
|
# Public key ·{" "}
|
||||||
@@ -97,4 +98,4 @@ export default function InterfaceConfigSection({
|
|||||||
</div>
|
</div>
|
||||||
</AdminCard>
|
</AdminCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
|
||||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||||
import grabNextAvailablePrivateIPSubnet from "@/src/functions/backend/setup/grab-next-available-private-ip-subnet";
|
import grabNextAvailablePrivateIPSubnet from "@/src/functions/backend/setup/grab-next-available-private-ip-subnet";
|
||||||
import setupWireguardHost from "@/src/functions/backend/setup/setup-wireguard-host";
|
|
||||||
import type { ApiReqParams } from "@/src/types";
|
|
||||||
import type {
|
import type {
|
||||||
APIResponseObject,
|
APIResponseObject,
|
||||||
BunextAPIRouteHandler,
|
BunextAPIRouteHandler,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
host: {
|
host: {
|
||||||
wg_ip_address: body.ip_address,
|
wg_ip_address: body.ip_address,
|
||||||
},
|
},
|
||||||
|
user,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ log() { echo "==> $*"; }
|
|||||||
fail() { echo "error: $*" >&2; exit 1; }
|
fail() { echo "error: $*" >&2; exit 1; }
|
||||||
|
|
||||||
WGUI_LIB_DIR="/var/lib/wgui"
|
WGUI_LIB_DIR="/var/lib/wgui"
|
||||||
WG_QUICK_HELPER="$WGUI_LIB_DIR/scripts/wg-quick-systemd.sh"
|
WG_QUICK_HELPER="$WGUI_LIB_DIR/scripts/wg-quick-manage.sh"
|
||||||
INSTALL_DIR="${INSTALL_DIR:-}"
|
INSTALL_DIR="${INSTALL_DIR:-}"
|
||||||
SERVICE_USER="${SERVICE_USER:-}"
|
|
||||||
SERVICE_GROUP="${SERVICE_GROUP:-}"
|
|
||||||
SERVICE_NAME="${SERVICE_NAME:-wgui}"
|
SERVICE_NAME="${SERVICE_NAME:-wgui}"
|
||||||
REPO_URL="${REPO_URL:-}"
|
REPO_URL="${REPO_URL:-}"
|
||||||
BRANCH="${BRANCH:-main}"
|
BRANCH="${BRANCH:-main}"
|
||||||
@@ -42,18 +40,8 @@ require_root() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve_service_user() {
|
|
||||||
if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] && id -u "$SUDO_USER" >/dev/null 2>&1; then
|
|
||||||
echo "$SUDO_USER"
|
|
||||||
else
|
|
||||||
echo "root"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_root
|
require_root
|
||||||
SERVICE_USER="${SERVICE_USER:-$(resolve_service_user)}"
|
log "running the wg-ui service as root"
|
||||||
SERVICE_GROUP="${SERVICE_GROUP:-$(id -gn "$SERVICE_USER")}"
|
|
||||||
log "running the wg-ui service as $SERVICE_USER (group: $SERVICE_GROUP)"
|
|
||||||
|
|
||||||
if [ "$DEV_MODE" = true ]; then
|
if [ "$DEV_MODE" = true ]; then
|
||||||
INSTALL_DIR="${INSTALL_DIR:-$REPO_ROOT}"
|
INSTALL_DIR="${INSTALL_DIR:-$REPO_ROOT}"
|
||||||
@@ -125,10 +113,10 @@ install_bun() {
|
|||||||
log "bun: $($BUN_BIN --version)"
|
log "bun: $($BUN_BIN --version)"
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_service_user() {
|
setup_lib_dirs() {
|
||||||
mkdir -p "$WGUI_LIB_DIR/iptables" "$WGUI_LIB_DIR/keys" "$WGUI_LIB_DIR/clients" "$WGUI_LIB_DIR/hosts" "$WGUI_LIB_DIR/scripts"
|
mkdir -p "$WGUI_LIB_DIR/iptables" "$WGUI_LIB_DIR/keys" "$WGUI_LIB_DIR/clients" "$WGUI_LIB_DIR/hosts" "$WGUI_LIB_DIR/scripts"
|
||||||
chown -R "$SERVICE_USER:$SERVICE_GROUP" "$WGUI_LIB_DIR"
|
chmod 700 "$WGUI_LIB_DIR/iptables" "$WGUI_LIB_DIR/keys" "$WGUI_LIB_DIR/clients" "$WGUI_LIB_DIR/hosts"
|
||||||
chmod 770 "$WGUI_LIB_DIR/hosts"
|
chmod 755 "$WGUI_LIB_DIR/scripts"
|
||||||
}
|
}
|
||||||
|
|
||||||
clone_or_update() {
|
clone_or_update() {
|
||||||
@@ -149,22 +137,12 @@ clone_or_update() {
|
|||||||
log "cloning wg-ui ($REPO_URL, branch $BRANCH) ..."
|
log "cloning wg-ui ($REPO_URL, branch $BRANCH) ..."
|
||||||
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
|
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
|
||||||
fi
|
fi
|
||||||
chown -R "$SERVICE_USER:$SERVICE_GROUP" "$INSTALL_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_as_service_user() {
|
|
||||||
local cmd
|
|
||||||
cmd="cd $(printf '%q' "$INSTALL_DIR") && export PATH=/usr/local/bin:/usr/bin:/bin && $(printf '%q ' "$@")"
|
|
||||||
if command -v runuser >/dev/null 2>&1; then
|
|
||||||
runuser -u "$SERVICE_USER" -- bash -c "$cmd"
|
|
||||||
else
|
|
||||||
su -s /bin/bash -c "$cmd" "$SERVICE_USER"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
install_dependencies() {
|
install_dependencies() {
|
||||||
log "installing app dependencies with bun ..."
|
log "installing app dependencies with bun ..."
|
||||||
run_as_service_user "$BUN_BIN" install
|
cd "$INSTALL_DIR"
|
||||||
|
"$BUN_BIN" install
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_env_file() {
|
ensure_env_file() {
|
||||||
@@ -176,7 +154,6 @@ ensure_env_file() {
|
|||||||
echo "ENCRYPTION_SALT=$(openssl rand -base64 32 | tr -d '\n')"
|
echo "ENCRYPTION_SALT=$(openssl rand -base64 32 | tr -d '\n')"
|
||||||
echo "DATA_DIR=$INSTALL_DIR/.data"
|
echo "DATA_DIR=$INSTALL_DIR/.data"
|
||||||
} > "$INSTALL_DIR/.env"
|
} > "$INSTALL_DIR/.env"
|
||||||
chown "$SERVICE_USER:$SERVICE_GROUP" "$INSTALL_DIR/.env"
|
|
||||||
chmod 600 "$INSTALL_DIR/.env"
|
chmod 600 "$INSTALL_DIR/.env"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -190,31 +167,14 @@ setup_wireguard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_wg_quick_helper() {
|
install_wg_quick_helper() {
|
||||||
local helper_src="$INSTALL_DIR/src/scripts/wg-quick-systemd.sh"
|
local helper_src="$INSTALL_DIR/src/scripts/wg-quick-manage.sh"
|
||||||
if [ ! -f "$helper_src" ]; then
|
if [ ! -f "$helper_src" ]; then
|
||||||
fail "missing $helper_src — cannot install the wg-quick systemd helper"
|
fail "missing $helper_src — cannot install the wg-quick helper"
|
||||||
fi
|
fi
|
||||||
log "installing wg-quick systemd helper to $WG_QUICK_HELPER ..."
|
log "installing wg-quick helper to $WG_QUICK_HELPER ..."
|
||||||
install -m 755 -o root -g root "$helper_src" "$WG_QUICK_HELPER"
|
install -m 755 -o root -g root "$helper_src" "$WG_QUICK_HELPER"
|
||||||
}
|
}
|
||||||
|
|
||||||
install_wg_quick_sudoers() {
|
|
||||||
if [ "$SERVICE_USER" = "root" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if ! command -v visudo >/dev/null 2>&1; then
|
|
||||||
log "visudo not found — skipping sudoers entry for $SERVICE_USER (tunnel management will require root)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local sudoers_file="/etc/sudoers.d/wgui-wg-quick"
|
|
||||||
log "granting $SERVICE_USER passwordless access to $WG_QUICK_HELPER ..."
|
|
||||||
cat > "$sudoers_file" <<EOF
|
|
||||||
$SERVICE_USER ALL=(root) NOPASSWD: $WG_QUICK_HELPER *
|
|
||||||
EOF
|
|
||||||
chmod 440 "$sudoers_file"
|
|
||||||
visudo -cf "$sudoers_file" >/dev/null 2>&1 || fail "invalid sudoers file $sudoers_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_systemd_unit() {
|
install_systemd_unit() {
|
||||||
local unit="/etc/systemd/system/$SERVICE_NAME.service"
|
local unit="/etc/systemd/system/$SERVICE_NAME.service"
|
||||||
log "writing systemd unit $unit ..."
|
log "writing systemd unit $unit ..."
|
||||||
@@ -226,8 +186,8 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=$SERVICE_USER
|
User=root
|
||||||
Group=$SERVICE_GROUP
|
Group=root
|
||||||
WorkingDirectory=$INSTALL_DIR
|
WorkingDirectory=$INSTALL_DIR
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
ExecStart=$BUN_BIN src/server.ts
|
ExecStart=$BUN_BIN src/server.ts
|
||||||
@@ -257,7 +217,7 @@ name="$SERVICE_NAME"
|
|||||||
description="Wireguard UI"
|
description="Wireguard UI"
|
||||||
command="$BUN_BIN"
|
command="$BUN_BIN"
|
||||||
command_args="src/server.ts"
|
command_args="src/server.ts"
|
||||||
command_user="$SERVICE_USER"
|
command_user="root"
|
||||||
directory="$INSTALL_DIR"
|
directory="$INSTALL_DIR"
|
||||||
output_log="/var/log/$SERVICE_NAME.log"
|
output_log="/var/log/$SERVICE_NAME.log"
|
||||||
error_log="/var/log/$SERVICE_NAME.log"
|
error_log="/var/log/$SERVICE_NAME.log"
|
||||||
@@ -288,7 +248,7 @@ grab_port() {
|
|||||||
|
|
||||||
install_deps
|
install_deps
|
||||||
install_bun
|
install_bun
|
||||||
setup_service_user
|
setup_lib_dirs
|
||||||
if [ "$DEV_MODE" = false ]; then
|
if [ "$DEV_MODE" = false ]; then
|
||||||
clone_or_update
|
clone_or_update
|
||||||
fi
|
fi
|
||||||
@@ -296,7 +256,6 @@ install_dependencies
|
|||||||
ensure_env_file
|
ensure_env_file
|
||||||
setup_wireguard
|
setup_wireguard
|
||||||
install_wg_quick_helper
|
install_wg_quick_helper
|
||||||
install_wg_quick_sudoers
|
|
||||||
|
|
||||||
if [ "$DEV_MODE" = false ]; then
|
if [ "$DEV_MODE" = false ]; then
|
||||||
case "$INIT_SYSTEM" in
|
case "$INIT_SYSTEM" in
|
||||||
@@ -308,7 +267,7 @@ if [ "$DEV_MODE" = false ]; then
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log "no supported init system found — start manually with:
|
log "no supported init system found — start manually with:
|
||||||
su -s /bin/bash $SERVICE_USER -c 'cd $INSTALL_DIR && NODE_ENV=production $BUN_BIN src/server.ts'
|
cd $INSTALL_DIR && NODE_ENV=production $BUN_BIN src/server.ts
|
||||||
(add the line above to your boot scripts)"
|
(add the line above to your boot scripts)"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -318,9 +277,9 @@ PORT="$(grab_port)"
|
|||||||
log "wg-ui install complete."
|
log "wg-ui install complete."
|
||||||
log "webapp: $INSTALL_DIR"
|
log "webapp: $INSTALL_DIR"
|
||||||
log "runtime: $WGUI_LIB_DIR (host configs, keys, iptables, client configs)"
|
log "runtime: $WGUI_LIB_DIR (host configs, keys, iptables, client configs)"
|
||||||
log "tunnels: systemd units wg-quick@wgui<host_id>.service (configs in $WGUI_LIB_DIR/hosts — /etc/wireguard is never touched)"
|
log "tunnels: managed by the web server via wg-quick up/down (configs in $WGUI_LIB_DIR/hosts — /etc/wireguard is never touched)"
|
||||||
if [ "$DEV_MODE" = true ]; then
|
if [ "$DEV_MODE" = true ]; then
|
||||||
log "process: development mode — no system service installed"
|
log "process: development mode — no system service installed; run the dev server as root (e.g. after sudo -i)"
|
||||||
else
|
else
|
||||||
log "process: managed by $INIT_SYSTEM as $SERVICE_NAME"
|
log "process: managed by $INIT_SYSTEM as $SERVICE_NAME"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -79,9 +79,10 @@ done
|
|||||||
|
|
||||||
# Configs are project-scoped under /var/lib/wgui (hosts + helper scripts),
|
# Configs are project-scoped under /var/lib/wgui (hosts + helper scripts),
|
||||||
# never /etc/wireguard, so stock wg-quick setups on this machine are untouched.
|
# never /etc/wireguard, so stock wg-quick setups on this machine are untouched.
|
||||||
|
# Everything runs as root: hosts holds private keys and is written by the web
|
||||||
|
# server, and the helper script is invoked directly (no sudoers entry).
|
||||||
mkdir -p /var/lib/wgui/hosts /var/lib/wgui/scripts
|
mkdir -p /var/lib/wgui/hosts /var/lib/wgui/scripts
|
||||||
chown root:root /var/lib/wgui/hosts /var/lib/wgui/scripts
|
chown root:root /var/lib/wgui/scripts
|
||||||
chmod 700 /var/lib/wgui/hosts
|
|
||||||
chmod 755 /var/lib/wgui/scripts
|
chmod 755 /var/lib/wgui/scripts
|
||||||
|
|
||||||
echo "wireguard setup complete."
|
echo "wireguard setup complete."
|
||||||
|
|||||||
Executable
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Manages a single wireguard-ui host tunnel directly with wg-quick.
|
||||||
|
#
|
||||||
|
# This project deliberately avoids /etc/wireguard entirely so it can never
|
||||||
|
# collide with an existing WireGuard setup. Host configs live under
|
||||||
|
# /var/lib/wgui/hosts and the web server brings tunnels up/down through this
|
||||||
|
# helper instead of systemd. Interfaces follow the project convention
|
||||||
|
# wgui<host_id> (e.g. wgui0), never stock names like wg0.
|
||||||
|
#
|
||||||
|
# Usage: wg-quick-manage.sh <up|down|restart> <interface> <config-path>
|
||||||
|
# action up bring the tunnel up if it is not already up
|
||||||
|
# down bring the tunnel down if it is up
|
||||||
|
# restart down (if needed), then up
|
||||||
|
# interface e.g. wgui0 — must match wgui[0-9]+ (project convention)
|
||||||
|
# config-path must be /var/lib/wgui/hosts/<interface>.conf
|
||||||
|
#
|
||||||
|
# Designed to run as root only (the web server runs as root).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ACTION="${1:-}"
|
||||||
|
INTERFACE="${2:-}"
|
||||||
|
CONFIG_PATH="${3:-}"
|
||||||
|
|
||||||
|
if [ -z "$ACTION" ] || [ -z "$INTERFACE" ] || [ -z "$CONFIG_PATH" ]; then
|
||||||
|
echo "usage: $0 <up|down|restart> <interface> <config-path>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$INTERFACE" =~ ^wgui[0-9]+$ ]]; then
|
||||||
|
echo "error: refusing to manage non-project interface '$INTERFACE' (expected wgui[0-9]+)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
up | down | restart) ;;
|
||||||
|
*)
|
||||||
|
echo "error: unknown action '$ACTION' (expected up, down, or restart)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
EXPECTED_CONFIG_PATH="/var/lib/wgui/hosts/${INTERFACE}.conf"
|
||||||
|
if [ "$CONFIG_PATH" != "$EXPECTED_CONFIG_PATH" ]; then
|
||||||
|
echo "error: config path must be $EXPECTED_CONFIG_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WG_QUICK_BIN="$(command -v wg-quick)"
|
||||||
|
if [ -z "$WG_QUICK_BIN" ]; then
|
||||||
|
echo "error: wg-quick not found — run setup-wireguard.sh first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
interface_is_up() {
|
||||||
|
ip link show dev "$INTERFACE" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
bring_up() {
|
||||||
|
if interface_is_up; then
|
||||||
|
echo "interface $INTERFACE is already up — nothing to do."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
"$WG_QUICK_BIN" up "$CONFIG_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
bring_down() {
|
||||||
|
if ! interface_is_up; then
|
||||||
|
echo "interface $INTERFACE is not up — nothing to do."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
"$WG_QUICK_BIN" down "$CONFIG_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
up)
|
||||||
|
bring_up
|
||||||
|
;;
|
||||||
|
down)
|
||||||
|
bring_down
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
bring_down
|
||||||
|
bring_up
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Manages the wg-quick systemd unit for a single wireguard-ui host tunnel.
|
|
||||||
#
|
|
||||||
# This project deliberately avoids /etc/wireguard entirely so it can never
|
|
||||||
# collide with an existing WireGuard setup. Host configs live under
|
|
||||||
# /var/lib/wgui/hosts and are wired into the stock wg-quick@ template unit
|
|
||||||
# via a per-instance drop-in that points ExecStart/ExecStop at the project
|
|
||||||
# config path. Interfaces follow the project convention wgui<host_id>
|
|
||||||
# (e.g. wgui0), never stock names like wg0.
|
|
||||||
#
|
|
||||||
# Usage: wg-quick-systemd.sh <interface> <config-path>
|
|
||||||
# interface e.g. wgui0 — must match wgui[0-9]* (project convention)
|
|
||||||
# config-path absolute path to the host config file
|
|
||||||
#
|
|
||||||
# Designed to run as root (directly or via a NOPASSWD sudoers entry).
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
INTERFACE="${1:-}"
|
|
||||||
CONFIG_PATH="${2:-}"
|
|
||||||
|
|
||||||
if [ -z "$INTERFACE" ] || [ -z "$CONFIG_PATH" ]; then
|
|
||||||
echo "usage: $0 <interface> <config-path>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$INTERFACE" in
|
|
||||||
wgui[0-9]*)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "error: refusing to manage non-project interface '$INTERFACE' (expected wgui[0-9]*)" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
WG_QUICK_BIN="$(command -v wg-quick)"
|
|
||||||
if [ -z "$WG_QUICK_BIN" ]; then
|
|
||||||
echo "error: wg-quick not found — run setup-wireguard.sh first" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DROP_IN_DIR="/etc/systemd/system/wg-quick@${INTERFACE}.service.d"
|
|
||||||
mkdir -p "$DROP_IN_DIR"
|
|
||||||
|
|
||||||
cat > "$DROP_IN_DIR/override.conf" <<EOF
|
|
||||||
[Service]
|
|
||||||
ExecStart=
|
|
||||||
ExecStart=$WG_QUICK_BIN up $CONFIG_PATH
|
|
||||||
ExecReload=
|
|
||||||
ExecReload=$WG_QUICK_BIN strip $CONFIG_PATH
|
|
||||||
ExecStop=
|
|
||||||
ExecStop=$WG_QUICK_BIN down $CONFIG_PATH
|
|
||||||
EOF
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable "wg-quick@${INTERFACE}.service"
|
|
||||||
systemctl restart "wg-quick@${INTERFACE}.service"
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import bunext from "@moduletrace/bunext";
|
import bunext from "@moduletrace/bunext";
|
||||||
|
import syncWireguardHosts from "./functions/backend/setup/sync-wireguard-hosts";
|
||||||
import handleMediaServer from "./functions/server/media/handle-media-server";
|
import handleMediaServer from "./functions/server/media/handle-media-server";
|
||||||
import { SiteData } from "./data/site-data";
|
import { SiteData } from "./data/site-data";
|
||||||
|
|
||||||
@@ -28,3 +29,5 @@ const server = Bun.serve({
|
|||||||
});
|
});
|
||||||
|
|
||||||
bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`);
|
bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`);
|
||||||
|
|
||||||
|
syncWireguardHosts();
|
||||||
|
|||||||
@@ -30,16 +30,18 @@ export default function grabDirNames(params?: Params) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const WGUI_LIB_DIR = `/var/lib/wgui`;
|
const WGUI_LIB_DIR = `/var/lib/wgui`;
|
||||||
const WGUI_LIB_IP_TABLES_DIR = path.join(WGUI_LIB_DIR, `iptables`);
|
|
||||||
const WGUI_LIB_KEYS_DIR = path.join(WGUI_LIB_DIR, `keys`);
|
const WGUI_LIB_KEYS_DIR = path.join(WGUI_LIB_DIR, `keys`);
|
||||||
const WGUI_LIB_CLIENTS_CONFIGS_DIR = path.join(WGUI_LIB_DIR, `clients`);
|
|
||||||
const WGUI_LIB_HOSTS_CONFIGS_DIR = path.join(WGUI_LIB_DIR, `hosts`);
|
const WGUI_LIB_HOSTS_CONFIGS_DIR = path.join(WGUI_LIB_DIR, `hosts`);
|
||||||
const WGUI_LIB_SCRIPTS_DIR = path.join(WGUI_LIB_DIR, `scripts`);
|
const WGUI_LIB_SCRIPTS_DIR = path.join(WGUI_LIB_DIR, `scripts`);
|
||||||
const WGUI_WG_QUICK_SYSTEMD_SCRIPT = path.join(
|
const WGUI_WG_QUICK_MANAGE_SCRIPT = path.join(
|
||||||
WGUI_LIB_SCRIPTS_DIR,
|
WGUI_LIB_SCRIPTS_DIR,
|
||||||
`wg-quick-systemd.sh`,
|
`wg-quick-manage.sh`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const WGUI_LIB_HOST_IPTABLES_DIR_NAME = `iptables`;
|
||||||
|
const WGUI_LIB_HOST_CLIENTS_DIR_NAME = `clients`;
|
||||||
|
|
||||||
const WIREGUARD_PRIVATE_KEY_FILE_NAME = `private.key`;
|
const WIREGUARD_PRIVATE_KEY_FILE_NAME = `private.key`;
|
||||||
const WIREGUARD_PUBLIC_KEY_FILE_NAME = `public.key`;
|
const WIREGUARD_PUBLIC_KEY_FILE_NAME = `public.key`;
|
||||||
const WIREGUARD_CLIENT_CONFIG_FILE_NAME = `wg.conf`;
|
const WIREGUARD_CLIENT_CONFIG_FILE_NAME = `wg.conf`;
|
||||||
@@ -54,15 +56,16 @@ export default function grabDirNames(params?: Params) {
|
|||||||
USER_MEDIA_PRIVATE_RELATIVE_DIR,
|
USER_MEDIA_PRIVATE_RELATIVE_DIR,
|
||||||
|
|
||||||
WGUI_LIB_DIR,
|
WGUI_LIB_DIR,
|
||||||
WGUI_LIB_IP_TABLES_DIR,
|
|
||||||
WGUI_LIB_KEYS_DIR,
|
WGUI_LIB_KEYS_DIR,
|
||||||
WGUI_LIB_CLIENTS_CONFIGS_DIR,
|
|
||||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
WGUI_LIB_SCRIPTS_DIR,
|
WGUI_LIB_SCRIPTS_DIR,
|
||||||
WGUI_WG_QUICK_SYSTEMD_SCRIPT,
|
WGUI_WG_QUICK_MANAGE_SCRIPT,
|
||||||
|
|
||||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||||
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
||||||
};
|
|
||||||
|
WGUI_LIB_HOST_IPTABLES_DIR_NAME,
|
||||||
|
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
|
||||||
|
} as const;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user