update firewall

This commit is contained in:
MacRimi
2026-02-08 16:19:48 +01:00
parent f07e8cfe14
commit 7f9da757aa
3 changed files with 532 additions and 78 deletions

View File

@@ -69,10 +69,24 @@ export function Security() {
cluster_fw_enabled: boolean cluster_fw_enabled: boolean
host_fw_enabled: boolean host_fw_enabled: boolean
rules_count: number rules_count: number
rules: Array<{ raw: string; direction?: string; action?: string; dport?: string; p?: string; source_file?: string; section?: string }> rules: Array<{ raw: string; direction?: string; action?: string; dport?: string; p?: string; source?: string; source_file?: string; section?: string; rule_index: number }>
monitor_port_open: boolean monitor_port_open: boolean
} | null>(null) } | null>(null)
const [firewallAction, setFirewallAction] = useState(false) const [firewallAction, setFirewallAction] = useState(false)
const [showAddRule, setShowAddRule] = useState(false)
const [newRule, setNewRule] = useState({
direction: "IN",
action: "ACCEPT",
protocol: "tcp",
dport: "",
sport: "",
source: "",
iface: "",
comment: "",
level: "host",
})
const [addingRule, setAddingRule] = useState(false)
const [deletingRuleIdx, setDeletingRuleIdx] = useState<number | null>(null)
// Security Tools state // Security Tools state
const [toolsLoading, setToolsLoading] = useState(true) const [toolsLoading, setToolsLoading] = useState(true)
@@ -235,6 +249,58 @@ export function Security() {
return `${Math.floor(s / 86400)}d` return `${Math.floor(s / 86400)}d`
} }
const handleAddRule = async () => {
if (!newRule.dport && !newRule.source) {
setError("Please specify at least a destination port or source address")
return
}
setAddingRule(true)
setError("")
setSuccess("")
try {
const data = await fetchApi("/api/security/firewall/rules", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newRule),
})
if (data.success) {
setSuccess(data.message || "Rule added successfully")
setShowAddRule(false)
setNewRule({ direction: "IN", action: "ACCEPT", protocol: "tcp", dport: "", sport: "", source: "", iface: "", comment: "", level: "host" })
loadFirewallStatus()
} else {
setError(data.message || "Failed to add rule")
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to add rule")
} finally {
setAddingRule(false)
}
}
const handleDeleteRule = async (ruleIndex: number, level: string) => {
setDeletingRuleIdx(ruleIndex)
setError("")
setSuccess("")
try {
const data = await fetchApi("/api/security/firewall/rules", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rule_index: ruleIndex, level }),
})
if (data.success) {
setSuccess(data.message || "Rule deleted")
loadFirewallStatus()
} else {
setError(data.message || "Failed to delete rule")
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete rule")
} finally {
setDeletingRuleIdx(null)
}
}
const handleFirewallToggle = async (level: "host" | "cluster", enable: boolean) => { const handleFirewallToggle = async (level: "host" | "cluster", enable: boolean) => {
setFirewallAction(true) setFirewallAction(true)
setError("") setError("")
@@ -1485,12 +1551,25 @@ export function Security() {
{/* Proxmox Firewall */} {/* Proxmox Firewall */}
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Flame className="h-5 w-5 text-orange-500" /> <Flame className="h-5 w-5 text-orange-500" />
<CardTitle>Proxmox Firewall</CardTitle> <CardTitle>Proxmox Firewall</CardTitle>
</div> </div>
{firewallData?.pve_firewall_installed && (
<Button
variant="ghost"
size="sm"
onClick={loadFirewallStatus}
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
>
<RefreshCw className="h-3 w-3 mr-1" />
Refresh
</Button>
)}
</div>
<CardDescription> <CardDescription>
Manage the Proxmox VE built-in firewall at cluster and host level Manage the Proxmox VE built-in firewall: enable/disable, configure rules, and protect your services
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@@ -1521,7 +1600,7 @@ export function Security() {
<div> <div>
<p className="font-medium text-sm">Cluster Firewall</p> <p className="font-medium text-sm">Cluster Firewall</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{firewallData.cluster_fw_enabled ? "Active" : "Disabled"} {firewallData.cluster_fw_enabled ? "Active - Required for host rules to work" : "Disabled - Must be enabled first"}
</p> </p>
</div> </div>
</div> </div>
@@ -1552,7 +1631,7 @@ export function Security() {
<div> <div>
<p className="font-medium text-sm">Host Firewall</p> <p className="font-medium text-sm">Host Firewall</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{firewallData.host_fw_enabled ? "Active" : "Disabled"} {firewallData.host_fw_enabled ? "Active - Rules are being enforced" : "Disabled"}
</p> </p>
</div> </div>
</div> </div>
@@ -1575,90 +1654,268 @@ export function Security() {
</div> </div>
</div> </div>
{/* ProxMenux Monitor Port 8008 */} {!firewallData.cluster_fw_enabled && (
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg border border-border"> <div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<div className="flex items-center gap-3"> <Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${firewallData.monitor_port_open ? "bg-green-500/10" : "bg-yellow-500/10"}`}> <p className="text-sm text-blue-500">
<Activity className={`h-5 w-5 ${firewallData.monitor_port_open ? "text-green-500" : "text-yellow-500"}`} /> The Cluster Firewall must be enabled for any host-level firewall rules to take effect. Enable it first, then configure your host rules.
</div>
<div>
<p className="font-medium text-sm">ProxMenux Monitor Port (8008/TCP)</p>
<p className="text-xs text-muted-foreground">
{firewallData.monitor_port_open
? "Port 8008 is allowed in the firewall"
: "Port 8008 is not configured in the firewall"}
</p> </p>
</div> </div>
)}
{/* Quick Presets */}
<div className="space-y-2">
<h3 className="text-sm font-semibold text-muted-foreground">Quick Access Rules</h3>
<div className="grid gap-2 sm:grid-cols-2">
{/* Monitor Port 8008 */}
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
<div className="flex items-center gap-2.5">
<div className={`w-2.5 h-2.5 rounded-full ${firewallData.monitor_port_open ? "bg-green-500" : "bg-yellow-500"}`} />
<div>
<p className="text-sm font-medium">ProxMenux Monitor</p>
<p className="text-xs text-muted-foreground">Port 8008/TCP</p>
</div>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
disabled={firewallAction} disabled={firewallAction}
onClick={() => handleMonitorPortToggle(!firewallData.monitor_port_open)} onClick={() => handleMonitorPortToggle(!firewallData.monitor_port_open)}
className={firewallData.monitor_port_open className={`h-7 text-xs ${firewallData.monitor_port_open
? "text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent" ? "text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent"
: "text-green-500 border-green-500/30 hover:bg-green-500/10 bg-transparent" : "text-green-500 border-green-500/30 hover:bg-green-500/10 bg-transparent"
} }`}
> >
{firewallData.monitor_port_open ? ( {firewallData.monitor_port_open ? "Remove" : "Allow"}
<><Minus className="h-3.5 w-3.5 mr-1" /> Remove Rule</>
) : (
<><Plus className="h-3.5 w-3.5 mr-1" /> Add Rule</>
)}
</Button> </Button>
</div> </div>
{/* Proxmox Web UI hint */}
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
<div className="flex items-center gap-2.5">
<div className="w-2.5 h-2.5 rounded-full bg-green-500" />
<div>
<p className="text-sm font-medium">Proxmox Web UI</p>
<p className="text-xs text-muted-foreground">Port 8006/TCP (always allowed)</p>
</div>
</div>
<span className="text-xs text-muted-foreground px-2 py-1 bg-muted/50 rounded">Built-in</span>
</div>
</div>
{!firewallData.monitor_port_open && (firewallData.cluster_fw_enabled || firewallData.host_fw_enabled) && ( {!firewallData.monitor_port_open && (firewallData.cluster_fw_enabled || firewallData.host_fw_enabled) && (
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-3 flex items-start gap-2"> <div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" /> <AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-yellow-500"> <p className="text-sm text-yellow-500">
The firewall is active but port 8008 is not allowed. ProxMenux Monitor may be inaccessible from other devices. Add the rule above to fix this. The firewall is active but port 8008 is not allowed. ProxMenux Monitor may be inaccessible from other devices.
</p> </p>
</div> </div>
)} )}
</div>
{/* Active Rules */} {/* Firewall Rules */}
{firewallData.rules.length > 0 && ( <div className="space-y-3">
<div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-muted-foreground"> <h3 className="text-sm font-semibold text-muted-foreground">
Active Rules ({firewallData.rules_count}) Firewall Rules ({firewallData.rules_count})
</h3> </h3>
<Button
variant="outline"
size="sm"
onClick={() => setShowAddRule(!showAddRule)}
className="h-7 text-xs text-orange-500 border-orange-500/30 hover:bg-orange-500/10 bg-transparent"
>
<Plus className="h-3 w-3 mr-1" />
Add Rule
</Button>
</div>
{/* Add Rule Form */}
{showAddRule && (
<div className="border border-orange-500/30 rounded-lg p-4 bg-orange-500/5 space-y-4">
<div className="flex items-center gap-2 mb-1">
<Plus className="h-4 w-4 text-orange-500" />
<p className="text-sm font-semibold text-orange-500">New Firewall Rule</p>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Direction</Label>
<select
value={newRule.direction}
onChange={(e) => setNewRule({...newRule, direction: e.target.value})}
className="w-full h-9 rounded-md border border-border bg-card px-3 text-sm"
>
<option value="IN">IN (incoming)</option>
<option value="OUT">OUT (outgoing)</option>
</select>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Action</Label>
<select
value={newRule.action}
onChange={(e) => setNewRule({...newRule, action: e.target.value})}
className="w-full h-9 rounded-md border border-border bg-card px-3 text-sm"
>
<option value="ACCEPT">ACCEPT (allow)</option>
<option value="DROP">DROP (block silently)</option>
<option value="REJECT">REJECT (block with response)</option>
</select>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Protocol</Label>
<select
value={newRule.protocol}
onChange={(e) => setNewRule({...newRule, protocol: e.target.value})}
className="w-full h-9 rounded-md border border-border bg-card px-3 text-sm"
>
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
<option value="icmp">ICMP (ping)</option>
</select>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Destination Port</Label>
<Input
placeholder="e.g. 80, 443, 8000:9000"
value={newRule.dport}
onChange={(e) => setNewRule({...newRule, dport: e.target.value})}
className="h-9 text-sm"
/>
<p className="text-[10px] text-muted-foreground">Single port, comma-separated, or range (8000:9000)</p>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Source Address (optional)</Label>
<Input
placeholder="e.g. 192.168.1.0/24"
value={newRule.source}
onChange={(e) => setNewRule({...newRule, source: e.target.value})}
className="h-9 text-sm"
/>
<p className="text-[10px] text-muted-foreground">IP, CIDR, or leave empty for any source</p>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Interface (optional)</Label>
<Input
placeholder="e.g. vmbr0"
value={newRule.iface}
onChange={(e) => setNewRule({...newRule, iface: e.target.value})}
className="h-9 text-sm"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Apply to</Label>
<select
value={newRule.level}
onChange={(e) => setNewRule({...newRule, level: e.target.value})}
className="w-full h-9 rounded-md border border-border bg-card px-3 text-sm"
>
<option value="host">Host firewall (this node)</option>
<option value="cluster">Cluster firewall (all nodes)</option>
</select>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">Comment (optional)</Label>
<Input
placeholder="e.g. Allow web traffic"
value={newRule.comment}
onChange={(e) => setNewRule({...newRule, comment: e.target.value})}
className="h-9 text-sm"
/>
</div>
<div className="flex gap-2 justify-end">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={loadFirewallStatus} onClick={() => setShowAddRule(false)}
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground" className="text-muted-foreground"
> >
<RefreshCw className="h-3 w-3 mr-1" /> Cancel
Refresh </Button>
<Button
size="sm"
disabled={addingRule}
onClick={handleAddRule}
className="bg-orange-600 hover:bg-orange-700 text-white"
>
{addingRule ? (
<div className="animate-spin h-3.5 w-3.5 border-2 border-white border-t-transparent rounded-full mr-1" />
) : (
<Plus className="h-3.5 w-3.5 mr-1" />
)}
Add Rule
</Button> </Button>
</div> </div>
<div className="max-h-48 overflow-y-auto space-y-1"> </div>
)}
{/* Rules List */}
{firewallData.rules.length > 0 ? (
<div className="border border-border rounded-lg overflow-hidden">
{/* Table header */}
<div className="grid grid-cols-[auto_1fr_auto_auto_auto_auto_auto] gap-2 p-2.5 bg-muted/50 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
<span className="w-14">Action</span>
<span>Direction</span>
<span className="w-12">Proto</span>
<span className="w-20">Port</span>
<span className="w-28 hidden sm:block">Source</span>
<span className="w-14">Level</span>
<span className="w-8" />
</div>
<div className="divide-y divide-border max-h-64 overflow-y-auto">
{firewallData.rules.map((rule, idx) => ( {firewallData.rules.map((rule, idx) => (
<div key={idx} className="flex items-center gap-2 p-2 bg-muted/30 rounded text-xs font-mono"> <div key={idx} className="grid grid-cols-[auto_1fr_auto_auto_auto_auto_auto] gap-2 p-2.5 items-center hover:bg-muted/20 transition-colors">
<span className={`px-1.5 py-0.5 rounded text-[10px] font-semibold ${ <span className={`w-14 px-1.5 py-0.5 rounded text-[10px] font-bold text-center ${
rule.action === "ACCEPT" ? "bg-green-500/10 text-green-500" : rule.action === "ACCEPT" ? "bg-green-500/10 text-green-500" :
rule.action === "DROP" ? "bg-red-500/10 text-red-500" : rule.action === "DROP" ? "bg-red-500/10 text-red-500" :
rule.action === "REJECT" ? "bg-orange-500/10 text-orange-500" :
"bg-gray-500/10 text-gray-500" "bg-gray-500/10 text-gray-500"
}`}> }`}>
{rule.action || "?"} {rule.action || "?"}
</span> </span>
<span className="text-muted-foreground">{rule.direction || "IN"}</span> <span className="text-xs text-muted-foreground font-mono">{rule.direction || "IN"}</span>
{rule.p && <span className="text-blue-400">{rule.p}</span>} <span className="w-12 text-xs text-blue-400 font-mono">{rule.p || "-"}</span>
{rule.dport && <span className="text-foreground">:{rule.dport}</span>} <span className="w-20 text-xs text-foreground font-mono">{rule.dport || "-"}</span>
<span className="text-muted-foreground/60 ml-auto">{rule.source_file}</span> <span className="w-28 text-xs text-muted-foreground font-mono hidden sm:block truncate">{rule.source || "any"}</span>
<span className={`w-14 text-[10px] px-1.5 py-0.5 rounded text-center ${
rule.source_file === "cluster" ? "bg-blue-500/10 text-blue-400" : "bg-purple-500/10 text-purple-400"
}`}>
{rule.source_file}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteRule(rule.rule_index, rule.source_file)}
disabled={deletingRuleIdx === rule.rule_index}
className="w-8 h-7 p-0 text-red-500/50 hover:text-red-500 hover:bg-red-500/10"
>
{deletingRuleIdx === rule.rule_index ? (
<div className="animate-spin h-3 w-3 border-2 border-red-500 border-t-transparent rounded-full" />
) : (
<Trash2 className="h-3.5 w-3.5" />
)}
</Button>
</div> </div>
))} ))}
</div> </div>
</div> </div>
) : (
<div className="text-center py-6 border border-dashed border-border rounded-lg">
<Shield className="h-8 w-8 text-muted-foreground/30 mx-auto mb-2" />
<p className="text-sm text-muted-foreground">No firewall rules configured yet</p>
<p className="text-xs text-muted-foreground/60 mt-1">Click "Add Rule" above to create your first rule</p>
</div>
)} )}
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-blue-500">
For advanced firewall configuration (IP sets, security groups, per-VM rules), use the Proxmox web interface at port 8006.
</p>
</div> </div>
</> </>
)} )}

View File

@@ -59,6 +59,53 @@ def firewall_disable():
return jsonify({"success": False, "message": str(e)}), 500 return jsonify({"success": False, "message": str(e)}), 500
@security_bp.route('/api/security/firewall/rules', methods=['POST'])
def firewall_add_rule():
"""Add a custom firewall rule"""
if not security_manager:
return jsonify({"success": False, "message": "Security manager not available"}), 500
try:
data = request.json or {}
success, message = security_manager.add_firewall_rule(
direction=data.get("direction", "IN"),
action=data.get("action", "ACCEPT"),
protocol=data.get("protocol", "tcp"),
dport=data.get("dport", ""),
sport=data.get("sport", ""),
source=data.get("source", ""),
dest=data.get("dest", ""),
iface=data.get("iface", ""),
comment=data.get("comment", ""),
level=data.get("level", "host"),
)
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@security_bp.route('/api/security/firewall/rules', methods=['DELETE'])
def firewall_delete_rule():
"""Delete a firewall rule by index"""
if not security_manager:
return jsonify({"success": False, "message": "Security manager not available"}), 500
try:
data = request.json or {}
rule_index = data.get("rule_index")
level = data.get("level", "host")
if rule_index is None:
return jsonify({"success": False, "message": "rule_index is required"}), 400
success, message = security_manager.delete_firewall_rule(int(rule_index), level)
if success:
return jsonify({"success": True, "message": message})
else:
return jsonify({"success": False, "message": message}), 400
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@security_bp.route('/api/security/firewall/monitor-port', methods=['POST']) @security_bp.route('/api/security/firewall/monitor-port', methods=['POST'])
def firewall_add_monitor_port(): def firewall_add_monitor_port():
"""Add firewall rule to allow port 8008 for ProxMenux Monitor""" """Add firewall rule to allow port 8008 for ProxMenux Monitor"""

View File

@@ -106,10 +106,12 @@ def get_firewall_status():
def _parse_firewall_rules(): def _parse_firewall_rules():
"""Parse all firewall rules from cluster and host configs""" """Parse all firewall rules from cluster and host configs"""
rules = [] rules = []
rule_idx_by_file = {} # Track rule index per file for deletion
for fw_file, source in [(CLUSTER_FW, "cluster"), (os.path.join(HOST_FW_DIR, "host.fw"), "host")]: for fw_file, source in [(CLUSTER_FW, "cluster"), (os.path.join(HOST_FW_DIR, "host.fw"), "host")]:
if not os.path.isfile(fw_file): if not os.path.isfile(fw_file):
continue continue
rule_idx_by_file[source] = 0
try: try:
with open(fw_file, 'r') as f: with open(fw_file, 'r') as f:
content = f.read() content = f.read()
@@ -132,7 +134,9 @@ def _parse_firewall_rules():
if in_rules or section in ("RULES", "IN", "OUT"): if in_rules or section in ("RULES", "IN", "OUT"):
rule = _parse_rule_line(line, source, section) rule = _parse_rule_line(line, source, section)
if rule: if rule:
rule["rule_index"] = rule_idx_by_file[source]
rules.append(rule) rules.append(rule)
rule_idx_by_file[source] += 1
except Exception: except Exception:
pass pass
@@ -181,6 +185,152 @@ def _parse_rule_line(line, source, section):
return rule return rule
def add_firewall_rule(direction="IN", action="ACCEPT", protocol="tcp", dport="", sport="",
source="", dest="", iface="", comment="", level="host"):
"""
Add a custom firewall rule to host or cluster firewall config.
Returns (success, message)
"""
# Validate inputs
action = action.upper()
if action not in ("ACCEPT", "DROP", "REJECT"):
return False, f"Invalid action: {action}. Must be ACCEPT, DROP, or REJECT"
direction = direction.upper()
if direction not in ("IN", "OUT"):
return False, f"Invalid direction: {direction}. Must be IN or OUT"
# Build rule line
parts = [direction, action]
if protocol:
parts.extend(["-p", protocol.lower()])
if dport:
# Validate port
if not re.match(r'^[\d:,]+$', dport):
return False, f"Invalid destination port: {dport}"
parts.extend(["-dport", dport])
if sport:
if not re.match(r'^[\d:,]+$', sport):
return False, f"Invalid source port: {sport}"
parts.extend(["-sport", sport])
if source:
parts.extend(["-source", source])
if dest:
parts.extend(["-dest", dest])
if iface:
parts.extend(["-i", iface])
parts.extend(["-log", "nolog"])
if comment:
# Sanitize comment
safe_comment = re.sub(r'[^\w\s\-._/():]', '', comment)
parts.append(f"# {safe_comment}")
rule_line = " ".join(parts)
# Determine target file
if level == "cluster":
fw_file = CLUSTER_FW
else:
fw_file = os.path.join(HOST_FW_DIR, "host.fw")
try:
content = ""
has_rules_section = False
if os.path.isfile(fw_file):
with open(fw_file, 'r') as f:
content = f.read()
has_rules_section = "[RULES]" in content
if has_rules_section:
lines = content.splitlines()
new_lines = []
inserted = False
for line in lines:
new_lines.append(line)
if not inserted and line.strip() == "[RULES]":
new_lines.append(rule_line)
inserted = True
content = "\n".join(new_lines) + "\n"
else:
if content and not content.endswith("\n"):
content += "\n"
content += "\n[RULES]\n"
content += rule_line + "\n"
os.makedirs(os.path.dirname(fw_file), exist_ok=True)
with open(fw_file, 'w') as f:
f.write(content)
_run_cmd(["pve-firewall", "reload"])
return True, f"Firewall rule added: {direction} {action} {protocol}{':' + dport if dport else ''}"
except PermissionError:
return False, "Permission denied. Cannot write to firewall config."
except Exception as e:
return False, f"Failed to add firewall rule: {str(e)}"
def delete_firewall_rule(rule_index, level="host"):
"""
Delete a firewall rule by index from host or cluster config.
The index corresponds to the order of rules in [RULES] section.
Returns (success, message)
"""
if level == "cluster":
fw_file = CLUSTER_FW
else:
fw_file = os.path.join(HOST_FW_DIR, "host.fw")
if not os.path.isfile(fw_file):
return False, "Firewall config file not found"
try:
with open(fw_file, 'r') as f:
content = f.read()
lines = content.splitlines()
new_lines = []
in_rules = False
current_rule_idx = 0
removed_rule = None
for line in lines:
stripped = line.strip()
if stripped.startswith('['):
section_match = re.match(r'\[(\w+)\]', stripped)
if section_match:
section = section_match.group(1).upper()
in_rules = section in ("RULES", "IN", "OUT")
if in_rules and stripped and not stripped.startswith('#') and not stripped.startswith('['):
# This is a rule line
if current_rule_idx == rule_index:
removed_rule = stripped
current_rule_idx += 1
continue # Skip this line (delete it)
current_rule_idx += 1
new_lines.append(line)
if removed_rule is None:
return False, f"Rule index {rule_index} not found"
with open(fw_file, 'w') as f:
f.write("\n".join(new_lines) + "\n")
_run_cmd(["pve-firewall", "reload"])
return True, f"Firewall rule deleted: {removed_rule}"
except PermissionError:
return False, "Permission denied. Cannot modify firewall config."
except Exception as e:
return False, f"Failed to delete rule: {str(e)}"
def add_monitor_port_rule(): def add_monitor_port_rule():
""" """
Add a firewall rule to allow port 8008 (ProxMenux Monitor) on the host. Add a firewall rule to allow port 8008 (ProxMenux Monitor) on the host.