import { useState } from 'react'; import { Card, CardHeader, CardTitle, CardContent } from './ui/card'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { Alert, AlertDescription } from './ui/alert'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; import { Shield, Loader2, AlertCircle, CheckCircle, Check, X, Plus, Trash2, BookOpen, Lock } from 'lucide-react'; import { Badge } from './ui/badge'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNftables } from '../hooks/useNftables'; import { useVpn } from '../hooks/useVpn'; import { nftablesConfigApi, haproxyRoutesApi } from '../services/api/settings'; type ExtraPort = { port: number; protocol?: string; label?: string }; const WELL_KNOWN_PORTS: Record = { 22: 'SSH', 53: 'DNS', 67: 'DHCP Server', 68: 'DHCP Client', 80: 'HTTP', 443: 'HTTPS', 3389: 'RDP', 5353: 'mDNS', 5900: 'VNC', 8404: 'HAProxy Stats', }; // Ports that are always included automatically const AUTO_TCP_PORTS = [80, 443, 8404]; const AUTO_UDP_PORTS = [53, 67, 68]; interface PortEntry { port: number; protocol: string; label: string; source: 'auto' | 'haproxy-custom' | 'user'; } export function FirewallComponent() { const queryClient = useQueryClient(); const { data: nftCfg } = useQuery({ queryKey: ['nftables', 'config'], queryFn: () => nftablesConfigApi.get() }); const { data: haproxyRoutes } = useQuery({ queryKey: ['haproxy', 'routes'], queryFn: () => haproxyRoutesApi.get() }); const nftMutation = useMutation({ mutationFn: (data: Parameters[0]) => nftablesConfigApi.update(data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['nftables', 'config'] }), }); const { interfaces, refetchStatus } = useNftables(); const { config: vpnConfig } = useVpn(); const isEnabled = nftCfg?.enabled !== false; const currentWan = nftCfg?.wanInterface ?? ''; const currentExtraPorts: ExtraPort[] = nftCfg?.extraPorts ?? []; const customRoutes = haproxyRoutes?.customRoutes ?? []; const [editingWan, setEditingWan] = useState(false); const [wanValue, setWanValue] = useState(''); const [savingWan, setSavingWan] = useState(false); const [togglingFirewall, setTogglingFirewall] = useState(false); const [successMessage, setSuccessMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [newPort, setNewPort] = useState(''); const [newProtocol, setNewProtocol] = useState('tcp'); const [newLabel, setNewLabel] = useState(''); const [portError, setPortError] = useState(''); const [savingPorts, setSavingPorts] = useState(false); const showSuccess = (msg: string) => { setSuccessMessage(msg); setErrorMessage(null); setTimeout(() => setSuccessMessage(null), 5000); }; const showError = (msg: string) => { setErrorMessage(msg); setSuccessMessage(null); setTimeout(() => setErrorMessage(null), 8000); }; // Build unified port list from all sources function buildPortList(): PortEntry[] { const entries: PortEntry[] = []; const seen = new Set(); const addEntry = (port: number, protocol: string, label: string, source: PortEntry['source']) => { const key = `${port}/${protocol}`; if (!seen.has(key)) { seen.add(key); entries.push({ port, protocol, label, source }); } }; // Auto TCP ports (HAProxy base) for (const port of AUTO_TCP_PORTS) { addEntry(port, 'tcp', WELL_KNOWN_PORTS[port] || '', 'auto'); } // HAProxy custom route ports for (const route of customRoutes) { addEntry(route.port, 'tcp', `HAProxy: ${route.name}`, 'haproxy-custom'); } // Auto UDP ports (DNS/DHCP) for (const port of AUTO_UDP_PORTS) { addEntry(port, 'udp', WELL_KNOWN_PORTS[port] || '', 'auto'); } // VPN listen port (when enabled) if (vpnConfig?.enabled && vpnConfig.listenPort) { addEntry(vpnConfig.listenPort, 'udp', 'WireGuard VPN', 'auto'); } // User extra ports for (const ep of currentExtraPorts) { const proto = ep.protocol || 'tcp'; addEntry(ep.port, proto, ep.label || WELL_KNOWN_PORTS[ep.port] || '', 'user'); } return entries; } const allPorts = buildPortList(); const tcpPorts = allPorts.filter(p => p.protocol === 'tcp'); const udpPorts = allPorts.filter(p => p.protocol === 'udp'); async function saveNftConfig(patch: Partial>) { await nftMutation.mutateAsync(patch); refetchStatus(); } async function toggleFirewall() { setTogglingFirewall(true); try { await saveNftConfig({ enabled: !isEnabled }); showSuccess(`Firewall ${isEnabled ? 'disabled' : 'enabled'}`); } catch (e) { showError(`Failed to toggle firewall: ${e instanceof Error ? e.message : String(e)}`); } setTogglingFirewall(false); } async function saveWan(value: string) { setSavingWan(true); try { await saveNftConfig({ wanInterface: value || undefined }); showSuccess(value ? `WAN interface set to ${value}` : 'Strict mode disabled'); } catch (e) { showError(`Failed to save WAN interface: ${e instanceof Error ? e.message : String(e)}`); } setSavingWan(false); setEditingWan(false); } async function addPort() { const port = parseInt(newPort, 10); if (isNaN(port) || port < 1 || port > 65535) { setPortError('Enter a valid port (1–65535)'); return; } if (currentExtraPorts.some((p) => p.port === port && (p.protocol || 'tcp') === newProtocol)) { setPortError('Port/protocol already in list'); return; } setPortError(''); setSavingPorts(true); try { const entry: ExtraPort = { port, protocol: newProtocol }; if (newLabel.trim()) entry.label = newLabel.trim(); await saveNftConfig({ extraPorts: [...currentExtraPorts, entry] }); showSuccess(`Port ${port}/${newProtocol} added`); setNewPort(''); setNewLabel(''); } catch (e) { showError(`Failed to add port: ${e instanceof Error ? e.message : String(e)}`); } setSavingPorts(false); } async function removePort(port: number, protocol: string) { try { await saveNftConfig({ extraPorts: currentExtraPorts.filter( (ep) => !(ep.port === port && (ep.protocol || 'tcp') === protocol) ), }); showSuccess(`Port ${port}/${protocol} removed`); } catch (e) { showError(`Failed to remove port: ${e instanceof Error ? e.message : String(e)}`); } } function renderPortRow(entry: PortEntry) { const isAuto = entry.source !== 'user'; return (
{entry.port} {entry.protocol} {entry.label} {isAuto ? ( ) : ( )}
); } return (
{/* Header with enable/disable toggle */}

Firewall

nftables rules protecting Wild Central itself

{isEnabled && } {isEnabled ? 'Enabled' : 'Disabled'}
{/* Educational card */}

Wild Central uses nftables to protect itself. Rules are generated automatically from your HAProxy ingress configuration — the ports HAProxy listens on are always allowed. The firewall is permissive by default; set a{' '} WAN interface to enable strict filtering that drops anything not explicitly listed. Make sure to add extra ports for any services you need to reach directly (like SSH) before enabling strict mode.

{successMessage && ( {successMessage} )} {errorMessage && ( {errorMessage} )} {/* Allowed Ports */} Allowed Ports

All ports the firewall allows through. Ports managed by HAProxy and system services are locked — add your own with the form below.

{/* TCP ports */}

TCP

{tcpPorts.map(renderPortRow)}
{/* UDP ports */}

UDP

{udpPorts.map(renderPortRow)}
{/* Add port form */}

Add Port

{ setNewPort(e.target.value); setPortError(''); }} onKeyDown={(e) => e.key === 'Enter' && addPort()} placeholder="e.g. 22" className="font-mono h-8 text-sm w-24 mt-1" />
setNewLabel(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addPort()} placeholder="e.g. SSH" className="h-8 text-sm w-32 mt-1" />
{!currentExtraPorts.some((p) => p.port === 22) && ( )}
{portError &&

{portError}

}
{/* WAN Interface */} WAN Interface

Your WAN interface is the network port that faces the internet — the one your router sends traffic to. When set, the firewall drops all inbound connections on that interface except for the ports listed above. Leave unset to keep the firewall permissive (safe default while you're getting started).

{currentWan && !currentExtraPorts.some((p) => p.port === 22) && ( Strict mode is active but SSH (port 22) is not in your allowed ports. You may lose remote access if you don't have another way in. )} {!editingWan ? (
{currentWan ? ( {currentWan} ) : ( Not set — permissive mode (all inbound allowed) )} {currentWan && ( )}
) : (
{currentWan === '' && !currentExtraPorts.some((p) => p.port === 22) && ( SSH (port 22) is not in your extra ports list. Enabling strict mode will block SSH access to Wild Central. )}
{interfaces.length > 0 ? ( ) : ( setWanValue(e.target.value)} placeholder="e.g. eth0" className="font-mono h-8 text-sm w-32" /> )}

Pick the interface that faces your router/internet. Typically the one with your external IP address.

)}
); }