Updates
This commit is contained in:
@@ -111,6 +111,8 @@ export default async function loginUser({
|
||||
},
|
||||
]);
|
||||
|
||||
console.log("new_logged_in_user", new_logged_in_user);
|
||||
|
||||
console.log(
|
||||
`User #${new_logged_in_user.id} [${new_logged_in_user.first_name} ${new_logged_in_user.last_name}] logged in successfully.`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db";
|
||||
import type { TableType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
client_id?: string | number;
|
||||
client_ids?: (string | number)[];
|
||||
};
|
||||
|
||||
export default async function grabClientRules({
|
||||
client_id,
|
||||
client_ids,
|
||||
}: Params) {
|
||||
const ids = client_id
|
||||
? [client_id]
|
||||
: (client_ids || []).filter((id) => id !== undefined && id !== "");
|
||||
|
||||
if (!ids[0]) {
|
||||
return [] as BUN_SQLITE_WGUI_CLIENT_RULES[];
|
||||
}
|
||||
|
||||
const res = await BunSQLite.select<BUN_SQLITE_WGUI_CLIENT_RULES, TableType>({
|
||||
table: "client_rules",
|
||||
});
|
||||
|
||||
const id_set = new Set(ids.map((id) => Number(id)));
|
||||
|
||||
return (res.payload || []).filter(
|
||||
(rule) => rule.client_id && id_set.has(Number(rule.client_id)),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import buildHostIptablesScripts from "./build-host-iptables-scripts";
|
||||
|
||||
const base = {
|
||||
host_id: 0,
|
||||
interface_name: "wgui0",
|
||||
target_interface: "eth0",
|
||||
};
|
||||
|
||||
describe("buildHostIptablesScripts", () => {
|
||||
test("deny by default with no clients", () => {
|
||||
const res = buildHostIptablesScripts({
|
||||
...base,
|
||||
clients: [],
|
||||
});
|
||||
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.post_up).toContain("iptables -N WGUI0FWD");
|
||||
expect(res.post_up).toContain("iptables -A WGUI0FWD -j DROP");
|
||||
expect(res.post_up).toContain("iptables -A WGUI0IN -j DROP");
|
||||
expect(res.post_up).not.toContain("WGUI0FWD -s");
|
||||
expect(res.post_down).toContain("iptables -X WGUI0FWD");
|
||||
expect(res.post_down).toContain("iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE");
|
||||
});
|
||||
|
||||
test("all access accepts the client on forward and input", () => {
|
||||
const res = buildHostIptablesScripts({
|
||||
...base,
|
||||
clients: [
|
||||
{
|
||||
id: 1,
|
||||
wg_ip_address: "10.0.0.2",
|
||||
rules: [{ rule_type: "all" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.post_up).toContain(
|
||||
"iptables -A WGUI0FWD -s 10.0.0.2/32 -j ACCEPT",
|
||||
);
|
||||
expect(res.post_up).toContain(
|
||||
"iptables -A WGUI0IN -s 10.0.0.2/32 -j ACCEPT",
|
||||
);
|
||||
});
|
||||
|
||||
test("destination IP and ports", () => {
|
||||
const res = buildHostIptablesScripts({
|
||||
...base,
|
||||
clients: [
|
||||
{
|
||||
id: 2,
|
||||
wg_ip_address: "10.0.0.3",
|
||||
rules: [
|
||||
{
|
||||
rule_type: "destination",
|
||||
destination: "192.168.1.10",
|
||||
ports: "80,443",
|
||||
protocol: "tcp",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.post_up).toContain(
|
||||
"iptables -A WGUI0FWD -s 10.0.0.3/32 -d 192.168.1.10/32 -p tcp -m multiport --dports 80,443 -j ACCEPT",
|
||||
);
|
||||
expect(res.post_up).toContain(
|
||||
"iptables -A WGUI0IN -s 10.0.0.3/32 -d 192.168.1.10/32 -p tcp -m multiport --dports 80,443 -j ACCEPT",
|
||||
);
|
||||
});
|
||||
|
||||
test("protocol any with ports emits tcp and udp", () => {
|
||||
const res = buildHostIptablesScripts({
|
||||
...base,
|
||||
clients: [
|
||||
{
|
||||
id: 3,
|
||||
wg_ip_address: "10.0.0.4",
|
||||
rules: [
|
||||
{
|
||||
rule_type: "destination",
|
||||
ports: "53",
|
||||
protocol: "any",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.post_up).toContain(
|
||||
"iptables -A WGUI0FWD -s 10.0.0.4/32 -p tcp --dport 53 -j ACCEPT",
|
||||
);
|
||||
expect(res.post_up).toContain(
|
||||
"iptables -A WGUI0FWD -s 10.0.0.4/32 -p udp --dport 53 -j ACCEPT",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid destination", () => {
|
||||
const res = buildHostIptablesScripts({
|
||||
...base,
|
||||
clients: [
|
||||
{
|
||||
id: 4,
|
||||
wg_ip_address: "10.0.0.5",
|
||||
rules: [
|
||||
{
|
||||
rule_type: "destination",
|
||||
destination: "not-an-ip",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.success).toBe(false);
|
||||
});
|
||||
|
||||
test("cleans up legacy allow-all rules", () => {
|
||||
const res = buildHostIptablesScripts({
|
||||
...base,
|
||||
});
|
||||
|
||||
expect(res.post_up).toContain(
|
||||
"iptables -D FORWARD -i wgui0 -j ACCEPT 2>/dev/null || true",
|
||||
);
|
||||
expect(res.post_down).toContain(
|
||||
"iptables -D FORWARD -i wgui0 -j ACCEPT 2>/dev/null || true",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db";
|
||||
import parsePortList from "@/src/utils/parse-port-list";
|
||||
import validateIpv4 from "@/src/utils/validate-ipv4";
|
||||
import deriveIptablesChainNames from "./derive-iptables-chain-names";
|
||||
import normalizeIptablesDestination from "./normalize-iptables-destination";
|
||||
import validateClientRule from "./validate-client-rule";
|
||||
|
||||
type HostIptablesClient = {
|
||||
id?: number | "";
|
||||
wg_ip_address?: string;
|
||||
rules?: BUN_SQLITE_WGUI_CLIENT_RULES[];
|
||||
};
|
||||
|
||||
type Params = {
|
||||
host_id: number;
|
||||
interface_name: string;
|
||||
target_interface: string;
|
||||
clients?: HostIptablesClient[];
|
||||
};
|
||||
|
||||
const SAFE_IFACE = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
||||
const MULTIPORT_LIMIT = 15;
|
||||
|
||||
function joinArgs(parts: (string | undefined)[]) {
|
||||
return parts.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
function protocolsForRule({
|
||||
protocol,
|
||||
has_ports,
|
||||
}: {
|
||||
protocol?: string;
|
||||
has_ports: boolean;
|
||||
}) {
|
||||
if (has_ports) {
|
||||
if (protocol == "udp") {
|
||||
return ["udp"];
|
||||
}
|
||||
|
||||
if (protocol == "tcp") {
|
||||
return ["tcp"];
|
||||
}
|
||||
|
||||
return ["tcp", "udp"];
|
||||
}
|
||||
|
||||
if (protocol == "tcp" || protocol == "udp") {
|
||||
return [protocol];
|
||||
}
|
||||
|
||||
return [undefined];
|
||||
}
|
||||
|
||||
function portMatch({ ports }: { ports: string[] }) {
|
||||
if (!ports[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ports.length == 1) {
|
||||
return `--dport ${ports[0]}`;
|
||||
}
|
||||
|
||||
return `-m multiport --dports ${ports.join(",")}`;
|
||||
}
|
||||
|
||||
function chunkPorts({ ports }: { ports: string[] }) {
|
||||
if (ports.length <= MULTIPORT_LIMIT) {
|
||||
return [ports];
|
||||
}
|
||||
|
||||
const chunks: string[][] = [];
|
||||
|
||||
for (let i = 0; i < ports.length; i += MULTIPORT_LIMIT) {
|
||||
chunks.push(ports.slice(i, i + MULTIPORT_LIMIT));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function acceptLines({
|
||||
chain,
|
||||
source,
|
||||
destination,
|
||||
protocol,
|
||||
ports,
|
||||
}: {
|
||||
chain: string;
|
||||
source: string;
|
||||
destination?: string;
|
||||
protocol?: string;
|
||||
ports: string[];
|
||||
}) {
|
||||
const lines: string[] = [];
|
||||
const protocols = protocolsForRule({
|
||||
protocol,
|
||||
has_ports: Boolean(ports[0]),
|
||||
});
|
||||
const port_chunks = ports[0] ? chunkPorts({ ports }) : [[]];
|
||||
|
||||
for (let p = 0; p < protocols.length; p++) {
|
||||
const proto = protocols[p];
|
||||
|
||||
for (let c = 0; c < port_chunks.length; c++) {
|
||||
const chunk = port_chunks[c] || [];
|
||||
|
||||
lines.push(
|
||||
joinArgs([
|
||||
`iptables -A ${chain}`,
|
||||
`-s ${source}`,
|
||||
destination ? `-d ${destination}` : undefined,
|
||||
proto ? `-p ${proto}` : undefined,
|
||||
proto ? portMatch({ ports: chunk }) : undefined,
|
||||
`-j ACCEPT`,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
export default function buildHostIptablesScripts({
|
||||
host_id,
|
||||
interface_name,
|
||||
target_interface,
|
||||
clients,
|
||||
}: Params) {
|
||||
if (!Number.isInteger(host_id) || host_id < 0) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid host id`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!SAFE_IFACE.test(interface_name)) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid interface name`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!SAFE_IFACE.test(target_interface)) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid target interface`,
|
||||
};
|
||||
}
|
||||
|
||||
const { forward, input } = deriveIptablesChainNames({ host_id });
|
||||
const accept_lines: string[] = [];
|
||||
const host_clients = clients || [];
|
||||
|
||||
for (let i = 0; i < host_clients.length; i++) {
|
||||
const client = host_clients[i];
|
||||
|
||||
if (!client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const source_ip = client.wg_ip_address?.trim();
|
||||
|
||||
if (!source_ip) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Client ${client.id || i} is missing a WireGuard IP`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!validateIpv4({ ip: source_ip })) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Client ${client.id || i} has an invalid WireGuard IP`,
|
||||
};
|
||||
}
|
||||
|
||||
const source = `${source_ip}/32`;
|
||||
const rules = client.rules || [];
|
||||
|
||||
for (let r = 0; r < rules.length; r++) {
|
||||
const rule = rules[r];
|
||||
|
||||
if (!rule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const valid = validateClientRule({ rule });
|
||||
|
||||
if (!valid.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Client ${client.id || i}: ${valid.msg}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (rule.rule_type == "all") {
|
||||
accept_lines.push(
|
||||
`iptables -A ${forward} -s ${source} -j ACCEPT`,
|
||||
);
|
||||
accept_lines.push(
|
||||
`iptables -A ${input} -s ${source} -j ACCEPT`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const destination = normalizeIptablesDestination({
|
||||
destination: rule.destination,
|
||||
});
|
||||
const parsed_ports = parsePortList({ ports: rule.ports });
|
||||
|
||||
if (!parsed_ports.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Client ${client.id || i}: ${parsed_ports.msg}`,
|
||||
};
|
||||
}
|
||||
|
||||
accept_lines.push(
|
||||
...acceptLines({
|
||||
chain: forward,
|
||||
source,
|
||||
destination,
|
||||
protocol: rule.protocol || "any",
|
||||
ports: parsed_ports.ports,
|
||||
}),
|
||||
...acceptLines({
|
||||
chain: input,
|
||||
source,
|
||||
destination,
|
||||
protocol: rule.protocol || "any",
|
||||
ports: parsed_ports.ports,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const post_up = [
|
||||
`#!/bin/bash`,
|
||||
``,
|
||||
`iptables -D INPUT -i ${interface_name} -j ACCEPT 2>/dev/null || true`,
|
||||
`iptables -D OUTPUT -o ${interface_name} -j ACCEPT 2>/dev/null || true`,
|
||||
`iptables -D FORWARD -i ${interface_name} -j ACCEPT 2>/dev/null || true`,
|
||||
`iptables -D FORWARD -o ${interface_name} -j ACCEPT 2>/dev/null || true`,
|
||||
``,
|
||||
`iptables -N ${forward} 2>/dev/null || true`,
|
||||
`iptables -F ${forward}`,
|
||||
`iptables -N ${input} 2>/dev/null || true`,
|
||||
`iptables -F ${input}`,
|
||||
``,
|
||||
`iptables -A ${forward} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT`,
|
||||
`iptables -A ${input} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT`,
|
||||
``,
|
||||
...accept_lines,
|
||||
``,
|
||||
`iptables -A ${forward} -j DROP`,
|
||||
`iptables -A ${input} -j DROP`,
|
||||
``,
|
||||
`iptables -C FORWARD -i ${interface_name} -j ${forward} 2>/dev/null || iptables -I FORWARD 1 -i ${interface_name} -j ${forward}`,
|
||||
`iptables -C FORWARD -o ${interface_name} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -o ${interface_name} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT`,
|
||||
`iptables -C INPUT -i ${interface_name} -j ${input} 2>/dev/null || iptables -I INPUT 1 -i ${interface_name} -j ${input}`,
|
||||
``,
|
||||
`iptables -t nat -C POSTROUTING -o ${target_interface} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -o ${target_interface} -j MASQUERADE`,
|
||||
``,
|
||||
].join("\n");
|
||||
|
||||
const post_down = [
|
||||
`#!/bin/bash`,
|
||||
``,
|
||||
`iptables -D FORWARD -i ${interface_name} -j ${forward} 2>/dev/null || true`,
|
||||
`iptables -D FORWARD -o ${interface_name} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true`,
|
||||
`iptables -D INPUT -i ${interface_name} -j ${input} 2>/dev/null || true`,
|
||||
`iptables -D INPUT -i ${interface_name} -j ACCEPT 2>/dev/null || true`,
|
||||
`iptables -D OUTPUT -o ${interface_name} -j ACCEPT 2>/dev/null || true`,
|
||||
`iptables -D FORWARD -i ${interface_name} -j ACCEPT 2>/dev/null || true`,
|
||||
`iptables -D FORWARD -o ${interface_name} -j ACCEPT 2>/dev/null || true`,
|
||||
`iptables -F ${forward} 2>/dev/null || true`,
|
||||
`iptables -X ${forward} 2>/dev/null || true`,
|
||||
`iptables -F ${input} 2>/dev/null || true`,
|
||||
`iptables -X ${input} 2>/dev/null || true`,
|
||||
`iptables -t nat -D POSTROUTING -o ${target_interface} -j MASQUERADE 2>/dev/null || true`,
|
||||
``,
|
||||
].join("\n");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
post_up,
|
||||
post_down,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
type Params = {
|
||||
host_id: number;
|
||||
};
|
||||
|
||||
export default function deriveIptablesChainNames({ host_id }: Params) {
|
||||
return {
|
||||
forward: `WGUI${host_id}FWD`,
|
||||
input: `WGUI${host_id}IN`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db";
|
||||
|
||||
type Params = {
|
||||
rule: BUN_SQLITE_WGUI_CLIENT_RULES;
|
||||
};
|
||||
|
||||
export default function expandClientRuleDestinations({ rule }: Params) {
|
||||
if (rule.rule_type == "all") {
|
||||
return [rule];
|
||||
}
|
||||
|
||||
const destinations = (rule.destination || "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (destinations.length <= 1) {
|
||||
return [
|
||||
{
|
||||
...rule,
|
||||
destination: destinations[0] || "",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return destinations.map((destination) => ({
|
||||
...rule,
|
||||
id: undefined,
|
||||
destination,
|
||||
}));
|
||||
}
|
||||
@@ -49,19 +49,6 @@ export default function grabClientDirnames({
|
||||
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
||||
);
|
||||
|
||||
const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const HOST_PUBLIC_KEY = execSync(
|
||||
`cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
||||
{ encoding: "utf-8" },
|
||||
).trim();
|
||||
|
||||
return {
|
||||
CLIENT_WG_IP,
|
||||
HOST_CLIENTS_DIR,
|
||||
@@ -70,8 +57,5 @@ export default function grabClientDirnames({
|
||||
CLIENT_PRIVATE_KEY_FILE,
|
||||
CLIENT_PUBLIC_KEY_FILE,
|
||||
CLIENT_CONFIG_FILE,
|
||||
CLIENT_PRIVATE_KEY,
|
||||
CLIENT_PUBLIC_KEY,
|
||||
HOST_PUBLIC_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ export default function manageWireguardHost({
|
||||
const MANAGE_COMMAND = `${WGUI_WG_QUICK_MANAGE_SCRIPT} ${action} ${INTERFACE_NAME} ${HOST_CONFIG_FILE}`;
|
||||
|
||||
try {
|
||||
const output = execSync(MANAGE_COMMAND, { encoding: "utf-8" }).trim();
|
||||
const output = execSync(MANAGE_COMMAND, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import validateIpv4 from "@/src/utils/validate-ipv4";
|
||||
import validateIpv4Cidr from "@/src/utils/validate-ipv4-cidr";
|
||||
|
||||
type Params = {
|
||||
destination?: string;
|
||||
};
|
||||
|
||||
export default function normalizeIptablesDestination({ destination }: Params) {
|
||||
const value = destination?.trim();
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateIpv4Cidr({ value })) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validateIpv4({ ip: value })) {
|
||||
return `${value}/32`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db";
|
||||
|
||||
type Params = {
|
||||
rule?: BUN_SQLITE_WGUI_CLIENT_RULES | null;
|
||||
client_id?: number | "";
|
||||
user_id?: number | "";
|
||||
};
|
||||
|
||||
export default function sanitizeClientRule({
|
||||
rule,
|
||||
client_id,
|
||||
user_id,
|
||||
}: Params): BUN_SQLITE_WGUI_CLIENT_RULES | undefined {
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rule_type = rule.rule_type == "all" ? "all" : "destination";
|
||||
|
||||
return {
|
||||
...(rule.id ? { id: rule.id } : {}),
|
||||
user_id: user_id || rule.user_id,
|
||||
client_id: client_id || rule.client_id,
|
||||
rule_type,
|
||||
destination:
|
||||
rule_type == "all" ? "" : (rule.destination || "").trim(),
|
||||
ports: rule_type == "all" ? "" : (rule.ports || "").trim(),
|
||||
protocol:
|
||||
rule_type == "all"
|
||||
? "any"
|
||||
: rule.protocol == "tcp" || rule.protocol == "udp"
|
||||
? rule.protocol
|
||||
: "any",
|
||||
};
|
||||
}
|
||||
@@ -64,11 +64,7 @@ export default async function setupWireguardClient({
|
||||
CLIENT_DIR,
|
||||
CLIENT_PRIVATE_KEY_FILE,
|
||||
CLIENT_PUBLIC_KEY_FILE,
|
||||
HOST_CLIENTS_DIR,
|
||||
HOST_CONFIG_DIR,
|
||||
CLIENT_PRIVATE_KEY,
|
||||
CLIENT_PUBLIC_KEY,
|
||||
HOST_PUBLIC_KEY,
|
||||
} = grabClientDirnames({ client, host_id });
|
||||
|
||||
if (!CLIENT_WG_IP) {
|
||||
@@ -114,6 +110,19 @@ export default async function setupWireguardClient({
|
||||
client?.public_ip_address ||
|
||||
HOST_WG_IP;
|
||||
|
||||
const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const HOST_PUBLIC_KEY = execSync(
|
||||
`cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
||||
{ encoding: "utf-8" },
|
||||
).trim();
|
||||
|
||||
let sh = ``;
|
||||
|
||||
sh += `cd ${CLIENT_DIR}\n`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
BUN_SQLITE_WGUI_CLIENT_RULES,
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
BUN_SQLITE_WGUI_VARIABLES,
|
||||
} from "@/db/types/db";
|
||||
@@ -12,8 +13,8 @@ import type { TableType, User } from "@/src/types";
|
||||
import checkPrivateIPAvailability from "./check-private-ip-availability";
|
||||
import manageWireguardHost from "./manage-wireguard-host";
|
||||
import grabHostDirnames from "./grab-host-dir-names";
|
||||
import path from "node:path";
|
||||
import grabClientDirnames from "./grab-client-dir-names";
|
||||
import buildHostIptablesScripts from "./build-host-iptables-scripts";
|
||||
|
||||
const { WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME } =
|
||||
grabDirNames();
|
||||
@@ -69,6 +70,36 @@ export default async function setupWireguardHost({
|
||||
|
||||
const clients = host_clients_res.payload || [];
|
||||
|
||||
const client_rules_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_CLIENT_RULES,
|
||||
TableType
|
||||
>({
|
||||
table: "client_rules",
|
||||
});
|
||||
|
||||
const client_ids = new Set(
|
||||
clients
|
||||
.map((client) => client.id)
|
||||
.filter((id): id is number => Boolean(id)),
|
||||
);
|
||||
|
||||
const rules_by_client_id = new Map<
|
||||
number,
|
||||
BUN_SQLITE_WGUI_CLIENT_RULES[]
|
||||
>();
|
||||
|
||||
for (let i = 0; i < (client_rules_res.payload || []).length; i++) {
|
||||
const rule = client_rules_res.payload?.[i];
|
||||
|
||||
if (!rule?.client_id || !client_ids.has(rule.client_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing_rules = rules_by_client_id.get(rule.client_id) || [];
|
||||
existing_rules.push(rule);
|
||||
rules_by_client_id.set(rule.client_id, existing_rules);
|
||||
}
|
||||
|
||||
const TARGET_INTERFACE = await grabHostNetworkInterface();
|
||||
const HOST_WG_IP =
|
||||
host?.wg_ip_address ||
|
||||
@@ -172,37 +203,37 @@ export default async function setupWireguardHost({
|
||||
|
||||
sh += `cd ${HOST_CONFIG_DIR}\n`;
|
||||
|
||||
sh += `cat > ${POST_UP_PATH} << EOF\n`;
|
||||
sh += `#!/bin/bash\n\n`;
|
||||
sh += `# Allow WireGuard traffic to/from the server itself\n`;
|
||||
sh += `iptables -I INPUT 1 -i ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `iptables -I OUTPUT 1 -o ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `\n`;
|
||||
sh += `# Allow WireGuard traffic to be forwarded (insert above Docker rules)\n`;
|
||||
sh += `iptables -I FORWARD 1 -i ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `iptables -I FORWARD 1 -o ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `\n`;
|
||||
sh += `iptables -t nat -A POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`;
|
||||
sh += `EOF\n`;
|
||||
sh += `chmod +x ${POST_UP_PATH}\n`;
|
||||
const iptables_scripts = buildHostIptablesScripts({
|
||||
host_id: Number(HOST_ID),
|
||||
interface_name: INTERFACE_NAME,
|
||||
target_interface: TARGET_INTERFACE || "eth0",
|
||||
clients: clients.map((client) => ({
|
||||
id: client.id,
|
||||
wg_ip_address: client.wg_ip_address,
|
||||
rules:
|
||||
client.id && typeof client.id == "number"
|
||||
? rules_by_client_id.get(client.id) || []
|
||||
: [],
|
||||
})),
|
||||
});
|
||||
|
||||
sh += `\n`;
|
||||
if (
|
||||
!iptables_scripts.success ||
|
||||
!iptables_scripts.post_up ||
|
||||
!iptables_scripts.post_down
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
msg: iptables_scripts.msg || `Could not build iptables scripts`,
|
||||
};
|
||||
}
|
||||
|
||||
sh += `cat > ${POST_DOWN_PATH} << EOF\n`;
|
||||
sh += `#!/bin/bash\n\n`;
|
||||
sh += `# Remove WireGuard INPUT/OUTPUT rules\n`;
|
||||
sh += `iptables -D INPUT -i ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `iptables -D OUTPUT -o ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `\n`;
|
||||
sh += `# Remove FORWARD rules\n`;
|
||||
sh += `iptables -D FORWARD -i ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `iptables -D FORWARD -o ${INTERFACE_NAME} -j ACCEPT\n`;
|
||||
sh += `\n`;
|
||||
sh += `iptables -t nat -D POSTROUTING -o ${TARGET_INTERFACE} -j MASQUERADE\n`;
|
||||
sh += `EOF\n`;
|
||||
sh += `chmod +x ${POST_DOWN_PATH}\n`;
|
||||
|
||||
sh += `\n`;
|
||||
execSync(`mkdir -p ${HOST_IPTABLES_DIR}`, { encoding: "utf-8" });
|
||||
await Bun.write(POST_UP_PATH, iptables_scripts.post_up);
|
||||
await Bun.write(POST_DOWN_PATH, iptables_scripts.post_down);
|
||||
execSync(`chmod +x ${POST_UP_PATH} ${POST_DOWN_PATH}`, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
sh += `cat > ${INTERFACE_NAME}.conf << EOF\n`;
|
||||
sh += `[Interface]\n`;
|
||||
@@ -218,14 +249,21 @@ export default async function setupWireguardHost({
|
||||
const client = clients[i];
|
||||
if (!client?.id) continue;
|
||||
|
||||
const { CLIENT_PUBLIC_KEY } = grabClientDirnames({
|
||||
const { CLIENT_PUBLIC_KEY_FILE } = grabClientDirnames({
|
||||
client,
|
||||
host_id: HOST_ID,
|
||||
});
|
||||
|
||||
const CLIENT_PUBLIC_KEY = execSync(
|
||||
`cat ${CLIENT_PUBLIC_KEY_FILE}`,
|
||||
{
|
||||
encoding: "utf-8",
|
||||
},
|
||||
).trim();
|
||||
|
||||
sh += `[Peer]\n`;
|
||||
sh += `PublicKey = ${CLIENT_PUBLIC_KEY}\n`;
|
||||
sh += `AllowedIPs = ${client.allowed_ips}\n`;
|
||||
sh += `AllowedIPs = ${client.wg_ip_address}/32\n`;
|
||||
sh += `\n`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,27 +35,40 @@ export default async function syncWireguardHosts() {
|
||||
table: "hosts",
|
||||
});
|
||||
|
||||
const main_host_id = AppData["WireguardHostID"];
|
||||
const main_host_ip = variables.payload?.find(
|
||||
(v) => v.key == "main_host_wg_ip_address",
|
||||
)?.value;
|
||||
|
||||
const main_host_public_ip = variables.payload?.find(
|
||||
(v) => v.key == "main_host_public_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 all_hosts: BUN_SQLITE_WGUI_HOSTS[] = [
|
||||
{
|
||||
id: 0,
|
||||
public_ip_address: main_host_public_ip,
|
||||
wg_ip_address: main_host_ip,
|
||||
},
|
||||
...(hosts.payload || []),
|
||||
];
|
||||
|
||||
const res = manageWireguardHost({ action: "up", host_id });
|
||||
for (const host of all_hosts) {
|
||||
if (typeof host.id == "number") {
|
||||
const res = manageWireguardHost({
|
||||
action: "up",
|
||||
host_id: host.id,
|
||||
});
|
||||
|
||||
bunext.bunextLog.info(
|
||||
`[wgui] wireguard host ${host_config_file_name}: ${
|
||||
res.success ? `up` : `failed — ${res.msg}`
|
||||
}`,
|
||||
);
|
||||
bunext.bunextLog.info(
|
||||
`[wgui] wireguard host ${host.id}: ${
|
||||
res.success ? `up` : `failed — ${res.msg}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(`[wgui] skipping wireguard host sync — ${error.message}`);
|
||||
|
||||
@@ -139,8 +139,6 @@ export default async function updateWireguardHostPublicIP({
|
||||
skip_host_setup: true,
|
||||
});
|
||||
|
||||
console.log("client_setup_res", client_setup_res);
|
||||
|
||||
if (client_setup_res.success) {
|
||||
updated_count++;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db";
|
||||
import {
|
||||
ClientRuleProtocols,
|
||||
ClientRuleTypes,
|
||||
} from "@/src/dict/client-rules-dict";
|
||||
import type { ClientRuleProtocol, ClientRuleType } from "@/src/types";
|
||||
import parsePortList from "@/src/utils/parse-port-list";
|
||||
import validateIpv4Cidr from "@/src/utils/validate-ipv4-cidr";
|
||||
|
||||
type Params = {
|
||||
rule?: BUN_SQLITE_WGUI_CLIENT_RULES | null;
|
||||
};
|
||||
|
||||
const RULE_TYPES = ClientRuleTypes.map((rt) => rt.value);
|
||||
const PROTOCOLS = ClientRuleProtocols.map((p) => p.value);
|
||||
|
||||
export default function validateClientRule({ rule }: Params) {
|
||||
if (!rule) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No client rule provided`,
|
||||
};
|
||||
}
|
||||
|
||||
const rule_type = rule.rule_type as ClientRuleType | undefined;
|
||||
|
||||
if (!rule_type || !RULE_TYPES.includes(rule_type)) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid rule type`,
|
||||
};
|
||||
}
|
||||
|
||||
const protocol = (rule.protocol || "any") as ClientRuleProtocol;
|
||||
|
||||
if (!PROTOCOLS.includes(protocol)) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid protocol`,
|
||||
};
|
||||
}
|
||||
|
||||
if (rule_type == "all") {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
const destination = rule.destination?.trim() || "";
|
||||
const ports = rule.ports?.trim() || "";
|
||||
|
||||
if (!destination && !ports && protocol == "any") {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Destination rules need an IP, ports, or a protocol`,
|
||||
};
|
||||
}
|
||||
|
||||
if (destination && !validateIpv4Cidr({ value: destination })) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid destination "${destination}"`,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed_ports = parsePortList({ ports });
|
||||
|
||||
if (!parsed_ports.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: parsed_ports.msg,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user