fix: WireGuard routing, DNS, service access, and UI improvements
- Fix CoreDNS not loading .cell zones (wrong Corefile path, now uses -conf flag) - Fix WireGuard server address conflict (172.20.0.1/16 overlapped with Docker network; changed to 10.0.0.1/24 to eliminate duplicate routes) - Add SERVERMODE=true and sysctls to WireGuard docker-compose for server mode - Fix DNS zone file parser to handle 4-field records (name IN type value) - Add get_dns_records() to NetworkManager; mount data/dns into API container - Fix peer config endpoint: look up IP/key from registry, use real endpoint - Add bulk peer statuses endpoint keyed by public_key - Normalize snake_case API fields to camelCase in WireGuard UI - Add port check endpoint (checks via live handshake, not unreliable TCP probe) - Add Caddy virtual hosts for ui/calendar/files/mail .cell domains (HTTP only) - Fix cell config domain default from cell.local to cell - Fix Routing Network Config tab (was calling hardcoded localhost:3000) - Fix DNS records display (record.value not record.ip) - Move service access guide to top of Dashboard with login hints - Add /api/routing/setup endpoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,13 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { cellAPI, servicesAPI } from '../services/api';
|
||||
|
||||
const SERVICES = [
|
||||
{ name: 'Cell Home', url: 'http://mycell.cell', desc: 'Main UI — no login needed' },
|
||||
{ name: 'Calendar', url: 'http://calendar.cell', desc: 'Login: your WireGuard username' },
|
||||
{ name: 'Files', url: 'http://files.cell', desc: 'Login: admin / admin123' },
|
||||
{ name: 'Webmail', url: 'http://mail.cell', desc: 'Login: admin@rainloop.net / 12345' },
|
||||
];
|
||||
|
||||
function Dashboard({ isOnline }) {
|
||||
const navigate = useNavigate();
|
||||
const [cellStatus, setCellStatus] = useState(null);
|
||||
@@ -203,11 +210,29 @@ function Dashboard({ isOnline }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Overview of your Personal Internet Cell status and services
|
||||
</p>
|
||||
<p className="mt-1 text-gray-600">Personal Internet Cell — connect via WireGuard to access services</p>
|
||||
</div>
|
||||
|
||||
{/* Access Services — shown first, no scroll needed */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">Services (connect via WireGuard first)</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{SERVICES.map(svc => (
|
||||
<a
|
||||
key={svc.url}
|
||||
href={svc.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="card hover:shadow-md transition-shadow group border border-gray-100"
|
||||
>
|
||||
<p className="text-sm font-semibold text-primary-700 group-hover:text-primary-900">{svc.name}</p>
|
||||
<p className="font-mono text-xs text-gray-400 mt-0.5 truncate">{svc.url}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{svc.desc}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cell Status */}
|
||||
|
||||
@@ -58,8 +58,11 @@ function NetworkServices() {
|
||||
{dnsRecords.length > 0 ? (
|
||||
dnsRecords.map((record, index) => (
|
||||
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<span className="text-sm font-medium">{record.name}</span>
|
||||
<span className="text-sm text-gray-500">{record.ip}</span>
|
||||
<div>
|
||||
<span className="text-sm font-medium">{record.name}</span>
|
||||
<span className="text-xs text-gray-400 ml-1">.{record.zone}</span>
|
||||
</div>
|
||||
<span className="text-sm font-mono text-gray-600">{record.value}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
|
||||
+33
-109
@@ -95,7 +95,7 @@ function Routing() {
|
||||
setNetworkLoading(true);
|
||||
setNetworkError(null);
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/api/wireguard/network/status');
|
||||
const response = await fetch('/api/routing/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setNetworkStatus(data);
|
||||
@@ -114,11 +114,9 @@ function Routing() {
|
||||
setIsSettingUp(true);
|
||||
setNetworkError(null);
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/api/wireguard/network/setup', {
|
||||
const response = await fetch('/api/routing/setup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -404,125 +402,51 @@ function Routing() {
|
||||
</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 className="space-y-4">
|
||||
{/* Status cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: 'Routing', value: networkStatus.status === 'online' ? 'Online' : 'Offline', ok: networkStatus.running },
|
||||
{ label: 'NAT Rules', value: networkStatus.nat_rules_count ?? 0, ok: true },
|
||||
{ label: 'Firewall Rules', value: networkStatus.firewall_rules_count ?? 0, ok: true },
|
||||
{ label: 'Peer Routes', value: networkStatus.peer_routes_count ?? 0, ok: true },
|
||||
].map(item => (
|
||||
<div key={item.label} className="bg-white p-4 rounded-lg border border-gray-200">
|
||||
<p className="text-xs text-gray-500">{item.label}</p>
|
||||
<p className={`text-lg font-semibold mt-1 ${item.ok ? 'text-gray-900' : 'text-red-600'}`}>{item.value}</p>
|
||||
</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>
|
||||
{/* Routing table */}
|
||||
{networkStatus.routing_status?.routing_table?.length > 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Active Routes</h4>
|
||||
<div className="space-y-1 font-mono text-xs text-gray-600">
|
||||
{networkStatus.routing_status.routing_table.map((r, i) => (
|
||||
<div key={i} className="flex gap-4">
|
||||
<span className="text-gray-900 w-40 truncate">{r.parsed?.destination || r.route}</span>
|
||||
<span className="text-gray-500">via {r.parsed?.dev || '—'}</span>
|
||||
{r.parsed?.via && <span className="text-gray-400">{r.parsed.via}</span>}
|
||||
</div>
|
||||
))}
|
||||
</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 className="flex gap-2">
|
||||
<button className="btn btn-secondary text-sm" onClick={fetchNetworkStatus}>Refresh</button>
|
||||
</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>
|
||||
<p className="text-gray-500">Could not load network status</p>
|
||||
<button className="btn btn-primary mt-2" onClick={fetchNetworkStatus}>Retry</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,14 @@ function WireGuard() {
|
||||
const refreshExternalIp = async () => {
|
||||
setIsRefreshingIp(true);
|
||||
try {
|
||||
const response = await fetch('/api/wireguard/refresh-ip', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
setServerConfig(prev => ({ ...prev, ...data }));
|
||||
// Refresh IP first (fast)
|
||||
const ipResp = await fetch('/api/wireguard/refresh-ip', { method: 'POST' });
|
||||
const ipData = await ipResp.json();
|
||||
setServerConfig(prev => ({ ...prev, ...ipData, port_open: 'checking' }));
|
||||
// Then check port (slow — external call)
|
||||
const portResp = await fetch('/api/wireguard/check-port', { method: 'POST' });
|
||||
const portData = await portResp.json();
|
||||
setServerConfig(prev => ({ ...prev, port_open: portData.port_open }));
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh IP:', e);
|
||||
} finally {
|
||||
@@ -71,36 +76,36 @@ function WireGuard() {
|
||||
persistent_keepalive: peer.persistent_keepalive || wireguardMap[peer.peer || peer.name]?.PersistentKeepalive || 25
|
||||
}));
|
||||
|
||||
// Load peer statuses first
|
||||
const statusPromises = mergedPeers.map(async (peer) => {
|
||||
if (peer.public_key) {
|
||||
const status = await getPeerStatus(peer);
|
||||
return { peerId: peer.name, status };
|
||||
}
|
||||
return { peerId: peer.name, status: { online: null, lastHandshake: null, transferRx: 0, transferTx: 0 } };
|
||||
// Load all peer statuses in one call (keyed by public_key)
|
||||
let liveStatuses = {};
|
||||
try {
|
||||
const stResp = await fetch('/api/wireguard/peers/statuses');
|
||||
if (stResp.ok) liveStatuses = await stResp.json();
|
||||
} catch (_) {}
|
||||
|
||||
// Normalize snake_case API fields to camelCase for UI
|
||||
const normalizeStatus = (st) => ({
|
||||
online: st.online ?? null,
|
||||
lastHandshake: st.last_handshake || st.lastHandshake || null,
|
||||
lastHandshakeSecondsAgo: st.last_handshake_seconds_ago ?? null,
|
||||
transferRx: st.transfer_rx ?? st.transferRx ?? 0,
|
||||
transferTx: st.transfer_tx ?? st.transferTx ?? 0,
|
||||
endpoint: st.endpoint || null,
|
||||
});
|
||||
|
||||
const statusResults = await Promise.all(statusPromises);
|
||||
// Build name→status map and annotate peers
|
||||
const statusMap = {};
|
||||
statusResults.forEach(({ peerId, status }) => {
|
||||
statusMap[peerId] = status;
|
||||
const annotated = mergedPeers.map(peer => {
|
||||
const raw = liveStatuses[peer.public_key] || { online: null };
|
||||
const st = normalizeStatus(raw);
|
||||
statusMap[peer.name] = st;
|
||||
return { ...peer, _liveStatus: st };
|
||||
});
|
||||
setPeerStatuses(statusMap);
|
||||
setTotalPeers(annotated.length);
|
||||
|
||||
// Set total peers count
|
||||
setTotalPeers(mergedPeers.length);
|
||||
|
||||
// Filter to only show live connected peers
|
||||
const livePeers = mergedPeers.filter(peer => {
|
||||
const peerStatus = statusMap[peer.name];
|
||||
return peerStatus && (
|
||||
peerStatus.online === true ||
|
||||
(peerStatus.lastHandshake && peerStatus.lastHandshake !== null) ||
|
||||
(peerStatus.transferRx > 0 || peerStatus.transferTx > 0)
|
||||
);
|
||||
});
|
||||
|
||||
setPeers(livePeers);
|
||||
// Show all peers; live ones bubble up via status indicator
|
||||
setPeers(annotated);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch WireGuard data:', error);
|
||||
} finally {
|
||||
@@ -339,13 +344,13 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 bg-green-100 rounded-lg">
|
||||
<Activity className="h-6 w-6 text-green-600" />
|
||||
<div className={`p-2 rounded-lg ${peers.some(p => p._liveStatus?.online) ? 'bg-green-100' : 'bg-gray-100'}`}>
|
||||
<Activity className={`h-6 w-6 ${peers.some(p => p._liveStatus?.online) ? 'text-green-600' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Live Connections</p>
|
||||
<p className="text-lg font-semibold text-gray-900">
|
||||
{peers.length}
|
||||
<p className={`text-lg font-semibold ${peers.some(p => p._liveStatus?.online) ? 'text-green-600' : 'text-gray-900'}`}>
|
||||
{peers.filter(p => p._liveStatus?.online).length} / {totalPeers}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -380,7 +385,7 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
||||
Refresh IP
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">External IP</p>
|
||||
<p className="font-mono font-semibold text-gray-900">
|
||||
@@ -393,6 +398,25 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
||||
{serverConfig?.endpoint || `<SERVER_IP>:${serverConfig?.port || 51820}`}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">UDP Port {serverConfig?.port || 51820}</p>
|
||||
{serverConfig ? (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
serverConfig.port_open === true ? 'bg-green-100 text-green-800' :
|
||||
serverConfig.port_open === false ? 'bg-red-100 text-red-800' :
|
||||
'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full mr-1.5 ${
|
||||
serverConfig.port_open === true ? 'bg-green-400' :
|
||||
serverConfig.port_open === false ? 'bg-red-400' : 'bg-gray-400'
|
||||
}`} />
|
||||
{serverConfig.port_open === true ? 'Open' :
|
||||
serverConfig.port_open === false ? 'Blocked' :
|
||||
serverConfig.port_open === 'checking' ? 'Checking…' :
|
||||
'Click Refresh IP to check'}
|
||||
</span>
|
||||
) : '—'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 mb-1">Server Public Key</p>
|
||||
<p className="font-mono text-xs text-gray-700 break-all">
|
||||
@@ -406,6 +430,12 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
||||
External IP could not be detected. Check internet connectivity, then click Refresh IP.
|
||||
</div>
|
||||
)}
|
||||
{serverConfig && serverConfig.port_open === false && (
|
||||
<div className="mt-3 flex items-center text-red-700 bg-red-50 rounded p-2 text-sm">
|
||||
<AlertCircle className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
UDP port {serverConfig.port || 51820} appears closed. Check your router/firewall and forward this port to this machine.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Traffic Stats */}
|
||||
@@ -486,7 +516,7 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{peers.map((peer, index) => {
|
||||
const peerStatus = peerStatuses[peer.name] || { online: null, lastHandshake: null, transferRx: 0, transferTx: 0 };
|
||||
const peerStatus = peerStatuses[peer.name] || { online: null, lastHandshake: null, transferRx: 0, transferTx: 0, endpoint: null };
|
||||
return (
|
||||
<tr key={index} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
|
||||
Reference in New Issue
Block a user