Files
wireguard-ui/src/functions/backend/firewall/allow-subnet-firewall-rules.ts
T

192 lines
5.5 KiB
TypeScript

import { execSync } from "node:child_process";
import validateIpv4Cidr from "@/src/utils/validate-ipv4-cidr";
import detectFirewallManager, {
type FirewallManager,
} from "./detect-firewall-manager";
export type AllowSubnetFirewallRulesResult = {
success: boolean;
manager: FirewallManager | "none";
msg: string;
added_rules: string[];
skipped_rules: string[];
};
type Params = {
subnet: string;
listen_port: number;
};
type ShellResult = {
success: boolean;
output: string;
};
const tryShell = ({ command }: { command: string }): ShellResult => {
try {
const result = execSync(command, {
encoding: "utf-8",
stdio: "pipe",
timeout: 30000,
});
return {
success: true,
output: String(result).trim(),
};
} catch (error: any) {
const stdout = error?.stdout ? String(error.stdout).trim() : "";
const stderr = error?.stderr ? String(error.stderr).trim() : "";
return {
success: false,
output: [stdout, stderr].filter(Boolean).join("\n").trim(),
};
}
};
const isValidPort = ({ port }: { port: number }) =>
Number.isInteger(port) && port >= 1 && port <= 65535;
export default async function allowSubnetFirewallRules({
subnet,
listen_port,
}: Params): Promise<AllowSubnetFirewallRulesResult> {
const subnet_trimmed = subnet?.trim();
if (!validateIpv4Cidr({ value: subnet_trimmed })) {
return {
success: false,
manager: "none",
msg: `Invalid subnet "${subnet_trimmed}"`,
added_rules: [],
skipped_rules: [],
};
}
if (!isValidPort({ port: listen_port })) {
return {
success: false,
manager: "none",
msg: `Invalid listen port ${listen_port}`,
added_rules: [],
skipped_rules: [],
};
}
const manager = detectFirewallManager();
const added_rules: string[] = [];
const skipped_rules: string[] = [];
if (manager == "firewalld") {
const subnet_rule = `rule family="ipv4" source address="${subnet_trimmed}" accept`;
const subnet_add = tryShell({
command: `firewall-cmd --permanent --add-rich-rule='${subnet_rule}'`,
});
if (subnet_add.success) {
added_rules.push(`allow all traffic from ${subnet_trimmed}`);
} else {
skipped_rules.push(
`firewalld subnet rule not applied: ${subnet_add.output}`,
);
}
const port_rule = `${listen_port}/udp`;
const port_add = tryShell({
command: `firewall-cmd --permanent --add-port=${port_rule}`,
});
if (port_add.success) {
added_rules.push(`allow ${port_rule} from anywhere (wireguard)`);
} else {
skipped_rules.push(
`firewalld port rule not applied: ${port_add.output}`,
);
}
const reload = tryShell({ command: `firewall-cmd --reload` });
if (!reload.success) {
skipped_rules.push(`firewalld reload failed: ${reload.output}`);
}
} else if (manager == "ufw") {
const ufw_status = tryShell({ command: `ufw status` });
if (
!ufw_status.success ||
!ufw_status.output.includes(`Status: active`)
) {
return {
success: false,
manager,
msg: `ufw is not active — cannot add firewall rules`,
added_rules,
skipped_rules,
};
}
if (ufw_status.output.includes(subnet_trimmed)) {
skipped_rules.push(`subnet ${subnet_trimmed} already allowed`);
} else {
const subnet_add = tryShell({
command: `ufw allow from ${subnet_trimmed}`,
});
if (subnet_add.success) {
added_rules.push(`allow all traffic from ${subnet_trimmed}`);
} else {
skipped_rules.push(
`ufw subnet rule not applied: ${subnet_add.output}`,
);
}
}
const port_rule = `${listen_port}/udp`;
if (ufw_status.output.includes(port_rule)) {
skipped_rules.push(`port ${port_rule} already allowed`);
} else {
const port_add = tryShell({ command: `ufw allow ${port_rule}` });
if (port_add.success) {
added_rules.push(
`allow ${port_rule} from anywhere (wireguard)`,
);
} else {
skipped_rules.push(
`ufw port rule not applied: ${port_add.output}`,
);
}
}
}
if (manager == "none") {
return {
success: true,
manager,
msg: `no active firewall manager detected (firewalld/ufw) — no rules added; if you use iptables/nftables directly, allow ${subnet_trimmed} and udp port ${listen_port} manually`,
added_rules,
skipped_rules,
};
}
const summary = [
added_rules.length
? `firewall(${manager}): added — ${added_rules.join("; ")}`
: `firewall(${manager}): no new rules added`,
skipped_rules.length ? `skipped — ${skipped_rules.join("; ")}` : ``,
]
.filter(Boolean)
.join(` `);
return {
success: true,
manager,
msg: summary,
added_rules,
skipped_rules,
};
}