Add firewall rules logic for all hosts
This commit is contained in:
@@ -52,6 +52,36 @@ The install script will:
|
|||||||
8. Install the `wg-quick` helper script
|
8. Install the `wg-quick` helper script
|
||||||
9. Set up a systemd or OpenRC service (`wgui`)
|
9. Set up a systemd or OpenRC service (`wgui`)
|
||||||
|
|
||||||
|
The installer **never modifies your firewall** — it will print the commands to
|
||||||
|
temporarily allow the dashboard port so you can reach the first login, and then
|
||||||
|
point you to **Setup → Firewall** for reviewing and removing rules.
|
||||||
|
|
||||||
|
### Firewall
|
||||||
|
|
||||||
|
Firewall rules are managed per host, automatically:
|
||||||
|
|
||||||
|
1. **Bootstrap (manual, temporary)** — allow the dashboard port so you can
|
||||||
|
reach the web UI once:
|
||||||
|
```bash
|
||||||
|
ufw allow 10752/tcp # ufw (Debian/Ubuntu)
|
||||||
|
firewall-cmd --permanent --add-port=10752/tcp && firewall-cmd --reload # firewalld (RHEL/Fedora)
|
||||||
|
```
|
||||||
|
2. **Host creation** — every host setup automatically allows its entire
|
||||||
|
wireguard subnet (e.g. `10.5.0.1` → `10.5.0.0/24` from anywhere) and the
|
||||||
|
host's wireguard listen port (e.g. `51821/udp`) in ufw or firewalld.
|
||||||
|
Detailed per-client traffic rules are enforced by the existing iptables
|
||||||
|
chain scripts, not the host firewall.
|
||||||
|
3. **Host deletion** — the host's subnet and listen-port rules are removed
|
||||||
|
automatically.
|
||||||
|
4. **Cleanup** — once everything is reachable via the tunnel, remove the
|
||||||
|
temporary bootstrap rule and review all rules under **Setup → Firewall**:
|
||||||
|
```bash
|
||||||
|
ufw delete allow 10752/tcp
|
||||||
|
firewall-cmd --permanent --remove-port=10752/tcp && firewall-cmd --reload
|
||||||
|
```
|
||||||
|
Rules that no longer match any host (`temporary`/`orphaned`) are flagged
|
||||||
|
with copyable removal commands on the setup page.
|
||||||
|
|
||||||
### Manual (Development)
|
### Manual (Development)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
export type FirewallManager = "firewalld" | "ufw" | "none";
|
||||||
|
|
||||||
|
export default function detectFirewallManager(): FirewallManager {
|
||||||
|
try {
|
||||||
|
execSync(`command -v firewall-cmd`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
execSync(`systemctl is-active --quiet firewalld`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return "firewalld";
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ufw_status = execSync(`ufw status`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (String(ufw_status).includes(`Status: active`)) {
|
||||||
|
return "ufw";
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
|
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||||
|
import { AppData } from "@/src/data/app-data";
|
||||||
|
import { SiteData } from "@/src/data/site-data";
|
||||||
|
import type { TableType } from "@/src/types";
|
||||||
|
import validateIpv4Cidr from "@/src/utils/validate-ipv4-cidr";
|
||||||
|
import detectFirewallManager, {
|
||||||
|
type FirewallManager,
|
||||||
|
} from "./detect-firewall-manager";
|
||||||
|
|
||||||
|
export type FirewallRuleKind = "required" | "temporary" | "orphaned" | "other";
|
||||||
|
|
||||||
|
export type FirewallRuleInfo = {
|
||||||
|
id: string;
|
||||||
|
kind: FirewallRuleKind;
|
||||||
|
description: string;
|
||||||
|
delete_command?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FirewallStatus = {
|
||||||
|
manager: FirewallManager;
|
||||||
|
rules: FirewallRuleInfo[];
|
||||||
|
managed_subnets: string[];
|
||||||
|
managed_ports: string[];
|
||||||
|
dashboard_port: number;
|
||||||
|
hosts_found: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShellResult = {
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryShell = ({ command }: { command: string }): ShellResult => {
|
||||||
|
try {
|
||||||
|
const result = execSync(command, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 isCidr = ({ value }: { value: string }) => validateIpv4Cidr({ value });
|
||||||
|
|
||||||
|
const stripV6Suffix = ({ value }: { value: string }) =>
|
||||||
|
value.replace(/\s*\(v6\)$/i, "").trim();
|
||||||
|
|
||||||
|
const normalizeSpec = ({ value }: { value: string }) =>
|
||||||
|
stripV6Suffix({ value: value.toLowerCase() });
|
||||||
|
|
||||||
|
type ManagedSpecs = {
|
||||||
|
managed_subnets: string[];
|
||||||
|
managed_ports: string[];
|
||||||
|
dashboard_port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ClassifiedRule = {
|
||||||
|
id: string;
|
||||||
|
kind: FirewallRuleKind;
|
||||||
|
description: string;
|
||||||
|
delete_command?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const classifyRule = ({
|
||||||
|
spec,
|
||||||
|
manager,
|
||||||
|
managed,
|
||||||
|
}: {
|
||||||
|
spec: string;
|
||||||
|
manager: "ufw" | "firewalld";
|
||||||
|
managed: ManagedSpecs;
|
||||||
|
}): ClassifiedRule | null => {
|
||||||
|
const normalized = normalizeSpec({ value: spec });
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized == `${managed.dashboard_port}/tcp`) {
|
||||||
|
const delete_command =
|
||||||
|
manager == "ufw"
|
||||||
|
? `ufw delete allow ${normalized}`
|
||||||
|
: `firewall-cmd --permanent --remove-port=${normalized} && firewall-cmd --reload`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${manager}-${normalized}`,
|
||||||
|
kind: "temporary",
|
||||||
|
description: `TCP ${managed.dashboard_port} open to the world — the wg-ui dashboard bootstrap rule (remove once hosts are reachable via the tunnel)`,
|
||||||
|
delete_command,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (managed.managed_ports.includes(normalized)) {
|
||||||
|
return {
|
||||||
|
id: `${manager}-${normalized}`,
|
||||||
|
kind: "required",
|
||||||
|
description: `${normalized} — wireguard listen port (matches a host)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCidr({ value: normalized })) {
|
||||||
|
if (managed.managed_subnets.includes(normalized)) {
|
||||||
|
return {
|
||||||
|
id: `${manager}-${normalized}`,
|
||||||
|
kind: "required",
|
||||||
|
description: `All traffic from ${normalized} — host subnet`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const delete_command =
|
||||||
|
manager == "ufw"
|
||||||
|
? `ufw delete allow from ${normalized}`
|
||||||
|
: `firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address="${normalized}" accept' && firewall-cmd --reload`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${manager}-${normalized}`,
|
||||||
|
kind: "orphaned",
|
||||||
|
description: `All traffic from ${normalized} — matches no host subnet (stale, safe to remove)`,
|
||||||
|
delete_command,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${manager}-${normalized}`,
|
||||||
|
kind: "other",
|
||||||
|
description: `${normalized} — unmanaged rule`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const grabUfwRules = ({
|
||||||
|
managed,
|
||||||
|
}: {
|
||||||
|
managed: ManagedSpecs;
|
||||||
|
}): ClassifiedRule[] => {
|
||||||
|
const status = tryShell({ command: `ufw status numbered` });
|
||||||
|
|
||||||
|
if (!status.success) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules: ClassifiedRule[] = [];
|
||||||
|
|
||||||
|
for (const line of status.output.split("\n")) {
|
||||||
|
const stripped = line
|
||||||
|
.replace(/^\[\s*\d+\]\s*/, "")
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const status_index = stripped.indexOf("ALLOW");
|
||||||
|
|
||||||
|
if (status_index < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spec = stripped.slice(0, status_index).join(" ");
|
||||||
|
const rule = classifyRule({ spec, manager: "ufw", managed });
|
||||||
|
|
||||||
|
if (rule) {
|
||||||
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
};
|
||||||
|
|
||||||
|
const grabFirewalldRules = ({
|
||||||
|
managed,
|
||||||
|
}: {
|
||||||
|
managed: ManagedSpecs;
|
||||||
|
}): ClassifiedRule[] => {
|
||||||
|
const rules: ClassifiedRule[] = [];
|
||||||
|
const port_res = tryShell({ command: `firewall-cmd --permanent --list-ports` });
|
||||||
|
|
||||||
|
if (port_res.success) {
|
||||||
|
for (const spec of port_res.output.split(/\s+/).filter(Boolean)) {
|
||||||
|
const rule = classifyRule({ spec, manager: "firewalld", managed });
|
||||||
|
|
||||||
|
if (rule) {
|
||||||
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rich_res = tryShell({
|
||||||
|
command: `firewall-cmd --permanent --list-rich-rules`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rich_res.success) {
|
||||||
|
for (const line of rich_res.output.split("\n")) {
|
||||||
|
const source_match = line.match(/source\s+address="([^"]+)"/);
|
||||||
|
|
||||||
|
if (!source_match?.[1]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rule = classifyRule({
|
||||||
|
spec: source_match[1],
|
||||||
|
manager: "firewalld",
|
||||||
|
managed,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rule) {
|
||||||
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function grabFirewallStatus(): Promise<FirewallStatus> {
|
||||||
|
const manager = detectFirewallManager();
|
||||||
|
|
||||||
|
const hosts_res = await BunSQLite.select<BUN_SQLITE_WGUI_HOSTS, TableType>({
|
||||||
|
table: "hosts",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hosts = hosts_res.payload || [];
|
||||||
|
const managed_subnets: string[] = [];
|
||||||
|
const managed_ports: string[] = [];
|
||||||
|
|
||||||
|
for (const host of hosts) {
|
||||||
|
if (host?.wg_ip_address) {
|
||||||
|
managed_subnets.push(
|
||||||
|
`${host.wg_ip_address.split(".").slice(0, 3).join(".")}.0/24`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listen_port =
|
||||||
|
host?.listen_port || AppData["DefaultWGListenPort"];
|
||||||
|
|
||||||
|
const port_spec = `${listen_port}/udp`;
|
||||||
|
|
||||||
|
if (!managed_ports.includes(port_spec)) {
|
||||||
|
managed_ports.push(port_spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const managed: ManagedSpecs = {
|
||||||
|
managed_subnets,
|
||||||
|
managed_ports,
|
||||||
|
dashboard_port: SiteData["ServerPort"],
|
||||||
|
};
|
||||||
|
|
||||||
|
let rules: ClassifiedRule[] = [];
|
||||||
|
|
||||||
|
if (manager == "ufw") {
|
||||||
|
rules = grabUfwRules({ managed });
|
||||||
|
} else if (manager == "firewalld") {
|
||||||
|
rules = grabFirewalldRules({ managed });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
manager,
|
||||||
|
rules,
|
||||||
|
managed_subnets,
|
||||||
|
managed_ports,
|
||||||
|
dashboard_port: SiteData["ServerPort"],
|
||||||
|
hosts_found: hosts.length > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import validateIpv4Cidr from "@/src/utils/validate-ipv4-cidr";
|
||||||
|
import detectFirewallManager, {
|
||||||
|
type FirewallManager,
|
||||||
|
} from "./detect-firewall-manager";
|
||||||
|
|
||||||
|
export type RemoveSubnetFirewallRulesResult = {
|
||||||
|
success: boolean;
|
||||||
|
manager: FirewallManager | "none";
|
||||||
|
msg: string;
|
||||||
|
removed_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 removeSubnetFirewallRules({
|
||||||
|
subnet,
|
||||||
|
listen_port,
|
||||||
|
}: Params): Promise<RemoveSubnetFirewallRulesResult> {
|
||||||
|
const subnet_trimmed = subnet?.trim();
|
||||||
|
|
||||||
|
if (!validateIpv4Cidr({ value: subnet_trimmed })) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
manager: "none",
|
||||||
|
msg: `Invalid subnet "${subnet_trimmed}"`,
|
||||||
|
removed_rules: [],
|
||||||
|
skipped_rules: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidPort({ port: listen_port })) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
manager: "none",
|
||||||
|
msg: `Invalid listen port ${listen_port}`,
|
||||||
|
removed_rules: [],
|
||||||
|
skipped_rules: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = detectFirewallManager();
|
||||||
|
const removed_rules: string[] = [];
|
||||||
|
const skipped_rules: string[] = [];
|
||||||
|
|
||||||
|
if (manager == "firewalld") {
|
||||||
|
const subnet_rule = `rule family="ipv4" source address="${subnet_trimmed}" accept`;
|
||||||
|
const subnet_remove = tryShell({
|
||||||
|
command: `firewall-cmd --permanent --remove-rich-rule='${subnet_rule}'`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subnet_remove.success) {
|
||||||
|
removed_rules.push(`removed subnet allow for ${subnet_trimmed}`);
|
||||||
|
} else {
|
||||||
|
skipped_rules.push(
|
||||||
|
`firewalld subnet rule not removed: ${subnet_remove.output}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const port_rule = `${listen_port}/udp`;
|
||||||
|
const port_remove = tryShell({
|
||||||
|
command: `firewall-cmd --permanent --remove-port=${port_rule}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (port_remove.success) {
|
||||||
|
removed_rules.push(`removed ${port_rule} allow`);
|
||||||
|
} else {
|
||||||
|
skipped_rules.push(`firewalld port rule not removed: ${port_remove.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` });
|
||||||
|
const ufw_rules = ufw_status.success ? ufw_status.output : "";
|
||||||
|
|
||||||
|
if (ufw_rules.includes(subnet_trimmed)) {
|
||||||
|
const subnet_remove = tryShell({
|
||||||
|
command: `ufw delete allow from ${subnet_trimmed}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subnet_remove.success) {
|
||||||
|
removed_rules.push(`removed subnet allow for ${subnet_trimmed}`);
|
||||||
|
} else {
|
||||||
|
skipped_rules.push(
|
||||||
|
`ufw subnet rule not removed: ${subnet_remove.output}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
skipped_rules.push(`subnet ${subnet_trimmed} not present — nothing to remove`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const port_rule = `${listen_port}/udp`;
|
||||||
|
|
||||||
|
if (ufw_rules.includes(port_rule)) {
|
||||||
|
const port_remove = tryShell({
|
||||||
|
command: `ufw delete allow ${port_rule}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (port_remove.success) {
|
||||||
|
removed_rules.push(`removed ${port_rule} allow`);
|
||||||
|
} else {
|
||||||
|
skipped_rules.push(`ufw port rule not removed: ${port_remove.output}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
skipped_rules.push(`port ${port_rule} not present — nothing to remove`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager == "none") {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
manager,
|
||||||
|
msg: `no active firewall manager detected (firewalld/ufw) — no rules removed; if you use iptables/nftables directly, remove ${subnet_trimmed} and udp port ${listen_port} rules manually`,
|
||||||
|
removed_rules,
|
||||||
|
skipped_rules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = [
|
||||||
|
removed_rules.length
|
||||||
|
? `firewall(${manager}): removed — ${removed_rules.join("; ")}`
|
||||||
|
: `firewall(${manager}): no rules removed`,
|
||||||
|
skipped_rules.length ? `skipped — ${skipped_rules.join("; ")}` : ``,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(` `);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
manager,
|
||||||
|
msg: summary,
|
||||||
|
removed_rules,
|
||||||
|
skipped_rules,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import type { TableType, User } from "@/src/types";
|
|||||||
import manageWireguardHost from "./manage-wireguard-host";
|
import manageWireguardHost from "./manage-wireguard-host";
|
||||||
import grabHostDirnames from "./grab-host-dir-names";
|
import grabHostDirnames from "./grab-host-dir-names";
|
||||||
import grabClientDirnames from "./grab-client-dir-names";
|
import grabClientDirnames from "./grab-client-dir-names";
|
||||||
|
import removeSubnetFirewallRules from "../firewall/remove-subnet-firewall-rules";
|
||||||
|
import { AppData } from "@/src/data/app-data";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
@@ -74,6 +76,41 @@ export default async function deleteWireguardHost({
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const host_res = await BunSQLite.select<
|
||||||
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "hosts",
|
||||||
|
targetId: host_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const host = host_res.singleRes;
|
||||||
|
|
||||||
|
if (host?.wg_ip_address) {
|
||||||
|
const host_subnet = `${host.wg_ip_address
|
||||||
|
.split(".")
|
||||||
|
.slice(0, 3)
|
||||||
|
.join(".")}.0/24`;
|
||||||
|
|
||||||
|
const firewall_res = await removeSubnetFirewallRules({
|
||||||
|
subnet: host_subnet,
|
||||||
|
listen_port:
|
||||||
|
host.listen_port || AppData["DefaultWGListenPort"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!firewall_res.success) {
|
||||||
|
console.log(
|
||||||
|
`Warning removing firewall rules for host ${host_id} => ${firewall_res.msg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(
|
||||||
|
`Warning removing firewall rules for host ${host_id} => ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(`rm -rf ${HOST_CONFIG_DIR}`, { encoding: "utf-8" });
|
execSync(`rm -rf ${HOST_CONFIG_DIR}`, { encoding: "utf-8" });
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import manageWireguardHost from "./manage-wireguard-host";
|
|||||||
import grabHostDirnames from "./grab-host-dir-names";
|
import grabHostDirnames from "./grab-host-dir-names";
|
||||||
import grabClientDirnames from "./grab-client-dir-names";
|
import grabClientDirnames from "./grab-client-dir-names";
|
||||||
import buildHostIptablesScripts from "./build-host-iptables-scripts";
|
import buildHostIptablesScripts from "./build-host-iptables-scripts";
|
||||||
|
import allowSubnetFirewallRules from "../firewall/allow-subnet-firewall-rules";
|
||||||
import { AppData } from "@/src/data/app-data";
|
import { AppData } from "@/src/data/app-data";
|
||||||
|
|
||||||
const { WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME } =
|
const { WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME } =
|
||||||
@@ -267,9 +268,20 @@ export default async function setupWireguardHost({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firewall_res = await allowSubnetFirewallRules({
|
||||||
|
subnet: HOST_SUBNET,
|
||||||
|
listen_port: host?.listen_port || AppData["DefaultWGListenPort"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!firewall_res.success) {
|
||||||
|
console.log(
|
||||||
|
`Warning setting up firewall rules for host ${HOST_ID} => ${firewall_res.msg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
msg: [exec.trim(), manage_res.msg].join("\n\n"),
|
msg: [exec.trim(), manage_res.msg, firewall_res.msg].join("\n\n"),
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||||
|
import type { FirewallStatus } from "@/src/functions/backend/firewall/grab-firewall-status";
|
||||||
|
import type { ApiReqParams } from "@/src/types";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type FirewallStatusResponse = {
|
||||||
|
success: boolean;
|
||||||
|
singleRes?: FirewallStatus | null;
|
||||||
|
msg?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useFirewallStatus() {
|
||||||
|
const [firewall_status, setFirewallStatus] =
|
||||||
|
useState<FirewallStatus | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const refresh = () =>
|
||||||
|
fetchApi<ApiReqParams, FirewallStatusResponse>(
|
||||||
|
`/api/admin/firewall-status`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
).then((res) => {
|
||||||
|
if (res.success && res.singleRes) {
|
||||||
|
setFirewallStatus(res.singleRes);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setError(res.msg || `Could not fetch firewall status`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshWithLoading = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
.catch((request_error: any) => {
|
||||||
|
setError(
|
||||||
|
request_error?.message ||
|
||||||
|
`Could not fetch firewall status`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshWithLoading();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
firewall_status,
|
||||||
|
refresh: refreshWithLoading,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import CodeBlock from "@/src/components/twui/elements/CodeBlock";
|
||||||
|
import Tag from "@/src/components/twui/elements/Tag";
|
||||||
|
import type {
|
||||||
|
FirewallRuleKind,
|
||||||
|
FirewallRuleInfo,
|
||||||
|
} from "@/src/functions/backend/firewall/grab-firewall-status";
|
||||||
|
import Row from "@/src/components/twui/layout/Row";
|
||||||
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
|
import type { TWUIButtonColorType } from "@/src/components/twui/layout/Button";
|
||||||
|
import {
|
||||||
|
ShieldAlert,
|
||||||
|
ShieldCheck,
|
||||||
|
ShieldX,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
const KIND_META: Record<
|
||||||
|
FirewallRuleKind,
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
color: TWUIButtonColorType;
|
||||||
|
icon: LucideIcon;
|
||||||
|
text_class: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
required: {
|
||||||
|
label: `Active`,
|
||||||
|
color: `success`,
|
||||||
|
icon: ShieldCheck,
|
||||||
|
text_class: `text-success`,
|
||||||
|
},
|
||||||
|
temporary: {
|
||||||
|
label: `Temporary`,
|
||||||
|
color: `warning`,
|
||||||
|
icon: ShieldAlert,
|
||||||
|
text_class: `text-warning`,
|
||||||
|
},
|
||||||
|
orphaned: {
|
||||||
|
label: `Orphaned`,
|
||||||
|
color: `error`,
|
||||||
|
icon: ShieldX,
|
||||||
|
text_class: `text-error`,
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
label: `Unmanaged`,
|
||||||
|
color: `gray`,
|
||||||
|
icon: ShieldCheck,
|
||||||
|
text_class: `text-foreground-light/45 dark:text-foreground-dark/45`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
rule: FirewallRuleInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FirewallRuleRow({ rule }: Props) {
|
||||||
|
const meta = KIND_META[rule.kind];
|
||||||
|
const Icon = meta.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className="w-full items-stretch gap-2.5 rounded-lg bg-background-light dark:bg-[#0e0e10] border border-slate-200 dark:border-white/10 p-3">
|
||||||
|
<Row className="w-full items-start justify-between gap-3 flex-wrap">
|
||||||
|
<Row className="gap-2.5 items-start min-w-0">
|
||||||
|
<Icon
|
||||||
|
size={15}
|
||||||
|
className={`mt-0.5 shrink-0 ${meta.text_class}`}
|
||||||
|
/>
|
||||||
|
<Span className="text-[12.5px] leading-5 min-w-0">
|
||||||
|
{rule.description}
|
||||||
|
</Span>
|
||||||
|
</Row>
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color={meta.color}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{meta.label}
|
||||||
|
</Tag>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{rule.delete_command ? (
|
||||||
|
<CodeBlock
|
||||||
|
singleBlock
|
||||||
|
language="shell"
|
||||||
|
wrapperProps={{
|
||||||
|
className: `max-w-none! xl:max-w-none!`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`# remove rule\n${rule.delete_command}`}
|
||||||
|
</CodeBlock>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import AdminCard from "@/src/components/general/admin-card";
|
||||||
|
import Button from "@/src/components/twui/layout/Button";
|
||||||
|
import H3 from "@/src/components/twui/layout/H3";
|
||||||
|
import P from "@/src/components/twui/layout/P";
|
||||||
|
import Row from "@/src/components/twui/layout/Row";
|
||||||
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
|
import Tag from "@/src/components/twui/elements/Tag";
|
||||||
|
import { RefreshCw, Shield, TriangleAlert } from "lucide-react";
|
||||||
|
import useFirewallStatus from "../(hooks)/use-firewall-status";
|
||||||
|
import FirewallRuleRow from "../(partials)/firewall-rule-row";
|
||||||
|
|
||||||
|
const MANAGER_LABEL: Record<string, string> = {
|
||||||
|
firewalld: `firewalld (active)`,
|
||||||
|
ufw: `ufw (active)`,
|
||||||
|
none: `none detected`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FirewallSection() {
|
||||||
|
const { firewall_status, refresh, loading, error } = useFirewallStatus();
|
||||||
|
|
||||||
|
if (!firewall_status) {
|
||||||
|
return (
|
||||||
|
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||||
|
<Row className="gap-3 items-start">
|
||||||
|
<Span className="p-2.5 rounded-lg bg-primary/10 dark:bg-primary-dark/10 text-primary dark:text-primary-dark shrink-0">
|
||||||
|
<Shield size={19} />
|
||||||
|
</Span>
|
||||||
|
<Stack className="gap-1 min-w-0">
|
||||||
|
<H3 className="text-[14px] font-semibold mb-0!">
|
||||||
|
Firewall
|
||||||
|
</H3>
|
||||||
|
<P
|
||||||
|
noMargin
|
||||||
|
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||||
|
>
|
||||||
|
{error ||
|
||||||
|
`Checking firewall status on this host ...`}
|
||||||
|
</P>
|
||||||
|
</Stack>
|
||||||
|
</Row>
|
||||||
|
</AdminCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { manager, rules, hosts_found } = firewall_status;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||||
|
<Row className="gap-3 items-start">
|
||||||
|
<Span className="p-2.5 rounded-lg bg-primary/10 dark:bg-primary-dark/10 text-primary dark:text-primary-dark shrink-0">
|
||||||
|
<Shield size={19} />
|
||||||
|
</Span>
|
||||||
|
<Stack className="gap-1 min-w-0 flex-1">
|
||||||
|
<Row className="gap-2 items-center flex-wrap">
|
||||||
|
<H3 className="text-[14px] font-semibold mb-0!">
|
||||||
|
Firewall
|
||||||
|
</H3>
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color={manager == "none" ? "warning" : "success"}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{MANAGER_LABEL[manager] || manager}
|
||||||
|
</Tag>
|
||||||
|
</Row>
|
||||||
|
<P
|
||||||
|
noMargin
|
||||||
|
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||||
|
>
|
||||||
|
Host setups automatically allow their wireguard subnet
|
||||||
|
and listen port here. Rules that are no longer needed
|
||||||
|
(bootstrap or stale) are listed with removal commands.
|
||||||
|
</P>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
title="Refresh"
|
||||||
|
variant="outlined"
|
||||||
|
loading={loading}
|
||||||
|
beforeIcon={<RefreshCw size={14} />}
|
||||||
|
onClick={refresh}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{manager == "none" ? (
|
||||||
|
<Row className="gap-2 items-center bg-warning/5 rounded-lg px-3 py-2.5 text-warning">
|
||||||
|
<TriangleAlert size={15} className="shrink-0" />
|
||||||
|
<Span className="text-[12.5px]">
|
||||||
|
No active high-level firewall manager detected
|
||||||
|
(firewalld/ufw). If you use iptables/nftables directly,
|
||||||
|
allow each host subnet and its wireguard listen port
|
||||||
|
manually.
|
||||||
|
</Span>
|
||||||
|
</Row>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{rules.length ? (
|
||||||
|
<Stack className="w-full gap-2 items-stretch">
|
||||||
|
{rules.map((rule, index) => (
|
||||||
|
<FirewallRuleRow
|
||||||
|
key={rule.id || `${manager}-${index}`}
|
||||||
|
rule={rule}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack className="w-full items-start gap-1 rounded-lg bg-background-light dark:bg-[#0e0e10] border border-slate-200 dark:border-white/10 p-3">
|
||||||
|
<Span className="text-[12.5px] text-foreground-light/60 dark:text-foreground-dark/60">
|
||||||
|
{hosts_found
|
||||||
|
? `No wg-ui related firewall rules found.`
|
||||||
|
: `No hosts created yet — creating a host automatically allows its subnet (e.g. 10.5.0.0/24) and its wireguard listen port (e.g. 51821/udp) here.`}
|
||||||
|
</Span>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</AdminCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import Stack from "@/src/components/twui/layout/Stack";
|
|||||||
import useSystemSetupStatus from "./(hooks)/use-system-setup-status";
|
import useSystemSetupStatus from "./(hooks)/use-system-setup-status";
|
||||||
import SystemStatusSection from "./(sections)/system-status-section";
|
import SystemStatusSection from "./(sections)/system-status-section";
|
||||||
import SetupActionsSection from "./(sections)/setup-actions-section";
|
import SetupActionsSection from "./(sections)/setup-actions-section";
|
||||||
|
import FirewallSection from "./(sections)/firewall-section";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { AppContext } from "@/src/pages/__root";
|
import { AppContext } from "@/src/pages/__root";
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export default function AdminWireguardSetupPage() {
|
|||||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||||
<SystemStatusSection setup={setup} />
|
<SystemStatusSection setup={setup} />
|
||||||
<SetupActionsSection setup={setup} />
|
<SetupActionsSection setup={setup} />
|
||||||
|
<FirewallSection />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||||
|
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||||
|
import grabFirewallStatus, {
|
||||||
|
type FirewallStatus,
|
||||||
|
} from "@/src/functions/backend/firewall/grab-firewall-status";
|
||||||
|
import type {
|
||||||
|
APIResponseObject,
|
||||||
|
BunextAPIRouteHandler,
|
||||||
|
} from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
export const handler: BunextAPIRouteHandler<
|
||||||
|
APIResponseObject<FirewallStatus>
|
||||||
|
> = async ({ req }) => {
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user, user_types } = await userAuth({ req });
|
||||||
|
|
||||||
|
if (!user?.logged_in_status) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
logoutUser: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
singleRes: await grabFirewallStatus(),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -245,25 +245,51 @@ grab_port() {
|
|||||||
grep -oE 'ServerPort: [0-9]+' "$INSTALL_DIR/src/data/site-data.ts" 2>/dev/null | grep -oE '[0-9]+' | head -1 || echo "10752"
|
grep -oE 'ServerPort: [0-9]+' "$INSTALL_DIR/src/data/site-data.ts" 2>/dev/null | grep -oE '[0-9]+' | head -1 || echo "10752"
|
||||||
}
|
}
|
||||||
|
|
||||||
configure_firewall() {
|
print_firewall_instructions() {
|
||||||
local port="$1"
|
local port="$1"
|
||||||
log "configuring firewall to allow inbound traffic on port $port/tcp ..."
|
local manager="none"
|
||||||
|
|
||||||
# Check for firewalld (RHEL, CentOS, Fedora, Rocky, Alma)
|
# Check for firewalld (RHEL, CentOS, Fedora, Rocky, Alma)
|
||||||
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld 2>/dev/null; then
|
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld 2>/dev/null; then
|
||||||
log "firewalld is active. opening port $port/tcp ..."
|
manager="firewalld"
|
||||||
firewall-cmd --permanent --add-port="${port}/tcp" >/dev/null 2>&1 || fail "failed to add port to firewalld"
|
|
||||||
firewall-cmd --reload >/dev/null 2>&1 || fail "failed to reload firewalld"
|
|
||||||
|
|
||||||
# Check for ufw (Ubuntu, Debian, Mint)
|
# Check for ufw (Ubuntu, Debian, Mint)
|
||||||
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
|
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||||
log "ufw is active. opening port $port/tcp ..."
|
manager="ufw"
|
||||||
ufw allow "${port}/tcp" >/dev/null 2>&1 || log "warning: failed to add port to ufw"
|
|
||||||
|
|
||||||
# Fallback for Alpine or manual iptables
|
|
||||||
else
|
|
||||||
log "no active high-level firewall manager detected (firewalld/ufw). if you use iptables/nftables directly, ensure port $port/tcp is allowed."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "firewall: intentionally left untouched — the admin dashboard port $port/tcp was NOT opened to the internet (auto-opening it would be a security risk)."
|
||||||
|
log ""
|
||||||
|
|
||||||
|
if [ "$manager" = "firewalld" ]; then
|
||||||
|
log "to finish setup you must temporarily allow port $port/tcp so you can reach the dashboard once:"
|
||||||
|
log " firewall-cmd --permanent --add-port=${port}/tcp && firewall-cmd --reload"
|
||||||
|
log ""
|
||||||
|
log "afterwards, delete the temporary rule with:"
|
||||||
|
log " firewall-cmd --permanent --remove-port=${port}/tcp && firewall-cmd --reload"
|
||||||
|
elif [ "$manager" = "ufw" ]; then
|
||||||
|
log "to finish setup you must temporarily allow port $port/tcp so you can reach the dashboard once:"
|
||||||
|
log " ufw allow ${port}/tcp"
|
||||||
|
log ""
|
||||||
|
log "afterwards, delete the temporary rule with:"
|
||||||
|
log " ufw delete allow ${port}/tcp"
|
||||||
|
else
|
||||||
|
log "no active high-level firewall manager detected (firewalld/ufw). if you use iptables/nftables directly, ensure port $port/tcp is reachable temporarily while you finish setup."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local manager_text="your host firewall (iptables/nftables)"
|
||||||
|
local auto_allow_text="allow each host subnet and its wireguard listen port manually"
|
||||||
|
if [ "$manager" = "firewalld" ] || [ "$manager" = "ufw" ]; then
|
||||||
|
manager_text="$manager"
|
||||||
|
auto_allow_text="each host setup automatically allows its wireguard subnet plus the host's listen port in $manager"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "recommended flow:"
|
||||||
|
log " 1. add the temporary allow above (keep it only for as long as needed)"
|
||||||
|
log " 2. log in and create your hosts — $auto_allow_text"
|
||||||
|
log " 3. once everything is reachable via the tunnel, remove the temporary rule"
|
||||||
|
log " and review all firewall rules under /admin/setup -> Firewall"
|
||||||
}
|
}
|
||||||
|
|
||||||
install_deps
|
install_deps
|
||||||
@@ -296,7 +322,7 @@ fi
|
|||||||
PORT="$(grab_port)"
|
PORT="$(grab_port)"
|
||||||
|
|
||||||
if [ "$DEV_MODE" = false ]; then
|
if [ "$DEV_MODE" = false ]; then
|
||||||
configure_firewall "$PORT"
|
print_firewall_instructions "$PORT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "wg-ui install complete."
|
log "wg-ui install complete."
|
||||||
|
|||||||
Reference in New Issue
Block a user