903 lines
38 KiB
React
903 lines
38 KiB
React
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);
|
|
// Network Configuration state
|
|
const [networkStatus, setNetworkStatus] = useState(null);
|
|
const [networkLoading, setNetworkLoading] = useState(false);
|
|
const [networkError, setNetworkError] = useState(null);
|
|
const [isSettingUp, setIsSettingUp] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchRoutingStatus();
|
|
fetchNatRules();
|
|
fetchPeerRoutes();
|
|
fetchFirewallRules();
|
|
fetchNetworkStatus();
|
|
}, []);
|
|
|
|
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 fetchNetworkStatus = async () => {
|
|
setNetworkLoading(true);
|
|
setNetworkError(null);
|
|
try {
|
|
const response = await fetch('http://localhost:3000/api/wireguard/network/status');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setNetworkStatus(data);
|
|
} else {
|
|
throw new Error('Failed to fetch network status');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch network status:', error);
|
|
setNetworkError('Failed to fetch network status');
|
|
} finally {
|
|
setNetworkLoading(false);
|
|
}
|
|
};
|
|
|
|
const setupNetworkConfiguration = async () => {
|
|
setIsSettingUp(true);
|
|
setNetworkError(null);
|
|
try {
|
|
const response = await fetch('http://localhost:3000/api/wireguard/network/setup', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
console.log('Network setup successful:', data);
|
|
// Refresh network status
|
|
await fetchNetworkStatus();
|
|
} else {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to setup network configuration');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to setup network configuration:', error);
|
|
setNetworkError(error.message);
|
|
} finally {
|
|
setIsSettingUp(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: 'network', name: 'Network Config', icon: Wifi },
|
|
{ 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 === 'network' && (
|
|
<div>
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-medium text-gray-900">Network Configuration</h3>
|
|
<button
|
|
className="btn btn-primary flex items-center"
|
|
onClick={setupNetworkConfiguration}
|
|
disabled={isSettingUp}
|
|
>
|
|
{isSettingUp ? (
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
) : (
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
)}
|
|
{isSettingUp ? 'Setting up...' : 'Setup Network'}
|
|
</button>
|
|
</div>
|
|
|
|
{networkError && (
|
|
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
<p className="text-red-800">{networkError}</p>
|
|
</div>
|
|
)}
|
|
|
|
{networkLoading ? (
|
|
<div className="flex justify-center items-center py-8">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
) : networkStatus ? (
|
|
<div className="space-y-6">
|
|
{/* Network Status Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div className="bg-white p-4 rounded-lg border border-gray-200">
|
|
<div className="flex items-center">
|
|
<div className={`w-3 h-3 rounded-full mr-3 ${networkStatus.ip_forwarding ? 'bg-green-400' : 'bg-red-400'}`}></div>
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900">IP Forwarding</p>
|
|
<p className="text-xs text-gray-500">{networkStatus.ip_forwarding ? 'Enabled' : 'Disabled'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-4 rounded-lg border border-gray-200">
|
|
<div className="flex items-center">
|
|
<div className={`w-3 h-3 rounded-full mr-3 ${networkStatus.interface_status ? 'bg-green-400' : 'bg-red-400'}`}></div>
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900">WireGuard Interface</p>
|
|
<p className="text-xs text-gray-500">{networkStatus.interface_status ? 'Up' : 'Down'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-4 rounded-lg border border-gray-200">
|
|
<div className="flex items-center">
|
|
<div className={`w-3 h-3 rounded-full mr-3 ${networkStatus.nat_rules ? 'bg-green-400' : 'bg-red-400'}`}></div>
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900">NAT Rules</p>
|
|
<p className="text-xs text-gray-500">{networkStatus.nat_rules ? 'Configured' : 'Missing'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-4 rounded-lg border border-gray-200">
|
|
<div className="flex items-center">
|
|
<div className={`w-3 h-3 rounded-full mr-3 ${networkStatus.forwarding_rules ? 'bg-green-400' : 'bg-red-400'}`}></div>
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900">Forwarding Rules</p>
|
|
<p className="text-xs text-gray-500">{networkStatus.forwarding_rules ? 'Configured' : 'Missing'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Configuration Details */}
|
|
<div className="bg-gray-50 p-4 rounded-lg">
|
|
<h4 className="text-md font-medium text-gray-900 mb-3">Configuration Details</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Last Updated:</span>
|
|
<span className="text-gray-900">{new Date(networkStatus.timestamp).toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">IP Forwarding:</span>
|
|
<span className={`font-medium ${networkStatus.ip_forwarding ? 'text-green-600' : 'text-red-600'}`}>
|
|
{networkStatus.ip_forwarding ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">WireGuard Interface:</span>
|
|
<span className={`font-medium ${networkStatus.interface_status ? 'text-green-600' : 'text-red-600'}`}>
|
|
{networkStatus.interface_status ? 'Up (wg0)' : 'Down'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">NAT Translation:</span>
|
|
<span className={`font-medium ${networkStatus.nat_rules ? 'text-green-600' : 'text-red-600'}`}>
|
|
{networkStatus.nat_rules ? 'Active' : 'Not Configured'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Traffic Forwarding:</span>
|
|
<span className={`font-medium ${networkStatus.forwarding_rules ? 'text-green-600' : 'text-red-600'}`}>
|
|
{networkStatus.forwarding_rules ? 'Allowed' : 'Blocked'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="bg-blue-50 p-4 rounded-lg">
|
|
<h4 className="text-md font-medium text-blue-900 mb-2">Quick Actions</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
className="btn btn-sm btn-outline"
|
|
onClick={fetchNetworkStatus}
|
|
>
|
|
Refresh Status
|
|
</button>
|
|
<button
|
|
className="btn btn-sm btn-primary"
|
|
onClick={setupNetworkConfiguration}
|
|
disabled={isSettingUp}
|
|
>
|
|
{isSettingUp ? 'Setting up...' : 'Setup Network'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<p className="text-gray-500">Failed to load network status</p>
|
|
<button
|
|
className="btn btn-primary mt-2"
|
|
onClick={fetchNetworkStatus}
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
)}
|
|
</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; |