Files
wireguard-ui/src/functions/backend/setup/build-host-iptables-scripts.ts
T
2026-09-28 12:37:24 +01:00

322 lines
9.5 KiB
TypeScript

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 validateIpv4Cidr from "@/src/utils/validate-ipv4-cidr";
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;
host_subnet?: 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({
forward_chain,
input_chain,
source,
protocol,
ports,
host_wg_ip,
}: {
forward_chain: string;
input_chain: string;
source: string;
protocol?: string;
ports: string[];
host_wg_ip?: 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] || [];
const port_match_str = proto
? portMatch({ ports: chunk })
: undefined;
lines.push(
joinArgs([
`iptables -A ${forward_chain}`,
`-s ${source}`,
proto ? `-p ${proto}` : undefined,
port_match_str,
`-j ACCEPT`,
]),
);
lines.push(
joinArgs([
`iptables -A ${input_chain}`,
`-s ${source}`,
host_wg_ip ? `-d ${host_wg_ip}` : undefined,
proto ? `-p ${proto}` : undefined,
port_match_str,
`-j ACCEPT`,
]),
);
}
}
return lines;
}
export default function buildHostIptablesScripts({
host_id,
interface_name,
target_interface,
host_subnet,
clients,
}: Params) {
if (!Number.isSafeInteger(host_id) || host_id < 0 || host_id > 999999999) {
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 scoped_subnet = host_subnet?.trim();
if (scoped_subnet && !validateIpv4Cidr({ value: scoped_subnet })) {
return {
success: false,
msg: `Invalid host subnet "${scoped_subnet}"`,
};
}
const { forward, input } = deriveIptablesChainNames({ host_id });
const masquerade_match = scoped_subnet ? `-s ${scoped_subnet} ` : ``;
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`,
scoped_subnet
? `iptables -A ${input} -s ${source} -d ${scoped_subnet} -j ACCEPT`
: `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({
forward_chain: forward,
input_chain: input,
source,
protocol: rule.protocol || "any",
ports: parsed_ports.ports,
host_wg_ip: scoped_subnet,
}),
);
}
}
accept_lines.push(
`# Prevent clients from forwarding traffic back into the WireGuard network`,
`iptables -A ${forward} -o ${interface_name} -j DROP`,
);
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`,
``,
`iptables -A ${input} -i ${interface_name} -p udp --dport 53 -j ACCEPT`,
`iptables -A ${input} -i ${interface_name} -p tcp --dport 53 -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 ${masquerade_match} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING ${masquerade_match} -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 ${masquerade_match} -j MASQUERADE 2>/dev/null || true`,
``,
].join("\n");
return {
success: true,
post_up,
post_down,
};
}