diff --git a/db/schema.ts b/db/schema.ts
index c5ba6a3..2be45c1 100644
--- a/db/schema.ts
+++ b/db/schema.ts
@@ -1,3 +1,4 @@
+import { AppData } from "@/src/data/app-data";
import {
ClientRuleProtocols,
ClientRuleTypes,
@@ -231,6 +232,11 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
fieldName: "public_ip_address",
dataType: "TEXT",
},
+ {
+ fieldName: "listen_port",
+ dataType: "INTEGER",
+ defaultValue: AppData["DefaultWGListenPort"],
+ },
{
fieldName: "interface",
dataType: "TEXT",
diff --git a/db/types/db.ts b/db/types/db.ts
index 77818a6..3e9433d 100644
--- a/db/types/db.ts
+++ b/db/types/db.ts
@@ -134,6 +134,7 @@ export type BUN_SQLITE_WGUI_HOSTS = {
name?: string;
short_description?: string;
public_ip_address?: string;
+ listen_port?: number | "";
interface?: string;
wg_ip_address?: string;
public_key?: string;
diff --git a/src/components/general/setup-main-host-button.tsx b/src/components/general/setup-main-host-button.tsx
index 700fe67..72f0349 100755
--- a/src/components/general/setup-main-host-button.tsx
+++ b/src/components/general/setup-main-host-button.tsx
@@ -5,17 +5,12 @@ import fetchApi from "../twui/utils/fetch/fetchApi";
import type { ApiReqParams } from "@/src/types";
import Row from "../twui/layout/Row";
import Stack from "../twui/layout/Stack";
-import {
- CircleCheck,
- TriangleAlert,
-} from "lucide-react";
+import { CircleCheck, TriangleAlert } from "lucide-react";
import Span from "../twui/layout/Span";
import Tag from "../twui/elements/Tag";
import { AppData } from "@/src/data/app-data";
import type { APIResponseObject } from "@moduletrace/bunext/types";
-import HostWgIpField, {
- type HostWgIpFieldStatus,
-} from "./host-wg-ip-field";
+import HostWgIpField, { type HostWgIpFieldStatus } from "./host-wg-ip-field";
import GrabNextSubnetButton from "./grab-next-subnet-button";
type Props = {
@@ -30,12 +25,12 @@ export default function SetupMainHostButton({ button_props }: Props) {
return (
-
+ /> */}
{status?.error && status.msg ? (
);
-}
\ No newline at end of file
+}
diff --git a/src/functions/backend/setup/create-wireguard-host.ts b/src/functions/backend/setup/create-wireguard-host.ts
index 9d8decc..fdb395d 100644
--- a/src/functions/backend/setup/create-wireguard-host.ts
+++ b/src/functions/backend/setup/create-wireguard-host.ts
@@ -1,4 +1,7 @@
-import type { BUN_SQLITE_WGUI_HOSTS, BUN_SQLITE_WGUI_VARIABLES } 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";
@@ -9,7 +12,11 @@ type Params = {
wg_ip_address?: string | null;
public_ip_address?: string | null;
interface?: string | null;
+ name?: string | null;
+ short_description?: string | null;
+ listen_port?: number | null;
user: User;
+ is_main_host?: boolean;
};
/**
@@ -24,7 +31,11 @@ export default async function createWireguardHost({
wg_ip_address,
public_ip_address,
interface: iface,
+ name,
+ short_description,
+ listen_port,
user,
+ is_main_host,
}: Params): Promise {
const wg_ip = (wg_ip_address || "").trim();
@@ -38,24 +49,34 @@ export default async function createWireguardHost({
const detected_public_ip = await grabHostPublicIPAddress();
const final_public_ip = public_ip_address?.trim() || detected_public_ip;
const final_interface = iface?.trim() || undefined;
+ const final_name = name?.trim() || undefined;
+ const final_short_description = short_description?.trim() || undefined;
try {
+ const insert_data: BUN_SQLITE_WGUI_HOSTS = {
+ user_id: user.id,
+ wg_ip_address: wg_ip,
+ public_ip_address: final_public_ip || undefined,
+ interface: final_interface,
+ name: final_name,
+ short_description: final_short_description,
+ listen_port: listen_port != null ? listen_port : undefined,
+ };
+
+ if (is_main_host) {
+ insert_data.id = 0;
+ }
+
const insert_host = await BunSQLite.insert<
BUN_SQLITE_WGUI_HOSTS,
TableType
>({
table: "hosts",
- data: [
- {
- user_id: user.id,
- wg_ip_address: wg_ip,
- public_ip_address: final_public_ip || undefined,
- interface: final_interface,
- },
- ],
+ data: [insert_data],
+ update_on_duplicate: true,
});
- if (!insert_host.success) {
+ if (!insert_host.success || !insert_host.postInsertReturn?.insertId) {
return {
success: false,
msg: `Could not create the host record: ${insert_host.msg}`,
@@ -70,13 +91,13 @@ export default async function createWireguardHost({
wg_ip_address: wg_ip,
public_ip_address: final_public_ip || undefined,
interface: final_interface,
+ name: final_name,
+ short_description: final_short_description,
+ listen_port: listen_port != null ? listen_port : undefined,
};
if (host_id === 0 && final_interface) {
- await BunSQLite.insert<
- BUN_SQLITE_WGUI_VARIABLES,
- TableType
- >({
+ await BunSQLite.insert({
table: "variables",
data: [
{
diff --git a/src/functions/backend/setup/get-next-available-listen-port.ts b/src/functions/backend/setup/get-next-available-listen-port.ts
new file mode 100644
index 0000000..e9f45f0
--- /dev/null
+++ b/src/functions/backend/setup/get-next-available-listen-port.ts
@@ -0,0 +1,40 @@
+import { exec } from "node:child_process";
+import { promisify } from "node:util";
+import { AppData } from "@/src/data/app-data";
+
+const execAsync = promisify(exec);
+
+/**
+ * Function to get the next available listen port on the system.
+ * Checks both UDP and TCP listening ports.
+ */
+export default async function getNextAvailableListenPort(): Promise {
+ const defaultPort = AppData["DefaultWGListenPort"] ?? 51820;
+
+ try {
+ const { stdout } = await execAsync("ss -tuln");
+ const usedPorts = new Set();
+
+ for (const line of stdout.split("\n")) {
+ const match = line.match(/[:](\d+)\s/);
+ if (match && match[1]) {
+ const port = Number.parseInt(match[1], 10);
+ if (!Number.isNaN(port)) {
+ usedPorts.add(port);
+ }
+ }
+ }
+
+ let port = defaultPort;
+ while (usedPorts.has(port)) {
+ port++;
+ if (port > 65535) {
+ return defaultPort;
+ }
+ }
+
+ return port;
+ } catch (error) {
+ return defaultPort;
+ }
+}
diff --git a/src/functions/backend/setup/setup-wireguard-client.ts b/src/functions/backend/setup/setup-wireguard-client.ts
index 019c509..e49130c 100644
--- a/src/functions/backend/setup/setup-wireguard-client.ts
+++ b/src/functions/backend/setup/setup-wireguard-client.ts
@@ -14,13 +14,8 @@ import BunSQLite from "@moduletrace/bun-sqlite";
import type { TableType, User } from "@/src/types";
import grabClientDirnames from "./grab-client-dir-names";
-const {
- WGUI_LIB_HOSTS_CONFIGS_DIR,
- WIREGUARD_PRIVATE_KEY_FILE_NAME,
- WIREGUARD_PUBLIC_KEY_FILE_NAME,
- WGUI_LIB_HOST_CLIENTS_DIR_NAME,
- WIREGUARD_CLIENT_CONFIG_FILE_NAME,
-} = grabDirNames();
+const { WIREGUARD_PUBLIC_KEY_FILE_NAME, WIREGUARD_CLIENT_CONFIG_FILE_NAME } =
+ grabDirNames();
type Params = {
client: BUN_SQLITE_WGUI_CLIENTS;
@@ -135,7 +130,7 @@ export default async function setupWireguardClient({
sh += `\n`;
sh += `[Peer]\n`;
sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`;
- sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:51820\n`;
+ sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:${host?.listen_port || AppData["DefaultWGListenPort"]}\n`;
if (client?.allow_all_ips == 1) {
sh += `AllowedIPs = 0.0.0.0/0, ::/0\n`;
diff --git a/src/functions/backend/setup/setup-wireguard-host.ts b/src/functions/backend/setup/setup-wireguard-host.ts
index 7cb9f2d..87b837c 100644
--- a/src/functions/backend/setup/setup-wireguard-host.ts
+++ b/src/functions/backend/setup/setup-wireguard-host.ts
@@ -15,6 +15,7 @@ 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();
@@ -100,7 +101,8 @@ export default async function setupWireguardHost({
rules_by_client_id.set(rule.client_id, existing_rules);
}
- const TARGET_INTERFACE = host?.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 ||
@@ -246,7 +248,7 @@ export default async function setupWireguardHost({
sh += `cat > ${INTERFACE_NAME}.conf << EOF\n`;
sh += `[Interface]\n`;
sh += `Address = ${HOST_WG_IP}/24\n`;
- sh += `ListenPort = 51820\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`;
@@ -284,6 +286,13 @@ export default async function setupWireguardHost({
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",
diff --git a/src/pages/admin/hosts/[host_id]/edit/(sections)/edit-host-form-section.tsx b/src/pages/admin/hosts/[host_id]/edit/(sections)/edit-host-form-section.tsx
index 1a13601..69ca94c 100644
--- a/src/pages/admin/hosts/[host_id]/edit/(sections)/edit-host-form-section.tsx
+++ b/src/pages/admin/hosts/[host_id]/edit/(sections)/edit-host-form-section.tsx
@@ -86,13 +86,13 @@ export default function EditHostFormSection() {
-
+ /> */}
{status?.error && status.msg ? (
>
);
-}
\ No newline at end of file
+}
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 cab4441..b88d514 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
@@ -24,6 +24,9 @@ export default async function submitAddHostForm({
wg_ip_address: form.wg_ip_address || "",
public_ip_address: form.public_ip_address || "",
interface: form.interface || "",
+ name: form.name || "",
+ short_description: form.short_description || "",
+ listen_port: form.listen_port != null ? Number(form.listen_port) : null,
},
},
);
diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-description.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-description.tsx
new file mode 100644
index 0000000..821469f
--- /dev/null
+++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-description.tsx
@@ -0,0 +1,26 @@
+import Stack from "@/src/components/twui/layout/Stack";
+import useFormInit from "@/src/hooks/use-form-init";
+import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
+import Textarea from "@/src/components/twui/form/Textarea";
+
+type Props = ReturnType>;
+
+export default function AddHostFormDescription({ form, setForm }: Props) {
+ return (
+
+
+ );
+}
diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-listen-port.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-listen-port.tsx
new file mode 100644
index 0000000..33bf3c7
--- /dev/null
+++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-listen-port.tsx
@@ -0,0 +1,70 @@
+import { useState } from "react";
+import Input from "@/src/components/twui/form/Input";
+import Button from "@/src/components/twui/layout/Button";
+import Stack from "@/src/components/twui/layout/Stack";
+import Row from "@/src/components/twui/layout/Row";
+import useFormInit from "@/src/hooks/use-form-init";
+import { AppData } from "@/src/data/app-data";
+import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
+import type { ApiReqParams } from "@/src/types";
+import type { APIResponseObject } from "@moduletrace/bunext/types";
+import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
+
+type Props = ReturnType>;
+
+export default function AddHostFormListenPort({ form, setForm }: Props) {
+ const [busy, setBusy] = useState(false);
+
+ async function handleGrabNext() {
+ setBusy(true);
+ try {
+ const res = await fetchApi(
+ "/api/admin/next-listen-port",
+ { method: "GET" },
+ );
+ if (res.success && res.numberRes) {
+ setForm((prev) => ({
+ ...prev,
+ listen_port: Number(res.numberRes),
+ }));
+ }
+ } catch {
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+
+ {
+ setForm((prev) => ({
+ ...prev,
+ listen_port: Number(v) || undefined,
+ }));
+ }}
+ />
+
+
+
+ );
+}
diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-name.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-name.tsx
new file mode 100644
index 0000000..541414a
--- /dev/null
+++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-name.tsx
@@ -0,0 +1,26 @@
+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 type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
+
+type Props = ReturnType>;
+
+export default function AddHostFormName({ form, setForm }: Props) {
+ return (
+
+ {
+ setForm((prev) => ({
+ ...prev,
+ name: 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 57a11a7..c7b7d96 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
@@ -12,6 +12,9 @@ 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 AddHostFormName from "./add-host-form-name";
+import AddHostFormDescription from "./add-host-form-description";
+import AddHostFormListenPort from "./add-host-form-listen-port";
import LoadingRectangleBlock from "@/src/components/twui/layout/LoadingRectangleBlock";
import type { HostWgIpFieldStatus } from "@/src/components/general/host-wg-ip-field";
@@ -19,17 +22,21 @@ export default function AddHostForm() {
const init = useFormInit({
default: {
wg_ip_address: AppData["DefaultPrivateIP"],
+ name: "Main",
},
- title: "Add Host",
+ title: "new_host",
async before_ready_function(params) {
- if (!params.form.interface) {
- params.setForm((prev) => ({
- ...prev,
- interface:
- params.app_context.pageProps?.network_interfaces?.[0] ||
- undefined,
- }));
- }
+ params.setForm((prev) => ({
+ ...prev,
+ interface:
+ params.form.interface ||
+ params.app_context.pageProps?.network_interfaces?.[0] ||
+ undefined,
+ listen_port:
+ params.form.listen_port ||
+ params.app_context.pageProps?.next_listen_port ||
+ undefined,
+ }));
},
});
@@ -49,7 +56,10 @@ export default function AddHostForm() {
{init.ready ? (
{/* */}
+
+
+
+ <>
-
+ >
);
-}
\ No newline at end of file
+}
diff --git a/src/pages/admin/hosts/add/index.server.ts b/src/pages/admin/hosts/add/index.server.ts
index 52bac35..a309e77 100644
--- a/src/pages/admin/hosts/add/index.server.ts
+++ b/src/pages/admin/hosts/add/index.server.ts
@@ -1,13 +1,16 @@
+import getNextAvailableListenPort from "@/src/functions/backend/setup/get-next-available-listen-port";
import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces";
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn = async ({ req }) => {
const network_interfaces = await grabHostNetworkInterfaces();
+ const next_listen_port = await getNextAvailableListenPort();
return {
props: {
network_interfaces,
+ next_listen_port,
},
};
};
diff --git a/src/pages/admin/hosts/index.tsx b/src/pages/admin/hosts/index.tsx
index 90b28b2..1cea724 100644
--- a/src/pages/admin/hosts/index.tsx
+++ b/src/pages/admin/hosts/index.tsx
@@ -51,6 +51,14 @@ export default function AdminHostPage() {
description="WireGuard server configuration and interface details"
buttons={
<>
+
) : (
diff --git a/src/pages/api/admin/add-host.ts b/src/pages/api/admin/add-host.ts
index 08c2b0f..476a7c4 100644
--- a/src/pages/api/admin/add-host.ts
+++ b/src/pages/api/admin/add-host.ts
@@ -43,6 +43,9 @@ export const handler: BunextAPIRouteHandler = async (
wg_ip_address: body?.wg_ip_address,
public_ip_address: body?.public_ip_address,
interface: body?.interface,
+ name: body?.name,
+ short_description: body?.short_description,
+ listen_port: body?.listen_port,
user,
});
} catch (error: any) {
diff --git a/src/pages/api/admin/next-listen-port.ts b/src/pages/api/admin/next-listen-port.ts
new file mode 100644
index 0000000..83c42e2
--- /dev/null
+++ b/src/pages/api/admin/next-listen-port.ts
@@ -0,0 +1,20 @@
+import getNextAvailableListenPort from "@/src/functions/backend/setup/get-next-available-listen-port";
+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 port = await getNextAvailableListenPort();
+ return { success: true, numberRes: port };
+ } catch (error: any) {
+ return { success: false, msg: error.message };
+ }
+};
diff --git a/src/scripts/wg-quick-manage.sh b/src/scripts/wg-quick-manage.sh
index cc43c97..c77ec06 100755
--- a/src/scripts/wg-quick-manage.sh
+++ b/src/scripts/wg-quick-manage.sh
@@ -66,11 +66,12 @@ bring_up() {
}
bring_down() {
- if ! interface_is_up; then
+ # If forced or interface exists, run wg-quick down to clean up routes/IPs safely
+ if interface_is_up || [ "${FORCE_DOWN:-0}" -eq 1 ]; then
+ "$WG_QUICK_BIN" down "$CONFIG_PATH" 2>/dev/null || true
+ else
echo "interface $INTERFACE is not up — nothing to do."
- return 0
fi
- "$WG_QUICK_BIN" down "$CONFIG_PATH"
}
case "$ACTION" in
@@ -81,7 +82,9 @@ case "$ACTION" in
bring_down
;;
restart)
- bring_down
+ # Force wg-quick down to clean up stuck IPs/routes even if link is missing
+ FORCE_DOWN=1 bring_down
bring_up
;;
esac
+
diff --git a/src/types/index.ts b/src/types/index.ts
index 7574a4c..40d1e52 100755
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -66,6 +66,7 @@ export type PagePropsType = {
url?: BunextPageModuleServerReturnURLObject;
main_host_dir_names?: ReturnType | null;
network_interfaces?: string[] | null;
+ next_listen_port?: number | null;
};
export type AppContextObject = {
@@ -224,6 +225,9 @@ export type ApiReqParams<
client_id?: string | number | null;
public_ip_address?: string | null;
interface?: string | null;
+ name?: string | null;
+ short_description?: string | null;
+ listen_port?: number | null;
// client_rules?: BUN_SQLITE_WGUI_CLIENT_RULES[];
media_base_64?: string;