mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-06-09 03:23:38 +02:00
Update App version to 0.1.2
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
940
ui/src/layout/components_setting/access/FirewallSettings.tsx
Normal file
940
ui/src/layout/components_setting/access/FirewallSettings.tsx
Normal file
@@ -0,0 +1,940 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button as AntdButton, Checkbox, Input, Modal, Select } from "antd";
|
||||
import { useReactAt } from "i18n-auto-extractor/react";
|
||||
import { SettingsSectionHeader } from "@components/Settings/SettingsSectionHeader";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { SettingsItem } from "@components/Settings/SettingsView";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import AutoHeight from "@components/AutoHeight";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||
|
||||
type FirewallChain = "input" | "output" | "forward";
|
||||
type FirewallAction = "accept" | "drop" | "reject";
|
||||
|
||||
export interface FirewallConfig {
|
||||
base: {
|
||||
inputPolicy: FirewallAction;
|
||||
outputPolicy: FirewallAction;
|
||||
forwardPolicy: FirewallAction;
|
||||
};
|
||||
rules: FirewallRule[];
|
||||
portForwards: FirewallPortRule[];
|
||||
}
|
||||
|
||||
export interface FirewallRule {
|
||||
chain: FirewallChain;
|
||||
sourceIP: string;
|
||||
sourcePort?: number | null;
|
||||
protocols: string[];
|
||||
destinationIP: string;
|
||||
destinationPort?: number | null;
|
||||
action: FirewallAction;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export interface FirewallPortRule {
|
||||
chain?: "output" | "prerouting" | "prerouting_redirect";
|
||||
managed?: boolean;
|
||||
sourcePort: number;
|
||||
protocols: string[];
|
||||
destinationIP: string;
|
||||
destinationPort: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
const defaultFirewallConfig: FirewallConfig = {
|
||||
base: { inputPolicy: "accept", outputPolicy: "accept", forwardPolicy: "accept" },
|
||||
rules: [],
|
||||
portForwards: [],
|
||||
};
|
||||
|
||||
const actionOptions: { value: FirewallAction; label: string }[] = [
|
||||
{ value: "accept", label: "Accept" },
|
||||
{ value: "drop", label: "Drop" },
|
||||
{ value: "reject", label: "Reject" },
|
||||
];
|
||||
|
||||
const chainOptions: { value: FirewallChain; label: string }[] = [
|
||||
{ value: "input", label: "Input" },
|
||||
{ value: "output", label: "Output" },
|
||||
{ value: "forward", label: "Forward" },
|
||||
];
|
||||
|
||||
const commProtocolOptions = [
|
||||
{ key: "any", label: "Any" },
|
||||
{ key: "tcp", label: "TCP" },
|
||||
{ key: "udp", label: "UDP" },
|
||||
{ key: "icmp", label: "ICMP" },
|
||||
{ key: "igmp", label: "IGMP" },
|
||||
];
|
||||
|
||||
const portForwardProtocolOptions = [
|
||||
{ key: "tcp", label: "TCP" },
|
||||
{ key: "udp", label: "UDP" },
|
||||
{ key: "sctp", label: "SCTP" },
|
||||
{ key: "dccp", label: "DCCP" },
|
||||
];
|
||||
|
||||
function formatProtocols(protocols: string[]) {
|
||||
if (!protocols?.length) return "-";
|
||||
if (protocols.includes("any")) return "Any";
|
||||
return protocols.map(p => p.toUpperCase()).join(", ");
|
||||
}
|
||||
|
||||
function actionLabel(action: FirewallAction) {
|
||||
return actionOptions.find(o => o.value === action)?.label ?? action;
|
||||
}
|
||||
|
||||
function chainLabel(chain: FirewallChain) {
|
||||
return chainOptions.find(o => o.value === chain)?.label ?? chain;
|
||||
}
|
||||
|
||||
function portForwardChainLabel(chain: FirewallPortRule["chain"]) {
|
||||
switch (chain ?? "prerouting") {
|
||||
case "output":
|
||||
return "OUTPUT";
|
||||
case "prerouting_redirect":
|
||||
return "PREROUTING_REDIRECT";
|
||||
default:
|
||||
return "PREROUTING";
|
||||
}
|
||||
}
|
||||
|
||||
function formatEndpoint(ip: string, port: number | null | undefined, anyText: string) {
|
||||
const t = (ip || "").trim();
|
||||
if (!t && (port === null || port === undefined)) return anyText;
|
||||
if (!t && port !== null && port !== undefined) return `${anyText}:${port}`;
|
||||
if (t && (port === null || port === undefined)) return t;
|
||||
return `${t}:${port}`;
|
||||
}
|
||||
|
||||
function normalizePort(v: string) {
|
||||
const t = v.trim();
|
||||
if (t === "") return null;
|
||||
const n = Number(t);
|
||||
if (!Number.isFinite(n)) return null;
|
||||
if (n < 1 || n > 65535) return null;
|
||||
return Math.trunc(n);
|
||||
}
|
||||
|
||||
function normalizeRuleProtocols(list: string[]) {
|
||||
if (list.includes("any")) return ["any"];
|
||||
return list;
|
||||
}
|
||||
|
||||
export default function FirewallSettings() {
|
||||
const { $at } = useReactAt();
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"base" | "rules" | "portForwards">(
|
||||
"base",
|
||||
);
|
||||
const [appliedConfig, setAppliedConfig] = useState<FirewallConfig>(defaultFirewallConfig);
|
||||
const [baseDraft, setBaseDraft] = useState<FirewallConfig["base"]>(
|
||||
defaultFirewallConfig.base,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [showBaseSubmitConfirm, setShowBaseSubmitConfirm] = useState(false);
|
||||
|
||||
const [selectedRuleRows, setSelectedRuleRows] = useState<Set<number>>(new Set());
|
||||
const [selectedPortForwardRows, setSelectedPortForwardRows] = useState<Set<number>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const [ruleModalOpen, setRuleModalOpen] = useState(false);
|
||||
const [ruleEditingIndex, setRuleEditingIndex] = useState<number | null>(null);
|
||||
const [ruleDraft, setRuleDraft] = useState<FirewallRule>({
|
||||
chain: "input",
|
||||
sourceIP: "",
|
||||
sourcePort: null,
|
||||
protocols: ["any"],
|
||||
destinationIP: "",
|
||||
destinationPort: null,
|
||||
action: "accept",
|
||||
comment: "",
|
||||
});
|
||||
const [ruleSourcePortText, setRuleSourcePortText] = useState<string>("");
|
||||
const [ruleDestinationPortText, setRuleDestinationPortText] = useState<string>("");
|
||||
|
||||
const [pfModalOpen, setPfModalOpen] = useState(false);
|
||||
const [pfEditingIndex, setPfEditingIndex] = useState<number | null>(null);
|
||||
const [pfDraft, setPfDraft] = useState<FirewallPortRule>({
|
||||
chain: "prerouting",
|
||||
sourcePort: 1,
|
||||
protocols: ["tcp"],
|
||||
destinationIP: "",
|
||||
destinationPort: 1,
|
||||
comment: "",
|
||||
});
|
||||
const [pfSourcePortText, setPfSourcePortText] = useState<string>("1");
|
||||
const [pfDestinationPortText, setPfDestinationPortText] = useState<string>("1");
|
||||
|
||||
const fetchConfig = useCallback(() => {
|
||||
setLoading(true);
|
||||
send("getFirewallConfig", {}, resp => {
|
||||
setLoading(false);
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`${$at("Failed to get firewall config")}: ${resp.error.data || resp.error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const cfg = resp.result as FirewallConfig;
|
||||
setAppliedConfig(cfg);
|
||||
setBaseDraft(cfg.base);
|
||||
setSelectedRuleRows(new Set());
|
||||
setSelectedPortForwardRows(new Set());
|
||||
});
|
||||
}, [send, $at]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, [fetchConfig]);
|
||||
|
||||
const hasBaseChanges = useMemo(() => {
|
||||
return JSON.stringify(appliedConfig.base) !== JSON.stringify(baseDraft);
|
||||
}, [appliedConfig.base, baseDraft]);
|
||||
|
||||
const applyFirewallConfig = useCallback(
|
||||
(
|
||||
nextConfig: FirewallConfig,
|
||||
opts?: { onSuccess?: () => void; successText?: string },
|
||||
) => {
|
||||
setApplying(true);
|
||||
send("setFirewallConfig", { config: nextConfig }, resp => {
|
||||
setApplying(false);
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`${$at("Failed to apply firewall config")}: ${resp.error.data || resp.error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setAppliedConfig(nextConfig);
|
||||
if (opts?.successText) notifications.success(opts.successText);
|
||||
if (opts?.onSuccess) opts.onSuccess();
|
||||
});
|
||||
},
|
||||
[send, $at],
|
||||
);
|
||||
|
||||
const handleBaseSubmit = useCallback(() => {
|
||||
const nextConfig: FirewallConfig = { ...appliedConfig, base: baseDraft };
|
||||
applyFirewallConfig(nextConfig, {
|
||||
successText: $at("Firewall config applied"),
|
||||
onSuccess: () => {
|
||||
setShowBaseSubmitConfirm(false);
|
||||
},
|
||||
});
|
||||
}, [appliedConfig, baseDraft, applyFirewallConfig, $at]);
|
||||
|
||||
const requestBaseSubmit = useCallback(() => {
|
||||
if (!hasBaseChanges) return;
|
||||
setShowBaseSubmitConfirm(true);
|
||||
}, [hasBaseChanges]);
|
||||
|
||||
const openAddRule = () => {
|
||||
setRuleEditingIndex(null);
|
||||
setRuleDraft({
|
||||
chain: "input",
|
||||
sourceIP: "",
|
||||
sourcePort: null,
|
||||
protocols: ["any"],
|
||||
destinationIP: "",
|
||||
destinationPort: null,
|
||||
action: "accept",
|
||||
comment: "",
|
||||
});
|
||||
setRuleSourcePortText("");
|
||||
setRuleDestinationPortText("");
|
||||
setRuleModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditRule = (idx: number) => {
|
||||
const current = appliedConfig.rules[idx];
|
||||
if (!current) return;
|
||||
setRuleEditingIndex(idx);
|
||||
setRuleDraft({ ...current });
|
||||
setRuleSourcePortText(current.sourcePort ? String(current.sourcePort) : "");
|
||||
setRuleDestinationPortText(current.destinationPort ? String(current.destinationPort) : "");
|
||||
setRuleModalOpen(true);
|
||||
};
|
||||
|
||||
const saveRuleDraft = () => {
|
||||
const next: FirewallRule = {
|
||||
...ruleDraft,
|
||||
protocols: normalizeRuleProtocols(ruleDraft.protocols),
|
||||
sourcePort: normalizePort(ruleSourcePortText),
|
||||
destinationPort: normalizePort(ruleDestinationPortText),
|
||||
sourceIP: ruleDraft.sourceIP.trim(),
|
||||
destinationIP: ruleDraft.destinationIP.trim(),
|
||||
comment: ruleDraft.comment.trim(),
|
||||
};
|
||||
|
||||
if (!next.protocols.length) {
|
||||
notifications.error($at("Please select protocol"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!next.chain || !next.action) {
|
||||
notifications.error($at("Missing required fields"));
|
||||
return;
|
||||
}
|
||||
|
||||
const rules = [...appliedConfig.rules];
|
||||
if (ruleEditingIndex === null) {
|
||||
rules.push(next);
|
||||
} else {
|
||||
rules[ruleEditingIndex] = next;
|
||||
}
|
||||
const nextConfig: FirewallConfig = {
|
||||
...appliedConfig,
|
||||
rules,
|
||||
};
|
||||
applyFirewallConfig(nextConfig, {
|
||||
successText: $at("Firewall config applied"),
|
||||
onSuccess: () => {
|
||||
setRuleModalOpen(false);
|
||||
setSelectedRuleRows(new Set());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSelectedRules = () => {
|
||||
const idxs = [...selectedRuleRows.values()].sort((a, b) => a - b);
|
||||
if (!idxs.length) return;
|
||||
const nextRules = appliedConfig.rules.filter((_, i) => !selectedRuleRows.has(i));
|
||||
const nextConfig: FirewallConfig = {
|
||||
...appliedConfig,
|
||||
rules: nextRules,
|
||||
};
|
||||
applyFirewallConfig(nextConfig, {
|
||||
successText: $at("Firewall config applied"),
|
||||
onSuccess: () => {
|
||||
setSelectedRuleRows(new Set());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const openAddPortForward = () => {
|
||||
setPfEditingIndex(null);
|
||||
setPfDraft({
|
||||
chain: "prerouting",
|
||||
sourcePort: 1,
|
||||
protocols: ["tcp"],
|
||||
destinationIP: "",
|
||||
destinationPort: 1,
|
||||
comment: "",
|
||||
});
|
||||
setPfSourcePortText("1");
|
||||
setPfDestinationPortText("1");
|
||||
setPfModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditPortForward = (idx: number) => {
|
||||
const current = appliedConfig.portForwards[idx];
|
||||
if (!current) return;
|
||||
if (current.managed === false) return;
|
||||
setPfEditingIndex(idx);
|
||||
const inferredChain =
|
||||
current.chain ??
|
||||
(current.destinationIP?.trim() === "0.0.0.0" || current.destinationIP?.trim() === "127.0.0.1"
|
||||
? "output"
|
||||
: "prerouting");
|
||||
setPfDraft({
|
||||
...current,
|
||||
chain: inferredChain,
|
||||
destinationIP:
|
||||
inferredChain === "output" || inferredChain === "prerouting_redirect"
|
||||
? "0.0.0.0"
|
||||
: current.destinationIP?.trim() === "0.0.0.0" || current.destinationIP?.trim() === "127.0.0.1"
|
||||
? ""
|
||||
: current.destinationIP,
|
||||
});
|
||||
setPfSourcePortText(String(current.sourcePort));
|
||||
setPfDestinationPortText(String(current.destinationPort));
|
||||
setPfModalOpen(true);
|
||||
};
|
||||
|
||||
const savePortForwardDraft = () => {
|
||||
const srcPort = normalizePort(pfSourcePortText);
|
||||
const dstPort = normalizePort(pfDestinationPortText);
|
||||
if (!srcPort || !dstPort) {
|
||||
notifications.error($at("Invalid port"));
|
||||
return;
|
||||
}
|
||||
const next: FirewallPortRule = {
|
||||
...pfDraft,
|
||||
sourcePort: srcPort,
|
||||
destinationPort: dstPort,
|
||||
destinationIP:
|
||||
(pfDraft.chain ?? "prerouting") === "output" ||
|
||||
(pfDraft.chain ?? "prerouting") === "prerouting_redirect"
|
||||
? "0.0.0.0"
|
||||
: pfDraft.destinationIP.trim(),
|
||||
protocols: normalizeRuleProtocols(pfDraft.protocols).filter(p => p !== "any"),
|
||||
comment: pfDraft.comment.trim(),
|
||||
};
|
||||
const pfChain = next.chain ?? "prerouting";
|
||||
if (pfChain === "prerouting" && ["0.0.0.0", "127.0.0.1"].includes(next.destinationIP.trim())) {
|
||||
notifications.error($at("For PREROUTING, Destination IP must be a real host IP"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pfChain === "prerouting" && !next.destinationIP) {
|
||||
notifications.error($at("Destination IP is required"));
|
||||
return;
|
||||
}
|
||||
if (!next.protocols.length) {
|
||||
notifications.error($at("Please select protocol"));
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [...appliedConfig.portForwards];
|
||||
if (pfEditingIndex === null) {
|
||||
items.push(next);
|
||||
} else {
|
||||
items[pfEditingIndex] = next;
|
||||
}
|
||||
const nextConfig: FirewallConfig = {
|
||||
...appliedConfig,
|
||||
portForwards: items,
|
||||
};
|
||||
applyFirewallConfig(nextConfig, {
|
||||
successText: $at("Firewall config applied"),
|
||||
onSuccess: () => {
|
||||
setPfModalOpen(false);
|
||||
setSelectedPortForwardRows(new Set());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSelectedPortForwards = () => {
|
||||
const idxs = [...selectedPortForwardRows.values()].sort((a, b) => a - b);
|
||||
if (!idxs.length) return;
|
||||
const nextItems = appliedConfig.portForwards.filter((r, i) => {
|
||||
if (!selectedPortForwardRows.has(i)) return true;
|
||||
return r.managed === false;
|
||||
});
|
||||
const nextConfig: FirewallConfig = {
|
||||
...appliedConfig,
|
||||
portForwards: nextItems,
|
||||
};
|
||||
applyFirewallConfig(nextConfig, {
|
||||
successText: $at("Firewall config applied"),
|
||||
onSuccess: () => {
|
||||
setSelectedPortForwardRows(new Set());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<SettingsItem
|
||||
title={$at("Firewall")}
|
||||
badge="Experimental"
|
||||
description={$at("Manage the firewall rules of the device")}
|
||||
>
|
||||
</SettingsItem>
|
||||
<div className="overflow-x-auto pb-2">
|
||||
<div className="flex min-w-max">
|
||||
{[
|
||||
{ id: "base", label: $at("Basic") },
|
||||
{ id: "rules", label: $at("Communication Rules") },
|
||||
{ id: "portForwards", label: $at("Port Forwarding") },
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
||||
className={`
|
||||
flex-1 min-w-[120px] px-6 py-3 text-sm font-medium transition-all duration-200 border-y border-r first:border-l first:rounded-l-lg last:rounded-r-lg flex items-center justify-center gap-2
|
||||
${
|
||||
activeTab === tab.id
|
||||
? "!bg-[rgba(22,152,217,1)] dark:!bg-[rgba(45,106,229,1))] !text-white border-[rgba(22,152,217,1)] dark:border-[rgba(45,106,229,1)]"
|
||||
: "bg-transparent text-slate-600 dark:text-slate-400 border-slate-200 dark:border-slate-700 hover:border-[rgba(22,152,217,1)] dark:hover:border-[rgba(45,106,229,1)] hover:text-[rgba(22,152,217,1)] dark:hover:text-[rgba(45,106,229,1)]"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{activeTab === "base" && (
|
||||
<AutoHeight>
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-[64px_1fr] gap-y-3 items-center">
|
||||
<div className="text-sm text-slate-700 dark:text-slate-300">
|
||||
{$at("Input")}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Select
|
||||
className={isMobile ? "!w-full !h-[36px]" : "!w-[28%] !h-[36px]"}
|
||||
value={baseDraft.inputPolicy}
|
||||
onChange={v =>
|
||||
setBaseDraft({
|
||||
...baseDraft,
|
||||
inputPolicy: v as FirewallAction,
|
||||
})
|
||||
}
|
||||
options={actionOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-slate-700 dark:text-slate-300">
|
||||
{$at("Output")}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Select
|
||||
className={isMobile ? "!w-full !h-[36px]" : "!w-[28%] !h-[36px]"}
|
||||
value={baseDraft.outputPolicy}
|
||||
onChange={v =>
|
||||
setBaseDraft({
|
||||
...baseDraft,
|
||||
outputPolicy: v as FirewallAction,
|
||||
})
|
||||
}
|
||||
options={actionOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-slate-700 dark:text-slate-300">
|
||||
{$at("Forward")}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Select
|
||||
className={isMobile ? "!w-full !h-[36px]" : "!w-[28%] !h-[36px]"}
|
||||
value={baseDraft.forwardPolicy}
|
||||
onChange={v =>
|
||||
setBaseDraft({
|
||||
...baseDraft,
|
||||
forwardPolicy: v as FirewallAction,
|
||||
})
|
||||
}
|
||||
options={actionOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AntdButton onClick={fetchConfig} loading={loading}>
|
||||
{$at("Refresh")}
|
||||
</AntdButton>
|
||||
<AntdButton
|
||||
type="primary"
|
||||
onClick={requestBaseSubmit}
|
||||
loading={applying}
|
||||
disabled={!hasBaseChanges}
|
||||
>
|
||||
{$at("Submit")}
|
||||
</AntdButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
</AutoHeight>
|
||||
)}
|
||||
|
||||
{activeTab === "rules" && (
|
||||
<AutoHeight>
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AntdButton
|
||||
type="primary"
|
||||
onClick={openAddRule}
|
||||
>
|
||||
{$at("Add")}
|
||||
</AntdButton>
|
||||
<AntdButton danger onClick={deleteSelectedRules} disabled={!selectedRuleRows.size}>
|
||||
{$at("Delete")}
|
||||
</AntdButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded border border-slate-200 dark:border-slate-700">
|
||||
<table className="min-w-max w-full text-sm whitespace-nowrap">
|
||||
<thead className="bg-slate-50 text-xs text-slate-700 dark:bg-slate-900/40 dark:text-slate-300">
|
||||
<tr>
|
||||
<th className="w-10 p-2 text-center font-medium" />
|
||||
<th className="p-2 text-center font-medium">{$at("Chain")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Source")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Protocol")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Destination")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Action")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Description")}</th>
|
||||
<th className="w-20 p-2 text-center font-medium">{$at("Operation")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{appliedConfig.rules.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="p-6 text-center text-slate-500 dark:text-slate-400">
|
||||
{$at("No rules available")}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
appliedConfig.rules.map((r, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
className="border-t border-slate-200 dark:border-slate-700"
|
||||
>
|
||||
<td className="p-2 text-center">
|
||||
<Checkbox
|
||||
checked={selectedRuleRows.has(idx)}
|
||||
onChange={e => {
|
||||
const next = new Set(selectedRuleRows);
|
||||
if (e.target.checked) next.add(idx);
|
||||
else next.delete(idx);
|
||||
setSelectedRuleRows(next);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2 text-center">{chainLabel(r.chain)}</td>
|
||||
<td className="p-2 text-center">
|
||||
{formatEndpoint(r.sourceIP, r.sourcePort, $at("Any"))}
|
||||
</td>
|
||||
<td className="p-2 text-center">{formatProtocols(r.protocols)}</td>
|
||||
<td className="p-2 text-center">
|
||||
{formatEndpoint(r.destinationIP, r.destinationPort, $at("Any"))}
|
||||
</td>
|
||||
<td className="p-2 text-center">{actionLabel(r.action)}</td>
|
||||
<td className="p-2 text-center">{r.comment || "-"}</td>
|
||||
<td className="p-2 text-center">
|
||||
<AntdButton size="small" onClick={() => openEditRule(idx)}>
|
||||
{$at("Edit")}
|
||||
</AntdButton>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
</AutoHeight>
|
||||
)}
|
||||
|
||||
{activeTab === "portForwards" && (
|
||||
<AutoHeight>
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AntdButton
|
||||
type="primary"
|
||||
onClick={openAddPortForward}
|
||||
>
|
||||
{$at("Add")}
|
||||
</AntdButton>
|
||||
<AntdButton
|
||||
danger
|
||||
onClick={deleteSelectedPortForwards}
|
||||
disabled={!selectedPortForwardRows.size}
|
||||
>
|
||||
{$at("Delete")}
|
||||
</AntdButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded border border-slate-200 dark:border-slate-700">
|
||||
<table className="min-w-max w-full text-sm whitespace-nowrap">
|
||||
<thead className="bg-slate-50 text-xs text-slate-700 dark:bg-slate-900/40 dark:text-slate-300">
|
||||
<tr>
|
||||
<th className="w-10 p-2 text-center font-medium" />
|
||||
<th className="p-2 text-center font-medium">{$at("Chain")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Source")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Protocol")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Destination")}</th>
|
||||
<th className="p-2 text-center font-medium">{$at("Description")}</th>
|
||||
<th className="w-20 p-2 text-center font-medium">{$at("Operation")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{appliedConfig.portForwards.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-6 text-center text-slate-500 dark:text-slate-400">
|
||||
{$at("No data available")}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
appliedConfig.portForwards.map((r, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
className="border-t border-slate-200 dark:border-slate-700"
|
||||
>
|
||||
<td className="p-2 text-center">
|
||||
<Checkbox
|
||||
checked={selectedPortForwardRows.has(idx)}
|
||||
disabled={r.managed === false}
|
||||
onChange={e => {
|
||||
const next = new Set(selectedPortForwardRows);
|
||||
if (e.target.checked) next.add(idx);
|
||||
else next.delete(idx);
|
||||
setSelectedPortForwardRows(next);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2 text-center">{portForwardChainLabel(r.chain)}</td>
|
||||
<td className="p-2 text-center">
|
||||
{formatEndpoint("", r.sourcePort, $at("Any"))}
|
||||
</td>
|
||||
<td className="p-2 text-center">{formatProtocols(r.protocols)}</td>
|
||||
<td className="p-2 text-center">
|
||||
{formatEndpoint(r.destinationIP, r.destinationPort, $at("Any"))}
|
||||
</td>
|
||||
<td className="p-2 text-center">{r.comment || "-"}</td>
|
||||
<td className="p-2 text-center">
|
||||
<AntdButton size="small" disabled={r.managed === false} onClick={() => openEditPortForward(idx)}>
|
||||
{$at("Edit")}
|
||||
</AntdButton>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
</AutoHeight>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={ruleModalOpen}
|
||||
onCancel={() => setRuleModalOpen(false)}
|
||||
onOk={saveRuleDraft}
|
||||
confirmLoading={applying}
|
||||
title={$at(ruleEditingIndex === null ? "Add Rule" : "Edit Rule")}
|
||||
okText={$at("OK")}
|
||||
cancelText={$at("Cancel")}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Chain")}</div>
|
||||
<Select
|
||||
className="!w-full"
|
||||
value={ruleDraft.chain}
|
||||
onChange={v => setRuleDraft({ ...ruleDraft, chain: v as FirewallChain })}
|
||||
options={chainOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Source IP")}</div>
|
||||
<Input
|
||||
value={ruleDraft.sourceIP}
|
||||
placeholder={$at("Any")}
|
||||
onChange={e => setRuleDraft({ ...ruleDraft, sourceIP: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Source Port")}</div>
|
||||
<Input
|
||||
value={ruleSourcePortText}
|
||||
placeholder={$at("Any")}
|
||||
onChange={e => setRuleSourcePortText(e.target.value)}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Protocol")}</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{commProtocolOptions.map(p => (
|
||||
<Checkbox
|
||||
key={p.key}
|
||||
checked={ruleDraft.protocols.includes(p.key)}
|
||||
onChange={e => {
|
||||
const checked = e.target.checked;
|
||||
const next = new Set(ruleDraft.protocols);
|
||||
if (checked) next.add(p.key);
|
||||
else next.delete(p.key);
|
||||
const arr = [...next.values()];
|
||||
setRuleDraft({ ...ruleDraft, protocols: normalizeRuleProtocols(arr) });
|
||||
}}
|
||||
>
|
||||
{p.label}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Destination IP")}</div>
|
||||
<Input
|
||||
value={ruleDraft.destinationIP}
|
||||
placeholder={$at("Any")}
|
||||
onChange={e => setRuleDraft({ ...ruleDraft, destinationIP: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Destination Port")}</div>
|
||||
<Input
|
||||
value={ruleDestinationPortText}
|
||||
placeholder={$at("Any")}
|
||||
onChange={e => setRuleDestinationPortText(e.target.value)}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Action")}</div>
|
||||
<Select
|
||||
className="!w-full"
|
||||
value={ruleDraft.action}
|
||||
onChange={v => setRuleDraft({ ...ruleDraft, action: v as FirewallAction })}
|
||||
options={actionOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Description")}</div>
|
||||
<Input
|
||||
value={ruleDraft.comment}
|
||||
onChange={e => setRuleDraft({ ...ruleDraft, comment: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={pfModalOpen}
|
||||
onCancel={() => setPfModalOpen(false)}
|
||||
onOk={savePortForwardDraft}
|
||||
confirmLoading={applying}
|
||||
title={$at(pfEditingIndex === null ? "Add Rule" : "Edit Rule")}
|
||||
okText={$at("OK")}
|
||||
cancelText={$at("Cancel")}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">
|
||||
<span className="text-red-500">*</span> {$at("Chain")}
|
||||
</div>
|
||||
<Select
|
||||
className="!w-full"
|
||||
value={pfDraft.chain ?? "prerouting"}
|
||||
onChange={v => {
|
||||
const c = v as "output" | "prerouting" | "prerouting_redirect";
|
||||
const curDst = pfDraft.destinationIP.trim();
|
||||
const forceLocal = c === "output" || c === "prerouting_redirect";
|
||||
setPfDraft({
|
||||
...pfDraft,
|
||||
chain: c,
|
||||
destinationIP: forceLocal
|
||||
? "0.0.0.0"
|
||||
: curDst === "0.0.0.0" || curDst === "127.0.0.1"
|
||||
? ""
|
||||
: pfDraft.destinationIP,
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ value: "prerouting", label: "PREROUTING" },
|
||||
{ value: "prerouting_redirect", label: "PREROUTING_REDIRECT" },
|
||||
{ value: "output", label: "OUTPUT" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">
|
||||
<span className="text-red-500">*</span> {$at("Source Port")}
|
||||
</div>
|
||||
<Input
|
||||
value={pfSourcePortText}
|
||||
onChange={e => setPfSourcePortText(e.target.value)}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Protocol")}</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{portForwardProtocolOptions.map(p => (
|
||||
<Checkbox
|
||||
key={p.key}
|
||||
checked={pfDraft.protocols.includes(p.key)}
|
||||
onChange={e => {
|
||||
const checked = e.target.checked;
|
||||
const next = new Set(pfDraft.protocols);
|
||||
if (checked) next.add(p.key);
|
||||
else next.delete(p.key);
|
||||
setPfDraft({ ...pfDraft, protocols: [...next.values()] });
|
||||
}}
|
||||
>
|
||||
{p.label}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">
|
||||
<span className="text-red-500">*</span> {$at("Destination IP")}
|
||||
</div>
|
||||
<Input
|
||||
value={pfDraft.destinationIP}
|
||||
onChange={e => setPfDraft({ ...pfDraft, destinationIP: e.target.value })}
|
||||
disabled={(pfDraft.chain ?? "prerouting") !== "prerouting"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">
|
||||
<span className="text-red-500">*</span> {$at("Destination Port")}
|
||||
</div>
|
||||
<Input
|
||||
value={pfDestinationPortText}
|
||||
onChange={e => setPfDestinationPortText(e.target.value)}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">{$at("Description")}</div>
|
||||
<Input
|
||||
value={pfDraft.comment}
|
||||
onChange={e => setPfDraft({ ...pfDraft, comment: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showBaseSubmitConfirm}
|
||||
onClose={() => {
|
||||
setShowBaseSubmitConfirm(false);
|
||||
}}
|
||||
title={$at("Submit Firewall Policies?")}
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
{$at(
|
||||
"Warning: Adjusting some policies may cause network address loss, leading to device unavailability.",
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
variant="warning"
|
||||
cancelText={$at("Cancel")}
|
||||
confirmText={$at("Submit")}
|
||||
onConfirm={handleBaseSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user