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 }) => (
))}
)}
{/* Tabs */}
{tabs.map(tab => (
{
setActiveTab(tab.id);
if (tab.id === 'live' && !liveIptables) fetchLiveIptables();
}}
className={`flex items-center whitespace-nowrap px-1 py-3 border-b-2 font-medium text-sm ${
activeTab === tab.id
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.name}
))}
{/* ── Overview ── */}
{activeTab === 'overview' && (
Refresh
} />
{statusLoading ? (
) : routingTable.length > 0 ? (
Destination
Gateway
Interface
Flags
{routingTable.map((r, i) => {
const p = r.parsed || {};
return (
{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 && (
)}
Protocol
External Port
→ Internal IP
Internal Port
{pfRules.length === 0 ? (
) : pfRules.map((r, i) => (
{r.protocol || 'ALL'}
{r.external_port || '—'}
{r.internal_ip || '—'}
{r.internal_port || '—'}
handleDeletePf(r.id || i)}>
))}
)}
{/* ── NAT Rules ── */}
{activeTab === 'nat' && (
setShowNatForm(v => !v)}>
{showNatForm ? 'Cancel' : 'Add Rule'}
} />
{showNatForm && (
)}
{natLoading ? Loading…
: (
Source Network
Interface
Type
{natRules.length === 0 ? (
) : natRules.map((r, i) => (
{r.source_network}
{r.target_interface}
{r.nat_type || 'MASQUERADE'}
handleDeleteNat(r.id || i)}>
))}
)}
)}
{/* ── 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 && (
)}
{peersLoading ? Loading…
: (
Peer
VPN IP
Networks
Type
{peerRoutes.length === 0 ? (
) : peerRoutes.map((r, i) => (
{r.peer_name}
{r.peer_ip}
{Array.isArray(r.allowed_networks) ? r.allowed_networks.join(', ') : r.allowed_networks}
{r.route_type}
handleDeletePeer(r.peer_name)}>
))}
)}
)}
{/* ── 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 && (
)}
{fwLoading ? Loading…
: (
Chain
Source
Destination
Proto
Port
Action
{firewallRules.length === 0 ? (
) : firewallRules.map((r, i) => (
{r.rule_type}
{r.source}
{r.destination}
{r.protocol || 'ALL'}
{r.port_range || r.port || '—'}
{r.action}
handleDeleteFw(r.id)}>
))}
)}
)}
{/* ── 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.
{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;