This commit is contained in:
Constantin
2025-09-12 23:04:52 +03:00
commit 2277b11563
127 changed files with 23640 additions and 0 deletions
+707
View File
@@ -0,0 +1,707 @@
import { useState, useEffect } from 'react';
import { Plus, Trash2, Wifi, Shield, Activity, Settings } from 'lucide-react';
import { routingAPI } from '../services/api';
function Routing() {
const [routingStatus, setRoutingStatus] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview');
// NAT management state
const [natRules, setNatRules] = useState([]);
const [natLoading, setNatLoading] = useState(false);
const [natError, setNatError] = useState(null);
const [showNatForm, setShowNatForm] = useState(false);
const [newNat, setNewNat] = useState({ source_network: '', target_interface: '', masquerade: true, nat_type: 'MASQUERADE', protocol: 'ALL', external_port: '', internal_ip: '', internal_port: '' });
const [natSubmitting, setNatSubmitting] = useState(false);
// Peer Routes management state
const [peerRoutes, setPeerRoutes] = useState([]);
const [peersLoading, setPeersLoading] = useState(false);
const [peersError, setPeersError] = useState(null);
const [showPeerForm, setShowPeerForm] = useState(false);
const [newPeerRoute, setNewPeerRoute] = useState({ peer_name: '', peer_ip: '', allowed_networks: '', route_type: 'lan' });
const [peerSubmitting, setPeerSubmitting] = useState(false);
// Firewall Rules management state
const [firewallRules, setFirewallRules] = useState([]);
const [fwLoading, setFwLoading] = useState(false);
const [fwError, setFwError] = useState(null);
const [showFwForm, setShowFwForm] = useState(false);
const [newFwRule, setNewFwRule] = useState({ rule_type: 'INPUT', source: '', destination: '', action: 'ACCEPT', protocol: 'ALL', port_range: '' });
const [fwSubmitting, setFwSubmitting] = useState(false);
useEffect(() => {
fetchRoutingStatus();
fetchNatRules();
fetchPeerRoutes();
fetchFirewallRules();
}, []);
const fetchRoutingStatus = async () => {
try {
const response = await routingAPI.getStatus();
setRoutingStatus(response.data);
} catch (error) {
console.error('Failed to fetch routing status:', error);
} finally {
setIsLoading(false);
}
};
const fetchNatRules = async () => {
setNatLoading(true);
setNatError(null);
try {
const response = await routingAPI.getNatRules();
setNatRules(response.data.nat_rules || []);
} catch (error) {
setNatError('Failed to load NAT rules');
} finally {
setNatLoading(false);
}
};
const fetchPeerRoutes = async () => {
setPeersLoading(true);
setPeersError(null);
try {
const response = await routingAPI.getPeerRoutes();
setPeerRoutes(response.data.peer_routes || []);
} catch (error) {
setPeersError('Failed to load peer routes');
} finally {
setPeersLoading(false);
}
};
const fetchFirewallRules = async () => {
setFwLoading(true);
setFwError(null);
try {
const response = await routingAPI.getFirewallRules();
setFirewallRules(response.data.firewall_rules || []);
} catch (error) {
setFwError('Failed to load firewall rules');
} finally {
setFwLoading(false);
}
};
const handleNatInputChange = (e) => {
const { name, value, type, checked } = e.target;
setNewNat((prev) => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleAddNatRule = async (e) => {
e.preventDefault();
setNatSubmitting(true);
setNatError(null);
try {
await routingAPI.addNatRule(newNat);
setShowNatForm(false);
setNewNat({ source_network: '', target_interface: '', masquerade: true, nat_type: 'MASQUERADE', protocol: 'ALL', external_port: '', internal_ip: '', internal_port: '' });
fetchNatRules();
fetchRoutingStatus();
} catch (error) {
setNatError('Failed to add NAT rule');
} finally {
setNatSubmitting(false);
}
};
const handleDeleteNatRule = async (ruleId) => {
if (!window.confirm('Delete this NAT rule?')) return;
setNatLoading(true);
setNatError(null);
try {
await routingAPI.deleteNatRule(ruleId);
fetchNatRules();
fetchRoutingStatus();
} catch (error) {
setNatError('Failed to delete NAT rule');
} finally {
setNatLoading(false);
}
};
const handlePeerInputChange = (e) => {
const { name, value } = e.target;
setNewPeerRoute((prev) => ({ ...prev, [name]: value }));
};
const handleAddPeerRoute = async (e) => {
e.preventDefault();
setPeerSubmitting(true);
setPeersError(null);
try {
// allowed_networks: comma-separated string to array
const payload = {
...newPeerRoute,
allowed_networks: newPeerRoute.allowed_networks.split(',').map((s) => s.trim()).filter(Boolean),
};
await routingAPI.addPeerRoute(payload);
setShowPeerForm(false);
setNewPeerRoute({ peer_name: '', peer_ip: '', allowed_networks: '', route_type: 'lan' });
fetchPeerRoutes();
fetchRoutingStatus();
} catch (error) {
setPeersError('Failed to add peer route');
} finally {
setPeerSubmitting(false);
}
};
const handleDeletePeerRoute = async (peerName) => {
if (!window.confirm('Delete this peer route?')) return;
setPeersLoading(true);
setPeersError(null);
try {
await routingAPI.deletePeerRoute(peerName);
fetchPeerRoutes();
fetchRoutingStatus();
} catch (error) {
setPeersError('Failed to delete peer route');
} finally {
setPeersLoading(false);
}
};
const handleFwInputChange = (e) => {
const { name, value } = e.target;
setNewFwRule((prev) => ({ ...prev, [name]: value }));
};
const handleAddFwRule = async (e) => {
e.preventDefault();
setFwSubmitting(true);
setFwError(null);
try {
const payload = { ...newFwRule };
if (!payload.port) delete payload.port;
await routingAPI.addFirewallRule(payload);
setShowFwForm(false);
setNewFwRule({ rule_type: 'INPUT', source: '', destination: '', action: 'ACCEPT', protocol: 'ALL', port_range: '' });
fetchFirewallRules();
fetchRoutingStatus();
} catch (error) {
setFwError('Failed to add firewall rule');
} finally {
setFwSubmitting(false);
}
};
const handleDeleteFwRule = async (ruleId) => {
if (!window.confirm('Delete this firewall rule?')) return;
setFwLoading(true);
setFwError(null);
try {
await routingAPI.deleteFirewallRule(ruleId);
fetchFirewallRules();
fetchRoutingStatus();
} catch (error) {
setFwError('Failed to delete firewall rule');
} finally {
setFwLoading(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
);
}
const tabs = [
{ id: 'overview', name: 'Overview', icon: Activity },
{ id: 'nat', name: 'NAT Rules', icon: Shield },
{ id: 'peers', name: 'Peer Routes', icon: Wifi },
{ id: 'firewall', name: 'Firewall', icon: Settings },
];
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Routing & Gateway</h1>
<p className="mt-2 text-gray-600">
Manage VPN gateway, NAT rules, and routing configuration
</p>
</div>
{/* Status Overview */}
{routingStatus && (
<div className="mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="card">
<div className="flex items-center">
<Shield className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">NAT Rules</p>
<p className="text-lg font-semibold text-gray-900">
{routingStatus.nat_rules_count || 0}
</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Wifi className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Peer Routes</p>
<p className="text-lg font-semibold text-gray-900">
{routingStatus.peer_routes_count || 0}
</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Settings className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Firewall Rules</p>
<p className="text-lg font-semibold text-gray-900">
{routingStatus.firewall_rules_count || 0}
</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<Activity className="h-8 w-8 text-primary-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Exit Nodes</p>
<p className="text-lg font-semibold text-gray-900">
{routingStatus.exit_nodes_count || 0}
</p>
</div>
</div>
</div>
</div>
</div>
)}
{/* Tabs */}
<div className="mb-6">
<nav className="flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center px-1 py-2 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.icon className="h-4 w-4 mr-2" />
{tab.name}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="card">
{activeTab === 'overview' && (
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Routing Overview</h3>
{routingStatus?.routing_table && routingStatus.routing_table.length > 0 ? (
<div className="space-y-2">
{routingStatus.routing_table.map((route, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center">
<Wifi className="h-4 w-4 text-primary-500 mr-2" />
<span className="text-sm font-medium text-gray-900">
{route.route}
</span>
</div>
<div className="text-xs text-gray-500">
{route.parsed?.via && `via ${route.parsed.via}`}
</div>
</div>
))}
</div>
) : (
<p className="text-gray-500">No routing table entries available.</p>
)}
</div>
)}
{activeTab === 'nat' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">NAT Rules</h3>
<button className="btn btn-primary flex items-center" onClick={() => setShowNatForm((v) => !v)}>
<Plus className="h-4 w-4 mr-2" />
{showNatForm ? 'Cancel' : 'Add NAT Rule'}
</button>
</div>
{showNatForm && (
<form className="mb-4 p-4 bg-gray-50 rounded-lg" onSubmit={handleAddNatRule}>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<input
type="text"
name="source_network"
placeholder="Source Network (e.g. 192.168.1.0/24)"
className="input"
value={newNat.source_network}
onChange={handleNatInputChange}
required
/>
<input
type="text"
name="target_interface"
placeholder="Target Interface (e.g. eth0)"
className="input"
value={newNat.target_interface}
onChange={handleNatInputChange}
required
/>
<label className="flex items-center space-x-2">
<input
type="checkbox"
name="masquerade"
checked={newNat.masquerade}
onChange={handleNatInputChange}
/>
<span>Masquerade</span>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<select
name="nat_type"
className="input"
value={newNat.nat_type}
onChange={handleNatInputChange}
>
<option value="MASQUERADE">MASQUERADE</option>
<option value="SNAT">SNAT</option>
<option value="DNAT">DNAT (Port Forward)</option>
</select>
<select
name="protocol"
className="input"
value={newNat.protocol}
onChange={handleNatInputChange}
>
<option value="ALL">ALL</option>
<option value="TCP">TCP</option>
<option value="UDP">UDP</option>
</select>
<input
type="text"
name="external_port"
placeholder="External Port (for DNAT)"
className="input"
value={newNat.external_port}
onChange={handleNatInputChange}
/>
<input
type="text"
name="internal_ip"
placeholder="Internal IP (for DNAT)"
className="input"
value={newNat.internal_ip}
onChange={handleNatInputChange}
/>
<input
type="text"
name="internal_port"
placeholder="Internal Port (for DNAT)"
className="input"
value={newNat.internal_port}
onChange={handleNatInputChange}
/>
</div>
<div className="text-xs text-gray-500 mt-2">
<span>Advanced: Use DNAT for port forwarding, specify protocol/ports as needed.</span>
</div>
<div className="mt-4 flex justify-end">
<button type="submit" className="btn btn-primary" disabled={natSubmitting}>
{natSubmitting ? 'Adding...' : 'Add Rule'}
</button>
</div>
{natError && <p className="text-red-500 mt-2">{natError}</p>}
</form>
)}
{natLoading ? (
<div className="py-8 text-center text-gray-500">Loading NAT rules...</div>
) : natError ? (
<div className="py-8 text-center text-red-500">{natError}</div>
) : natRules.length === 0 ? (
<div className="py-8 text-center text-gray-500">No NAT rules configured.</div>
) : (
<table className="min-w-full bg-white rounded-lg overflow-hidden">
<thead>
<tr>
<th className="px-4 py-2 text-left">Source Network</th>
<th className="px-4 py-2 text-left">Target Interface</th>
<th className="px-4 py-2 text-left">Masquerade</th>
<th className="px-4 py-2 text-left">Type</th>
<th className="px-4 py-2 text-left">Protocol</th>
<th className="px-4 py-2 text-left">Ext Port</th>
<th className="px-4 py-2 text-left">Int IP</th>
<th className="px-4 py-2 text-left">Int Port</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{natRules.map((rule, idx) => (
<tr key={rule.id || idx} className="border-t">
<td className="px-4 py-2">{rule.source_network}</td>
<td className="px-4 py-2">{rule.target_interface}</td>
<td className="px-4 py-2">{rule.masquerade ? 'Yes' : 'No'}</td>
<td className="px-4 py-2">{rule.nat_type || 'MASQUERADE'}</td>
<td className="px-4 py-2">{rule.protocol || 'ALL'}</td>
<td className="px-4 py-2">{rule.external_port || '-'}</td>
<td className="px-4 py-2">{rule.internal_ip || '-'}</td>
<td className="px-4 py-2">{rule.internal_port || '-'}</td>
<td className="px-4 py-2 text-right">
<button
className="btn btn-danger btn-sm flex items-center"
onClick={() => handleDeleteNatRule(rule.id || idx)}
>
<Trash2 className="h-4 w-4 mr-1" /> Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{activeTab === 'peers' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Peer Routes</h3>
<button className="btn btn-primary flex items-center" onClick={() => setShowPeerForm((v) => !v)}>
<Plus className="h-4 w-4 mr-2" />
{showPeerForm ? 'Cancel' : 'Add Peer Route'}
</button>
</div>
{showPeerForm && (
<form className="mb-4 p-4 bg-gray-50 rounded-lg" onSubmit={handleAddPeerRoute}>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<input
type="text"
name="peer_name"
placeholder="Peer Name"
className="input"
value={newPeerRoute.peer_name}
onChange={handlePeerInputChange}
required
/>
<input
type="text"
name="peer_ip"
placeholder="Peer IP (e.g. 10.0.0.2)"
className="input"
value={newPeerRoute.peer_ip}
onChange={handlePeerInputChange}
required
/>
<input
type="text"
name="allowed_networks"
placeholder="Allowed Networks (comma-separated)"
className="input"
value={newPeerRoute.allowed_networks}
onChange={handlePeerInputChange}
/>
<select
name="route_type"
className="input"
value={newPeerRoute.route_type}
onChange={handlePeerInputChange}
>
<option value="lan">LAN</option>
<option value="exit">Exit</option>
<option value="bridge">Bridge</option>
<option value="split">Split</option>
</select>
</div>
<div className="mt-4 flex justify-end">
<button type="submit" className="btn btn-primary" disabled={peerSubmitting}>
{peerSubmitting ? 'Adding...' : 'Add Route'}
</button>
</div>
{peersError && <p className="text-red-500 mt-2">{peersError}</p>}
</form>
)}
{peersLoading ? (
<div className="py-8 text-center text-gray-500">Loading peer routes...</div>
) : peersError ? (
<div className="py-8 text-center text-red-500">{peersError}</div>
) : peerRoutes.length === 0 ? (
<div className="py-8 text-center text-gray-500">No peer routes configured.</div>
) : (
<table className="min-w-full bg-white rounded-lg overflow-hidden">
<thead>
<tr>
<th className="px-4 py-2 text-left">Peer Name</th>
<th className="px-4 py-2 text-left">Peer IP</th>
<th className="px-4 py-2 text-left">Allowed Networks</th>
<th className="px-4 py-2 text-left">Route Type</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{peerRoutes.map((route, idx) => (
<tr key={route.peer_name || idx} className="border-t">
<td className="px-4 py-2">{route.peer_name}</td>
<td className="px-4 py-2">{route.peer_ip}</td>
<td className="px-4 py-2">{Array.isArray(route.allowed_networks) ? route.allowed_networks.join(', ') : route.allowed_networks}</td>
<td className="px-4 py-2">{route.route_type}</td>
<td className="px-4 py-2 text-right">
<button
className="btn btn-danger btn-sm flex items-center"
onClick={() => handleDeletePeerRoute(route.peer_name)}
>
<Trash2 className="h-4 w-4 mr-1" /> Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{activeTab === 'firewall' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Firewall Rules</h3>
<button className="btn btn-primary flex items-center" onClick={() => setShowFwForm((v) => !v)}>
<Plus className="h-4 w-4 mr-2" />
{showFwForm ? 'Cancel' : 'Add Firewall Rule'}
</button>
</div>
{showFwForm && (
<form className="mb-4 p-4 bg-gray-50 rounded-lg" onSubmit={handleAddFwRule}>
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<select
name="rule_type"
className="input"
value={newFwRule.rule_type}
onChange={handleFwInputChange}
>
<option value="INPUT">INPUT</option>
<option value="OUTPUT">OUTPUT</option>
<option value="FORWARD">FORWARD</option>
</select>
<input
type="text"
name="source"
placeholder="Source (e.g. 192.168.1.0/24)"
className="input"
value={newFwRule.source}
onChange={handleFwInputChange}
required
/>
<input
type="text"
name="destination"
placeholder="Destination (e.g. 0.0.0.0/0)"
className="input"
value={newFwRule.destination}
onChange={handleFwInputChange}
required
/>
<select
name="protocol"
className="input"
value={newFwRule.protocol}
onChange={handleFwInputChange}
>
<option value="ALL">ALL</option>
<option value="TCP">TCP</option>
<option value="UDP">UDP</option>
<option value="ICMP">ICMP</option>
</select>
<input
type="text"
name="port_range"
placeholder="Port or Range (e.g. 80 or 1000-2000)"
className="input"
value={newFwRule.port_range}
onChange={handleFwInputChange}
/>
<select
name="action"
className="input"
value={newFwRule.action}
onChange={handleFwInputChange}
>
<option value="ACCEPT">ACCEPT</option>
<option value="DROP">DROP</option>
<option value="REJECT">REJECT</option>
</select>
</div>
<div className="text-xs text-gray-500 mt-2">
<span>Advanced: Specify protocol and port/range for granular matching.</span>
</div>
<div className="mt-4 flex justify-end">
<button type="submit" className="btn btn-primary" disabled={fwSubmitting}>
{fwSubmitting ? 'Adding...' : 'Add Rule'}
</button>
</div>
{fwError && <p className="text-red-500 mt-2">{fwError}</p>}
</form>
)}
{fwLoading ? (
<div className="py-8 text-center text-gray-500">Loading firewall rules...</div>
) : fwError ? (
<div className="py-8 text-center text-red-500">{fwError}</div>
) : firewallRules.length === 0 ? (
<div className="py-8 text-center text-gray-500">No firewall rules configured.</div>
) : (
<table className="min-w-full bg-white rounded-lg overflow-hidden">
<thead>
<tr>
<th className="px-4 py-2 text-left">Rule Type</th>
<th className="px-4 py-2 text-left">Source</th>
<th className="px-4 py-2 text-left">Destination</th>
<th className="px-4 py-2 text-left">Protocol</th>
<th className="px-4 py-2 text-left">Port/Range</th>
<th className="px-4 py-2 text-left">Action</th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{firewallRules.map((rule, idx) => (
<tr key={rule.id || idx} className="border-t">
<td className="px-4 py-2">{rule.rule_type}</td>
<td className="px-4 py-2">{rule.source}</td>
<td className="px-4 py-2">{rule.destination}</td>
<td className="px-4 py-2">{rule.protocol || 'ALL'}</td>
<td className="px-4 py-2">{rule.port_range || '-'}</td>
<td className="px-4 py-2">{rule.action}</td>
<td className="px-4 py-2 text-right">
<button
className="btn btn-danger btn-sm flex items-center"
onClick={() => handleDeleteFwRule(rule.id || idx)}
>
<Trash2 className="h-4 w-4 mr-1" /> Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
</div>
);
}
export default Routing;