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:
2026-04-20 12:43:23 -04:00
parent bd67764bf4
commit e79ee08c63
14 changed files with 422 additions and 306 deletions
+63 -33
View File
@@ -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">