This commit is contained in:
2026-09-20 07:27:29 +01:00
parent dc2cd66b25
commit 8ec44ee0a7
70 changed files with 2657 additions and 128 deletions
@@ -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,
};
}