Add public IP field and Network interface to host form

This commit is contained in:
2026-09-20 12:22:54 +01:00
parent 957f2de13c
commit 8b40a876a1
17 changed files with 327 additions and 21 deletions
+4
View File
@@ -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",
+2 -1
View File
@@ -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;
}
@@ -98,6 +98,8 @@ export default function HostWgIpField({
WireGuard IP Address
</Span>
}
title="Wireguard IP Address"
showLabel
componentRef={inputRef}
autoFocus={autoFocus}
/>
+5
View File
@@ -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;
@@ -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<APIResponseObject> {
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,
};
}
}
}
@@ -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<string[]> {
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`];
}
@@ -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,
});
@@ -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 || []),
];
+32 -2
View File
@@ -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<T extends {} = BUN_SQLITE_WGUI_ALL_TYPEDEFS> = {
* 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<SetStateAction<T>>;
}) => Promise<void>;
};
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);
}
}
}
}, []);
@@ -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 || "",
},
},
);
@@ -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<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
export default function AddHostFormInterface({ form, setForm }: Props) {
const [interfaces, setInterfaces] = useState<string[]>([]);
useEffect(() => {
fetchApi<ApiReqParams, APIResponseObject>(
"/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 (
<Stack className="w-full gap-2 items-stretch">
<Select
name="interface"
label="Network Interface"
showLabel
options={interfaces.map((iface) => ({
value: iface,
title: iface,
default: iface === form.interface,
}))}
changeHandler={(v) => {
setForm((prev) => ({
...prev,
interface: v,
}));
}}
/>
</Stack>
);
}
@@ -0,0 +1,44 @@
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import Input from "@/src/components/twui/form/Input";
import Stack from "@/src/components/twui/layout/Stack";
import useFormInit from "@/src/hooks/use-form-init";
import { useEffect } 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<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
export default function AddHostFormPublicIP({ form, setForm }: Props) {
useEffect(() => {
fetchApi<ApiReqParams, APIResponseObject>(
"/api/admin/public-ip",
{ method: "GET" },
).then((res) => {
if (res.success && res.stringRes) {
setForm((prev) => ({
...prev,
public_ip_address: res.stringRes!,
}));
}
});
}, []);
return (
<Stack className="w-full gap-2">
<Input
name="public_ip_address"
label="Public IP Address"
showLabel
placeholder="e.g. 203.0.113.10"
value={form.public_ip_address || ""}
changeHandler={(v) => {
setForm((prev) => ({
...prev,
public_ip_address: v,
}));
}}
/>
</Stack>
);
}
@@ -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 && <Tag color="error">{status.msg}</Tag>}
{loading && <LoadingOverlay />}
{init.ready ? (
<Stack className="w-full gap-2">
<AddHostFormSubnet />
<Stack className="w-full gap-6 items-stretch">
{/* <AddHostFormSubnet /> */}
<AddHostFormPublicIP {...init} />
<AddHostFormInterface {...init} />
<AddHostFormWgIpAddress
{...init}
setIpStatus={setIpStatus}
+2
View File
@@ -41,6 +41,8 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = 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) {
+20
View File
@@ -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<APIResponseObject> = 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 };
}
};
+20
View File
@@ -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<APIResponseObject> = 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 };
}
};
+1
View File
@@ -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;