import { useState, useEffect } from 'react'; import { Plus, Trash2, Wifi, Shield, Activity, Settings, Terminal, ArrowRightLeft, RefreshCw, Info } from 'lucide-react'; import { routingAPI } from '../services/api'; const EMPTY_NAT = { source_network: '', target_interface: 'eth0', masquerade: true, nat_type: 'MASQUERADE', protocol: 'ALL', external_port: '', internal_ip: '', internal_port: '' }; const EMPTY_PF = { name: '', protocol: 'TCP', external_port: '', internal_ip: '', internal_port: '' }; const EMPTY_PEER = { peer_name: '', peer_ip: '', allowed_networks: '', route_type: 'lan' }; const EMPTY_FW = { rule_type: 'FORWARD', source: '', destination: '', action: 'ACCEPT', protocol: 'ALL', port_range: '' }; function Badge({ ok, children }) { return ( {children} ); } function SectionHeader({ title, action }) { return (

{title}

{action}
); } function EmptyRow({ cols, message }) { return ( {message} ); } function Routing() { const [activeTab, setActiveTab] = useState('overview'); const [routingStatus, setRoutingStatus] = useState(null); const [statusLoading, setStatusLoading] = useState(true); // NAT const [natRules, setNatRules] = useState([]); const [natLoading, setNatLoading] = useState(false); const [showNatForm, setShowNatForm] = useState(false); const [newNat, setNewNat] = useState(EMPTY_NAT); const [natError, setNatError] = useState(null); // Port Forwarding (DNAT subset) const [pfRules, setPfRules] = useState([]); const [showPfForm, setShowPfForm] = useState(false); const [newPf, setNewPf] = useState(EMPTY_PF); const [pfError, setPfError] = useState(null); const [pfSubmitting, setPfSubmitting] = useState(false); // Peer Routes const [peerRoutes, setPeerRoutes] = useState([]); const [peersLoading, setPeersLoading] = useState(false); const [showPeerForm, setShowPeerForm] = useState(false); const [newPeerRoute, setNewPeerRoute] = useState(EMPTY_PEER); const [peersError, setPeersError] = useState(null); // Firewall const [firewallRules, setFirewallRules] = useState([]); const [fwLoading, setFwLoading] = useState(false); const [showFwForm, setShowFwForm] = useState(false); const [newFwRule, setNewFwRule] = useState(EMPTY_FW); const [fwError, setFwError] = useState(null); // Live iptables const [liveIptables, setLiveIptables] = useState(null); const [iptLoading, setIptLoading] = useState(false); // Diagnostics const [diagTarget, setDiagTarget] = useState(''); const [diagResult, setDiagResult] = useState(null); const [diagRunning, setDiagRunning] = useState(false); useEffect(() => { fetchStatus(); fetchNatRules(); fetchPeerRoutes(); fetchFirewallRules(); }, []); // ── Fetch helpers ────────────────────────────────────────────────────────── const fetchStatus = async () => { try { const r = await routingAPI.getStatus(); setRoutingStatus(r.data); } catch {} finally { setStatusLoading(false); } }; const fetchNatRules = async () => { setNatLoading(true); try { const r = await routingAPI.getNatRules(); const all = r.data.nat_rules || []; setNatRules(all.filter(x => x.nat_type !== 'DNAT')); setPfRules(all.filter(x => x.nat_type === 'DNAT')); } catch { setNatError('Failed to load NAT rules'); } finally { setNatLoading(false); } }; const fetchPeerRoutes = async () => { setPeersLoading(true); try { const r = await routingAPI.getPeerRoutes(); setPeerRoutes(r.data.peer_routes || []); } catch { setPeersError('Failed to load peer routes'); } finally { setPeersLoading(false); } }; const fetchFirewallRules = async () => { setFwLoading(true); try { const r = await routingAPI.getFirewallRules(); setFirewallRules(r.data.firewall_rules || []); } catch { setFwError('Failed to load firewall rules'); } finally { setFwLoading(false); } }; const fetchLiveIptables = async () => { setIptLoading(true); try { const r = await routingAPI.getLiveIptables(); setLiveIptables(r.data); } catch { setLiveIptables({ error: 'Failed to load' }); } finally { setIptLoading(false); } }; // ── NAT handlers ────────────────────────────────────────────────────────── const handleAddNat = async (e) => { e.preventDefault(); setNatError(null); try { await routingAPI.addNatRule(newNat); setShowNatForm(false); setNewNat(EMPTY_NAT); fetchNatRules(); fetchStatus(); } catch { setNatError('Failed to add NAT rule'); } }; const handleDeleteNat = async (id) => { if (!window.confirm('Delete this NAT rule?')) return; try { await routingAPI.deleteNatRule(id); fetchNatRules(); fetchStatus(); } catch { setNatError('Failed to delete NAT rule'); } }; // ── Port Forwarding handlers ─────────────────────────────────────────────── const handleAddPf = async (e) => { e.preventDefault(); setPfError(null); setPfSubmitting(true); try { await routingAPI.addNatRule({ source_network: '0.0.0.0/0', target_interface: 'eth0', masquerade: false, nat_type: 'DNAT', protocol: newPf.protocol, external_port: newPf.external_port, internal_ip: newPf.internal_ip, internal_port: newPf.internal_port, }); setShowPfForm(false); setNewPf(EMPTY_PF); fetchNatRules(); } catch { setPfError('Failed to add port forwarding rule'); } finally { setPfSubmitting(false); } }; const handleDeletePf = async (id) => { if (!window.confirm('Delete this port forwarding rule?')) return; try { await routingAPI.deleteNatRule(id); fetchNatRules(); } catch { setPfError('Failed to delete rule'); } }; // ── Peer Route handlers ─────────────────────────────────────────────────── const handleAddPeer = async (e) => { e.preventDefault(); setPeersError(null); try { await routingAPI.addPeerRoute({ ...newPeerRoute, allowed_networks: newPeerRoute.allowed_networks.split(',').map(s => s.trim()).filter(Boolean), }); setShowPeerForm(false); setNewPeerRoute(EMPTY_PEER); fetchPeerRoutes(); fetchStatus(); } catch { setPeersError('Failed to add peer route'); } }; const handleDeletePeer = async (name) => { if (!window.confirm('Delete this peer route?')) return; try { await routingAPI.deletePeerRoute(name); fetchPeerRoutes(); fetchStatus(); } catch { setPeersError('Failed to delete peer route'); } }; // ── Firewall handlers ───────────────────────────────────────────────────── const handleAddFw = async (e) => { e.preventDefault(); setFwError(null); try { await routingAPI.addFirewallRule(newFwRule); setShowFwForm(false); setNewFwRule(EMPTY_FW); fetchFirewallRules(); fetchStatus(); } catch { setFwError('Failed to add firewall rule'); } }; const handleDeleteFw = async (id) => { if (!window.confirm('Delete this firewall rule?')) return; try { await routingAPI.deleteFirewallRule(id); fetchFirewallRules(); fetchStatus(); } catch { setFwError('Failed to delete firewall rule'); } }; // ── Diagnostics ─────────────────────────────────────────────────────────── const handleRunDiag = async (e) => { e.preventDefault(); if (!diagTarget.trim()) return; setDiagRunning(true); setDiagResult(null); try { const r = await routingAPI.testConnectivity({ target_ip: diagTarget.trim() }); setDiagResult(r.data); } catch (err) { setDiagResult({ error: err.message }); } finally { setDiagRunning(false); } }; // ── Summary counts ──────────────────────────────────────────────────────── const rs = routingStatus?.routing_status || routingStatus || {}; const routingTable = rs.routing_table || []; // ── Tabs ────────────────────────────────────────────────────────────────── const tabs = [ { id: 'overview', name: 'Overview', icon: Activity }, { id: 'portfwd', name: 'Port Forwarding', icon: ArrowRightLeft }, { id: 'nat', name: 'NAT Rules', icon: Shield }, { id: 'peers', name: 'Peer Routes', icon: Wifi }, { id: 'firewall', name: 'Firewall', icon: Settings }, { id: 'live', name: 'Live iptables', icon: Terminal }, { id: 'diagnostics', name: 'Diagnostics', icon: Activity }, ]; return (

Routing & Gateway

Manage VPN gateway, NAT, port forwarding, and firewall rules

{/* Summary strip */} {!statusLoading && (
{[ { label: 'NAT Rules', value: rs.nat_rules_count ?? natRules.length }, { label: 'Port Forwards', value: pfRules.length }, { label: 'Peer Routes', value: rs.peer_routes_count ?? peerRoutes.length }, { label: 'Firewall Rules', value: rs.firewall_rules_count ?? firewallRules.length }, ].map(({ label, value }) => (

{label}

{value}

))}
)} {/* Tabs */}
{/* ── Overview ── */} {activeTab === 'overview' && (
Refresh } /> {statusLoading ? (
) : routingTable.length > 0 ? ( {routingTable.map((r, i) => { const p = r.parsed || {}; return ( ); })}
Destination Gateway Interface Flags
{p.destination || r.route} {p.via || '—'} {p.dev || '—'} {p.metric ? `metric ${p.metric}` : ''}
) : (

No routing table entries available.

)}
)} {/* ── Port Forwarding ── */} {activeTab === 'portfwd' && (
setShowPfForm(v => !v)}> {showPfForm ? 'Cancel' : 'Add Rule'} } />

Forward inbound traffic on a public port to an internal host. Rules here are stored and re-applied on restart.

{showPfForm && (
setNewPf(p => ({...p, external_port: e.target.value}))} required />
setNewPf(p => ({...p, internal_ip: e.target.value}))} required />
setNewPf(p => ({...p, internal_port: e.target.value}))} required />
{pfError &&

{pfError}

}
)} {pfRules.length === 0 ? ( ) : pfRules.map((r, i) => ( ))}
Protocol External Port → Internal IP Internal Port
{r.protocol || 'ALL'} {r.external_port || '—'} {r.internal_ip || '—'} {r.internal_port || '—'}
)} {/* ── NAT Rules ── */} {activeTab === 'nat' && (
setShowNatForm(v => !v)}> {showNatForm ? 'Cancel' : 'Add Rule'} } /> {showNatForm && (
setNewNat(p => ({...p, source_network: e.target.value}))} required />
setNewNat(p => ({...p, target_interface: e.target.value}))} required />
{natError &&

{natError}

}
)} {natLoading ?
Loading…
: ( {natRules.length === 0 ? ( ) : natRules.map((r, i) => ( ))}
Source Network Interface Type
{r.source_network} {r.target_interface} {r.nat_type || 'MASQUERADE'}
)}
)} {/* ── Peer Routes ── */} {activeTab === 'peers' && (
setShowPeerForm(v => !v)}> {showPeerForm ? 'Cancel' : 'Add Route'} } />
OS-level ip route entries stored here. WireGuard per-peer access control (which traffic each peer is allowed) is on the Peers page, not here.
{showPeerForm && (
setNewPeerRoute(p => ({...p, peer_name: e.target.value}))} required />
setNewPeerRoute(p => ({...p, peer_ip: e.target.value}))} required />
setNewPeerRoute(p => ({...p, allowed_networks: e.target.value}))} />
{peersError &&

{peersError}

}
)} {peersLoading ?
Loading…
: ( {peerRoutes.length === 0 ? ( ) : peerRoutes.map((r, i) => ( ))}
Peer VPN IP Networks Type
{r.peer_name} {r.peer_ip} {Array.isArray(r.allowed_networks) ? r.allowed_networks.join(', ') : r.allowed_networks} {r.route_type}
)}
)} {/* ── Firewall ── */} {activeTab === 'firewall' && (
setShowFwForm(v => !v)}> {showFwForm ? 'Cancel' : 'Add Rule'} } />
This tab only shows rules added here. The Live iptables tab shows all running rules — including per-peer VPN rules (managed on the Peers page) and WireGuard PostUp rules. They are intentionally separate.
{showFwForm && (
setNewFwRule(p => ({...p, source: e.target.value}))} required />
setNewFwRule(p => ({...p, destination: e.target.value}))} required />
setNewFwRule(p => ({...p, port_range: e.target.value}))} />
{fwError &&

{fwError}

}
)} {fwLoading ?
Loading…
: ( {firewallRules.length === 0 ? ( ) : firewallRules.map((r, i) => ( ))}
Chain Source Destination Proto Port Action
{r.rule_type} {r.source} {r.destination} {r.protocol || 'ALL'} {r.port_range || r.port || '—'} {r.action}
)}
)} {/* ── Live iptables ── */} {activeTab === 'live' && (
Refresh } />
Read-only view of ALL rules running in cell-wireguard. Includes: pic-peer-* rules from the Peers page, MASQUERADE from wg0.conf PostUp, and any rules added via the forms above. Rules here cannot be edited directly.
{iptLoading ? (
) : liveIptables ? (
{Object.entries(liveIptables).map(([table, text]) => (

{table} table

                    {text || '(empty)'}
                  
))}
) : (

Click Refresh to load live iptables rules.

)}
)} {/* ── Diagnostics ── */} {activeTab === 'diagnostics' && (

Ping and traceroute a target from the server to verify routing.

setDiagTarget(e.target.value)} required />
{diagResult && (
{diagResult.error && (
{diagResult.error}
)} {diagResult.ping && (
Ping {diagResult.ping.success ? 'reachable' : 'unreachable'}
                    {diagResult.ping.output || diagResult.ping.error || '(no output)'}
                  
)} {diagResult.traceroute && (
Traceroute
                    {diagResult.traceroute.output || diagResult.traceroute.error || '(no output)'}
                  
)}
)}
)}
); } export default Routing;