322 lines
9.5 KiB
TypeScript
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,
|
|
};
|
|
}
|