/** * ManglePanel -- Firewall mangle rules management. * * View/add/edit/delete mangle rules (/ip/firewall/mangle), * chain selector, action types, move up/down for rule ordering. * Safe apply mode by default. */ import { useState, useCallback, useMemo } from 'react' import { Plus, Pencil, Trash2, ArrowUp, ArrowDown, Filter } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { SafetyToggle } from './SafetyToggle' import { ChangePreviewModal } from './ChangePreviewModal' import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel' import { cn } from '@/lib/utils' import type { ConfigPanelProps } from '@/lib/configPanelTypes' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- type ChainFilter = 'all' | 'prerouting' | 'input' | 'forward' | 'output' | 'postrouting' const CHAINS: ChainFilter[] = ['all', 'prerouting', 'input', 'forward', 'output', 'postrouting'] const ACTIONS = ['mark-connection', 'mark-packet', 'mark-routing', 'change-dscp', 'passthrough', 'accept', 'drop', 'jump', 'log'] interface MangleEntry { '.id': string chain: string action: string 'new-connection-mark': string 'new-packet-mark': string 'new-routing-mark': string 'src-address': string 'dst-address': string protocol: string 'dst-port': string 'src-port': string disabled: string comment: string [key: string]: string } interface MangleForm { chain: string action: string 'src-address': string 'dst-address': string protocol: string 'dst-port': string 'new-connection-mark': string 'new-packet-mark': string 'new-routing-mark': string comment: string } const EMPTY_FORM: MangleForm = { chain: 'prerouting', action: 'mark-connection', 'src-address': '', 'dst-address': '', protocol: '', 'dst-port': '', 'new-connection-mark': '', 'new-packet-mark': '', 'new-routing-mark': '', comment: '', } type PanelHook = ReturnType // --------------------------------------------------------------------------- // ManglePanel // --------------------------------------------------------------------------- export function ManglePanel({ tenantId, deviceId, active }: ConfigPanelProps) { const { entries, isLoading, error, refetch } = useConfigBrowse( tenantId, deviceId, '/ip/firewall/mangle', { enabled: active }, ) const panel = useConfigPanel(tenantId, deviceId, 'mangle') const [previewOpen, setPreviewOpen] = useState(false) const [chainFilter, setChainFilter] = useState('all') const typedEntries = entries as MangleEntry[] const filtered = useMemo(() => { if (chainFilter === 'all') return typedEntries return typedEntries.filter((e) => e.chain === chainFilter) }, [typedEntries, chainFilter]) if (isLoading) { return
Loading mangle rules...
} if (error) { return
Failed to load mangle rules.
} return (
{CHAINS.map((chain) => ( ))}
{ panel.applyChanges(); setPreviewOpen(false) }} isApplying={panel.isApplying} />
) } // --------------------------------------------------------------------------- // Mangle Table // --------------------------------------------------------------------------- function MangleTable({ entries, panel }: { entries: MangleEntry[]; panel: PanelHook }) { const [dialogOpen, setDialogOpen] = useState(false) const [editing, setEditing] = useState(null) const [form, setForm] = useState(EMPTY_FORM) const handleAdd = useCallback(() => { setEditing(null); setForm(EMPTY_FORM); setDialogOpen(true) }, []) const handleEdit = useCallback((e: MangleEntry) => { setEditing(e) setForm({ chain: e.chain || 'prerouting', action: e.action || 'mark-connection', 'src-address': e['src-address'] || '', 'dst-address': e['dst-address'] || '', protocol: e.protocol || '', 'dst-port': e['dst-port'] || '', 'new-connection-mark': e['new-connection-mark'] || '', 'new-packet-mark': e['new-packet-mark'] || '', 'new-routing-mark': e['new-routing-mark'] || '', comment: e.comment || '', }) setDialogOpen(true) }, []) const handleDelete = useCallback((e: MangleEntry) => { panel.addChange({ operation: 'remove', path: '/ip/firewall/mangle', entryId: e['.id'], properties: {}, description: `Remove mangle rule ${e.chain}/${e.action} ${e.comment || ''}`.trim() }) }, [panel]) const handleSave = useCallback(() => { if (!form.chain || !form.action) return const props: Record = { chain: form.chain, action: form.action } if (form['src-address']) props['src-address'] = form['src-address'] if (form['dst-address']) props['dst-address'] = form['dst-address'] if (form.protocol) props.protocol = form.protocol if (form['dst-port']) props['dst-port'] = form['dst-port'] if (form['new-connection-mark']) props['new-connection-mark'] = form['new-connection-mark'] if (form['new-packet-mark']) props['new-packet-mark'] = form['new-packet-mark'] if (form['new-routing-mark']) props['new-routing-mark'] = form['new-routing-mark'] if (form.comment) props.comment = form.comment if (editing) { panel.addChange({ operation: 'set', path: '/ip/firewall/mangle', entryId: editing['.id'], properties: props, description: `Edit mangle ${form.chain}/${form.action}` }) } else { panel.addChange({ operation: 'add', path: '/ip/firewall/mangle', properties: props, description: `Add mangle ${form.chain}/${form.action} ${form.comment || ''}`.trim() }) } setDialogOpen(false) }, [form, editing, panel]) return ( <>
Mangle Rules ({entries.length})
{entries.length === 0 ? (
No mangle rules found.
) : (
{entries.map((entry) => ( ))}
Chain Src Address Dst Address Protocol Action Mark Status Actions
{entry.chain} {entry['src-address'] || 'any'} {entry['dst-address'] || 'any'} {entry.protocol || 'any'} {entry.action} {entry['new-connection-mark'] || entry['new-packet-mark'] || entry['new-routing-mark'] || '—'} {entry.disabled === 'true' ? disabled : active}
)}
{editing ? 'Edit Mangle Rule' : 'Add Mangle Rule'} Configure the mangle rule properties. Changes are staged.
setForm((f) => ({ ...f, 'src-address': e.target.value }))} placeholder="any" className="h-8 text-sm font-mono" />
setForm((f) => ({ ...f, 'dst-address': e.target.value }))} placeholder="any" className="h-8 text-sm font-mono" />
setForm((f) => ({ ...f, protocol: e.target.value }))} placeholder="tcp" className="h-8 text-sm" />
setForm((f) => ({ ...f, 'dst-port': e.target.value }))} placeholder="80,443" className="h-8 text-sm font-mono" />
setForm((f) => ({ ...f, 'new-connection-mark': e.target.value }))} className="h-8 text-sm" />
setForm((f) => ({ ...f, 'new-packet-mark': e.target.value }))} className="h-8 text-sm" />
setForm((f) => ({ ...f, 'new-routing-mark': e.target.value }))} className="h-8 text-sm" />
setForm((f) => ({ ...f, comment: e.target.value }))} placeholder="optional" className="h-8 text-sm" />
) }