From 8b40a876a120ebf27453fe2161746b9ece5d74e8 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sun, 20 Sep 2026 12:22:54 +0100 Subject: [PATCH] Add public IP field and Network interface to host form --- db/schema.ts | 4 + db/types/db.ts | 3 +- src/components/general/host-wg-ip-field.tsx | 2 + src/dict/variables-dict.ts | 5 ++ .../backend/setup/create-wireguard-host.ts | 34 ++++++-- .../setup/grab-host-network-interfaces.ts | 85 +++++++++++++++++++ .../backend/setup/setup-wireguard-host.ts | 30 ++++--- .../backend/setup/sync-wireguard-hosts.ts | 5 ++ src/hooks/use-form-init.ts | 34 +++++++- .../add/(functions)/submit-add-host-form.ts | 2 + .../add-host-form/add-host-form-interface.tsx | 49 +++++++++++ .../add-host-form/add-host-form-public-ip.tsx | 44 ++++++++++ .../add-host-form/add-host-form.tsx | 8 +- src/pages/api/admin/add-host.ts | 2 + src/pages/api/admin/network-interfaces.ts | 20 +++++ src/pages/api/admin/public-ip.ts | 20 +++++ src/types/index.ts | 1 + 17 files changed, 327 insertions(+), 21 deletions(-) create mode 100644 src/functions/backend/setup/grab-host-network-interfaces.ts create mode 100644 src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-interface.tsx create mode 100644 src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-public-ip.tsx create mode 100644 src/pages/api/admin/network-interfaces.ts create mode 100644 src/pages/api/admin/public-ip.ts diff --git a/db/schema.ts b/db/schema.ts index 8189f00..1b1f2fe 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -222,6 +222,10 @@ const schema: BUN_SQLITE_DatabaseSchemaType = { fieldName: "public_ip_address", dataType: "TEXT", }, + { + fieldName: "interface", + dataType: "TEXT", + }, { fieldName: "wg_ip_address", dataType: "TEXT", diff --git a/db/types/db.ts b/db/types/db.ts index 4fdeb1b..4ee51a0 100644 --- a/db/types/db.ts +++ b/db/types/db.ts @@ -132,6 +132,7 @@ export type BUN_SQLITE_WGUI_HOSTS = { updated_at?: number | ""; user_id?: number | ""; public_ip_address?: string; + interface?: string; wg_ip_address?: string; public_key?: string; } @@ -171,7 +172,7 @@ export type BUN_SQLITE_WGUI_VARIABLES = { * The time when the record was updated. (Unix Timestamp) */ updated_at?: number | ""; - key?: "main_host_wg_ip_address" | "main_host_public_ip_address" | "main_host_wg_public_key" | "main_host_wg_private_key" | ""; + key?: "main_host_wg_ip_address" | "main_host_public_ip_address" | "main_host_wg_public_key" | "main_host_wg_private_key" | "main_host_public_interface" | ""; value?: string; } diff --git a/src/components/general/host-wg-ip-field.tsx b/src/components/general/host-wg-ip-field.tsx index ef59542..ab34254 100644 --- a/src/components/general/host-wg-ip-field.tsx +++ b/src/components/general/host-wg-ip-field.tsx @@ -98,6 +98,8 @@ export default function HostWgIpField({ WireGuard IP Address } + title="Wireguard IP Address" + showLabel componentRef={inputRef} autoFocus={autoFocus} /> diff --git a/src/dict/variables-dict.ts b/src/dict/variables-dict.ts index de2cd5b..eeaa1d8 100644 --- a/src/dict/variables-dict.ts +++ b/src/dict/variables-dict.ts @@ -19,4 +19,9 @@ export const Variables = [ value: "main_host_wg_private_key", description: `Private Key for the main host`, }, + { + title: `Main Host Public Interface`, + value: "main_host_public_interface", + description: `Network interface for the main host (e.g. eth0, wlan0)`, + }, ] as const; diff --git a/src/functions/backend/setup/create-wireguard-host.ts b/src/functions/backend/setup/create-wireguard-host.ts index 805eb0e..9d8decc 100644 --- a/src/functions/backend/setup/create-wireguard-host.ts +++ b/src/functions/backend/setup/create-wireguard-host.ts @@ -1,4 +1,4 @@ -import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db"; +import type { BUN_SQLITE_WGUI_HOSTS, BUN_SQLITE_WGUI_VARIABLES } from "@/db/types/db"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import type { TableType, User } from "@/src/types"; @@ -7,6 +7,8 @@ import grabHostPublicIPAddress from "./grab-host-public-ip-address"; type Params = { wg_ip_address?: string | null; + public_ip_address?: string | null; + interface?: string | null; user: User; }; @@ -20,6 +22,8 @@ type Params = { */ export default async function createWireguardHost({ wg_ip_address, + public_ip_address, + interface: iface, user, }: Params): Promise { const wg_ip = (wg_ip_address || "").trim(); @@ -31,7 +35,9 @@ export default async function createWireguardHost({ }; } - const public_ip_address = await grabHostPublicIPAddress(); + const detected_public_ip = await grabHostPublicIPAddress(); + const final_public_ip = public_ip_address?.trim() || detected_public_ip; + const final_interface = iface?.trim() || undefined; try { const insert_host = await BunSQLite.insert< @@ -43,7 +49,8 @@ export default async function createWireguardHost({ { user_id: user.id, wg_ip_address: wg_ip, - public_ip_address: public_ip_address || undefined, + public_ip_address: final_public_ip || undefined, + interface: final_interface, }, ], }); @@ -61,9 +68,26 @@ export default async function createWireguardHost({ id: host_id, user_id: user.id, wg_ip_address: wg_ip, - public_ip_address: public_ip_address || undefined, + public_ip_address: final_public_ip || undefined, + interface: final_interface, }; + if (host_id === 0 && final_interface) { + await BunSQLite.insert< + BUN_SQLITE_WGUI_VARIABLES, + TableType + >({ + table: "variables", + data: [ + { + key: "main_host_public_interface", + value: final_interface, + }, + ], + update_on_duplicate: true, + }); + } + const setup_res = await setupWireguardHost({ host, wg_subnet_ip: wg_ip, @@ -93,4 +117,4 @@ export default async function createWireguardHost({ msg: error.message, }; } -} \ No newline at end of file +} diff --git a/src/functions/backend/setup/grab-host-network-interfaces.ts b/src/functions/backend/setup/grab-host-network-interfaces.ts new file mode 100644 index 0000000..7c02161 --- /dev/null +++ b/src/functions/backend/setup/grab-host-network-interfaces.ts @@ -0,0 +1,85 @@ +import { execSync } from "node:child_process"; +import os from "node:os"; + +const VIRTUAL_INTERFACE_PATTERNS = [ + /^wg\d+$/, + /^docker\d*$/, + /^br\d*$/, + /^veth.*/, + /^tun.*/, + /^tap.*/, + /^vlan.*/, + /^bond\d*$/, + /^macvlan.*/, + /^ipvlan.*/, + /^wgui\d+$/, + /^virbr\d*$/, + /^vboxnet\d*$/, + /^vmnet\d*$/, +]; + +function isVirtualInterface(name: string): boolean { + return VIRTUAL_INTERFACE_PATTERNS.some((pattern) => + pattern.test(name), + ); +} + +function isPhysicalInterface(name: string): boolean { + try { + const devicePath = `/sys/class/net/${name}/device`; + const result = execSync(`test -e ${devicePath} && echo yes || echo no`, { + encoding: "utf-8", + }).trim(); + return result === "yes"; + } catch { + return true; + } +} + +/** + * Function to grab all available physical network interfaces on the machine + * @returns Array of interface names (e.g. ["enp2s0", "wlan0"]) + */ +export default async function grabHostNetworkInterfaces(): Promise { + try { + const interfaces = os.networkInterfaces(); + const names: string[] = []; + + for (const [name, addrs] of Object.entries(interfaces)) { + if (!addrs) continue; + if (name === "lo") continue; + if (isVirtualInterface(name)) continue; + for (const addr of addrs) { + if (addr.family === "IPv4" && !addr.internal) { + if (isPhysicalInterface(name)) { + names.push(name); + } + break; + } + } + } + + if (names.length > 0) { + return names.sort(); + } + } catch (error) {} + + try { + const route = execSync(`ip -4 addr show`, { encoding: "utf-8" }) + .trim() + .split(/\n/); + const names: string[] = []; + for (const line of route) { + const match = line.match(/^\d+:\s+(\S+):/); + if (match && match[1] && match[1] !== "lo") { + const iface = match[1]; + if (!isVirtualInterface(iface) && isPhysicalInterface(iface)) { + names.push(iface); + } + } + } + if (names.length > 0) return names.sort(); + } catch (error) {} + + return [`eth0`]; +} diff --git a/src/functions/backend/setup/setup-wireguard-host.ts b/src/functions/backend/setup/setup-wireguard-host.ts index b76de6b..7cb9f2d 100644 --- a/src/functions/backend/setup/setup-wireguard-host.ts +++ b/src/functions/backend/setup/setup-wireguard-host.ts @@ -100,7 +100,7 @@ export default async function setupWireguardHost({ rules_by_client_id.set(rule.client_id, existing_rules); } - const TARGET_INTERFACE = await grabHostNetworkInterface(); + const TARGET_INTERFACE = host?.interface || await grabHostNetworkInterface(); const HOST_WG_IP = host?.wg_ip_address || variables?.find((v) => v.key == "main_host_wg_ip_address")?.value || @@ -158,21 +158,29 @@ export default async function setupWireguardHost({ }).trim(); if (HOST_ID == 0 && !is_update_after_client_setup) { + const variables_data: Array<{ key: string; value: string }> = [ + { + key: "main_host_wg_ip_address", + value: HOST_WG_IP, + }, + { + key: "main_host_wg_public_key", + value: HOST_PUBLIC_KEY, + }, + ]; + if (host?.interface) { + variables_data.push({ + key: "main_host_public_interface", + value: host.interface, + }); + } + 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, - }, - ], + data: variables_data as BUN_SQLITE_WGUI_VARIABLES[], update_on_duplicate: true, }); diff --git a/src/functions/backend/setup/sync-wireguard-hosts.ts b/src/functions/backend/setup/sync-wireguard-hosts.ts index 27303a4..f966032 100644 --- a/src/functions/backend/setup/sync-wireguard-hosts.ts +++ b/src/functions/backend/setup/sync-wireguard-hosts.ts @@ -43,6 +43,10 @@ export default async function syncWireguardHosts() { (v) => v.key == "main_host_public_ip_address", )?.value; + const main_host_interface = variables.payload?.find( + (v) => v.key == "main_host_public_interface", + )?.value; + if (!main_host_ip) { throw new Error(`Main Host not set yet`); } @@ -52,6 +56,7 @@ export default async function syncWireguardHosts() { id: 0, public_ip_address: main_host_public_ip, wg_ip_address: main_host_ip, + interface: main_host_interface, }, ...(hosts.payload || []), ]; diff --git a/src/hooks/use-form-init.ts b/src/hooks/use-form-init.ts index ac9ef32..3facf9f 100644 --- a/src/hooks/use-form-init.ts +++ b/src/hooks/use-form-init.ts @@ -1,6 +1,13 @@ import type { BUN_SQLITE_WGUI_ALL_TYPEDEFS } from "@/db/types/db"; import useStatus from "@/src/components/twui/hooks/useStatus"; -import { useCallback, useContext, useEffect, useState } from "react"; +import { + useCallback, + useContext, + useEffect, + useState, + type Dispatch, + type SetStateAction, +} from "react"; import { AppContext } from "../pages/__root"; import type { ImageInputToBase64FunctionReturn } from "../components/twui/utils/form/imageInputToBase64"; import EJSON from "../utils/ejson"; @@ -32,6 +39,14 @@ type Params = { * Is this the first user? */ is_first_user?: boolean; + /** + * Function to run before the `setReady` + * dispatch is fired + */ + before_ready_function?: (params: { + form: T; + setForm: Dispatch>; + }) => Promise; }; export default function useFormInit< @@ -94,7 +109,22 @@ export default function useFormInit< } } catch (error) { } finally { - setReady(true); + if (params?.before_ready_function) { + params + .before_ready_function({ form, setForm }) + .then(() => {}) + .catch((e2) => { + console.log( + `Before ready function error:`, + e2.message, + ); + }) + .finally(() => { + setReady(true); + }); + } else { + setReady(true); + } } } }, []); diff --git a/src/pages/admin/hosts/add/(functions)/submit-add-host-form.ts b/src/pages/admin/hosts/add/(functions)/submit-add-host-form.ts index 7d50b9b..cab4441 100644 --- a/src/pages/admin/hosts/add/(functions)/submit-add-host-form.ts +++ b/src/pages/admin/hosts/add/(functions)/submit-add-host-form.ts @@ -22,6 +22,8 @@ export default async function submitAddHostForm({ method: "POST", body: { wg_ip_address: form.wg_ip_address || "", + public_ip_address: form.public_ip_address || "", + interface: form.interface || "", }, }, ); diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-interface.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-interface.tsx new file mode 100644 index 0000000..2e31136 --- /dev/null +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-interface.tsx @@ -0,0 +1,49 @@ +import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db"; +import Select from "@/src/components/twui/form/Select"; +import Stack from "@/src/components/twui/layout/Stack"; +import useFormInit from "@/src/hooks/use-form-init"; +import { useEffect, useState } from "react"; +import fetchApi from "@/src/components/twui/utils/fetch/fetchApi"; +import type { ApiReqParams } from "@/src/types"; +import type { APIResponseObject } from "@moduletrace/bunext/types"; + +type Props = ReturnType>; + +export default function AddHostFormInterface({ form, setForm }: Props) { + const [interfaces, setInterfaces] = useState([]); + + useEffect(() => { + fetchApi( + "/api/admin/network-interfaces", + { method: "GET" }, + ).then((res) => { + if (res.success && res.stringRes) { + try { + const parsed = JSON.parse(res.stringRes) as string[]; + setInterfaces(parsed); + } catch {} + } + }); + }, []); + + return ( + + { + setForm((prev) => ({ + ...prev, + public_ip_address: v, + })); + }} + /> + + ); +} diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx index ecf39fe..d1da526 100644 --- a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx @@ -10,6 +10,8 @@ import submitAddHostForm from "../../(functions)/submit-add-host-form"; import AddHostFormAction from "./add-host-form-action"; import AddHostFormSubnet from "./add-host-form-subnet"; import AddHostFormWgIpAddress from "./add-host-form-wg-ip-address"; +import AddHostFormPublicIP from "./add-host-form-public-ip"; +import AddHostFormInterface from "./add-host-form-interface"; import LoadingRectangleBlock from "@/src/components/twui/layout/LoadingRectangleBlock"; import type { HostWgIpFieldStatus } from "@/src/components/general/host-wg-ip-field"; @@ -35,8 +37,10 @@ export default function AddHostForm() { {status?.error && {status.msg}} {loading && } {init.ready ? ( - - + + {/* */} + + = async ( return await createWireguardHost({ wg_ip_address: body?.wg_ip_address, + public_ip_address: body?.public_ip_address, + interface: body?.interface, user, }); } catch (error: any) { diff --git a/src/pages/api/admin/network-interfaces.ts b/src/pages/api/admin/network-interfaces.ts new file mode 100644 index 0000000..4495a26 --- /dev/null +++ b/src/pages/api/admin/network-interfaces.ts @@ -0,0 +1,20 @@ +import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces"; +import type { ApiReqParams } from "@/src/types"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; + +export const handler: BunextAPIRouteHandler = async ( + params, +) => { + if (params.req.method !== "GET") { + return { success: false }; + } + try { + const interfaces = await grabHostNetworkInterfaces(); + return { success: true, stringRes: JSON.stringify(interfaces) }; + } catch (error: any) { + return { success: false, msg: error.message }; + } +}; diff --git a/src/pages/api/admin/public-ip.ts b/src/pages/api/admin/public-ip.ts new file mode 100644 index 0000000..837b448 --- /dev/null +++ b/src/pages/api/admin/public-ip.ts @@ -0,0 +1,20 @@ +import grabHostPublicIPAddress from "@/src/functions/backend/setup/grab-host-public-ip-address"; +import type { ApiReqParams } from "@/src/types"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; + +export const handler: BunextAPIRouteHandler = async ( + params, +) => { + if (params.req.method !== "GET") { + return { success: false }; + } + try { + const public_ip = await grabHostPublicIPAddress(); + return { success: true, stringRes: public_ip }; + } catch (error: any) { + return { success: false, msg: error.message }; + } +}; diff --git a/src/types/index.ts b/src/types/index.ts index 7e4b4cf..5d63f2b 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -222,6 +222,7 @@ export type ApiReqParams< host_id?: string | number | null; client_id?: string | number | null; public_ip_address?: string | null; + interface?: string | null; // client_rules?: BUN_SQLITE_WGUI_CLIENT_RULES[]; media_base_64?: string;