fix: correct DNS records, peer dashboard field names, and services API response

- network_manager: api/webui DNS records now point to Caddy (172.20.0.2)
  instead of their container IPs so Caddy can reverse-proxy correctly
- ip_utils: add webui.dev block to generated Caddyfile
- config/caddy/Caddyfile: regenerated with webui.dev block
- config/dns/Corefile: simplify to single forward zone (remove duplicate)
- app.py peer_dashboard: rename peer_name→name, rx_bytes→transfer_rx,
  tx_bytes→transfer_tx to match PeerDashboard.jsx; add service_urls dict
- app.py peer_services: fix DNS (10.0.0.1→real CoreDNS IP), CalDAV URL
  (radicale.dev:5232→calendar.dev), email structure (flat→nested smtp/imap
  objects), rename webdav→files, add WireGuard config text, add username field
- PeerDashboard.jsx: render service icon links from service_urls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:11:21 -04:00
parent e5d59fd94d
commit 3690c6d955
6 changed files with 132 additions and 95 deletions
+45 -13
View File
@@ -32,7 +32,7 @@ import contextvars
API_START_TIME = time.time() API_START_TIME = time.time()
from network_manager import NetworkManager from network_manager import NetworkManager
from wireguard_manager import WireGuardManager from wireguard_manager import WireGuardManager, _resolve_peer_dns
from peer_registry import PeerRegistry from peer_registry import PeerRegistry
from email_manager import EmailManager from email_manager import EmailManager
from calendar_manager import CalendarManager from calendar_manager import CalendarManager
@@ -3086,14 +3086,27 @@ def peer_dashboard():
peer_ip = peer.get('ip', '') peer_ip = peer.get('ip', '')
allowed_ips = f"{peer_ip.split('/')[0]}/32" if peer_ip else '' allowed_ips = f"{peer_ip.split('/')[0]}/32" if peer_ip else ''
domain = _configured_domain()
_svc_url_map = {
'calendar': f'http://calendar.{domain}',
'files': f'http://files.{domain}',
'mail': f'http://mail.{domain}',
'webdav': f'http://webdav.{domain}',
}
service_urls = {
svc: _svc_url_map[svc]
for svc in peer.get('service_access', [])
if svc in _svc_url_map
}
return jsonify({ return jsonify({
'peer_name': peer_name, 'name': peer_name,
'ip': peer_ip, 'ip': peer_ip,
'service_access': peer.get('service_access', []), 'service_access': peer.get('service_access', []),
'service_urls': service_urls,
'online': wg_stats.get('online'), 'online': wg_stats.get('online'),
'rx_bytes': wg_stats.get('transfer_rx', 0), 'transfer_rx': wg_stats.get('transfer_rx', 0),
'tx_bytes': wg_stats.get('transfer_tx', 0), 'transfer_tx': wg_stats.get('transfer_tx', 0),
'last_handshake': wg_stats.get('last_handshake'), 'last_handshake': wg_stats.get('last_handshake'),
'allowed_ips': peer.get('allowed_ips', allowed_ips), 'allowed_ips': peer.get('allowed_ips', allowed_ips),
}) })
@@ -3112,32 +3125,51 @@ def peer_services():
server_public_key = '' server_public_key = ''
wg_port = 51820 wg_port = 51820
server_endpoint = ''
try: try:
server_public_key = wireguard_manager.get_keys().get('public_key', '') server_public_key = wireguard_manager.get_keys().get('public_key', '')
wg_port = config_manager.configs.get('_identity', {}).get('wireguard_port', 51820) wg_port = config_manager.configs.get('_identity', {}).get('wireguard_port', 51820)
srv = wireguard_manager.get_server_config()
server_endpoint = srv.get('endpoint') or '<SERVER_IP>'
except Exception: except Exception:
pass pass
wg_config = ''
peer_private_key = peer.get('private_key', '')
if peer_private_key:
try:
internet_access = peer.get('internet_access', True)
allowed_ips = wireguard_manager.FULL_TUNNEL_IPS if internet_access else wireguard_manager.get_split_tunnel_ips()
wg_config = wireguard_manager.get_peer_config(
peer_name=peer_name,
peer_ip=peer_ip,
peer_private_key=peer_private_key,
server_endpoint=server_endpoint,
allowed_ips=allowed_ips,
)
except Exception:
pass
return jsonify({ return jsonify({
'username': peer_name,
'wireguard': { 'wireguard': {
'ip': peer_ip, 'ip': peer_ip,
'server_public_key': server_public_key, 'server_public_key': server_public_key,
'endpoint_port': wg_port, 'endpoint_port': wg_port,
'dns': '10.0.0.1', 'dns': _resolve_peer_dns(),
'config': wg_config,
}, },
'email': { 'email': {
'username': f'{peer_name}@{domain}', 'address': f'{peer_name}@{domain}',
'imap_host': f'mail.{domain}', 'smtp': {'host': f'mail.{domain}', 'port': 587},
'smtp_host': f'mail.{domain}', 'imap': {'host': f'mail.{domain}', 'port': 993},
'imap_port': 993,
'smtp_port': 587,
}, },
'caldav': { 'caldav': {
'url': f'http://radicale.{domain}:5232', 'url': f'http://calendar.{domain}',
'username': peer_name, 'username': peer_name,
}, },
'webdav': { 'files': {
'url': f'http://webdav.{domain}', 'url': f'http://files.{domain}',
'username': peer_name, 'username': peer_name,
}, },
}) })
+4
View File
@@ -189,6 +189,10 @@ http://api.{domain} {{
reverse_proxy cell-api:3000 reverse_proxy cell-api:3000
}} }}
http://webui.{domain} {{
reverse_proxy cell-webui:80
}}
# Catch-all for direct IP / localhost # Catch-all for direct IP / localhost
:80 {{ :80 {{
handle /api/* {{ handle /api/* {{
+11 -3
View File
@@ -150,13 +150,21 @@ class NetworkManager(BaseServiceManager):
return {'restarted': restarted, 'warnings': warnings} return {'restarted': restarted, 'warnings': warnings}
def _build_dns_records(self, cell_name: str, ip_range: str) -> List[Dict]: def _build_dns_records(self, cell_name: str, ip_range: str) -> List[Dict]:
"""Build the standard set of DNS A records for the given subnet.""" """Build the standard set of DNS A records for the given subnet.
All user-facing names resolve to the Caddy reverse proxy (caddy IP) so
the Host header is passed through and Caddy routes based on it.
Exception: calendar/files/mail/webdav use dedicated virtual IPs so that
iptables per-service firewall rules can target them by destination IP.
api and webui also go through Caddy they don't have their own VIPs and
their containers don't serve HTTP on port 80.
"""
import ip_utils import ip_utils
ips = ip_utils.get_service_ips(ip_range) ips = ip_utils.get_service_ips(ip_range)
return [ return [
{'name': cell_name, 'type': 'A', 'value': ips['caddy']}, {'name': cell_name, 'type': 'A', 'value': ips['caddy']},
{'name': 'api', 'type': 'A', 'value': ips['api']}, {'name': 'api', 'type': 'A', 'value': ips['caddy']},
{'name': 'webui', 'type': 'A', 'value': ips['webui']}, {'name': 'webui', 'type': 'A', 'value': ips['caddy']},
{'name': 'calendar', 'type': 'A', 'value': ips['vip_calendar']}, {'name': 'calendar', 'type': 'A', 'value': ips['vip_calendar']},
{'name': 'files', 'type': 'A', 'value': ips['vip_files']}, {'name': 'files', 'type': 'A', 'value': ips['vip_files']},
{'name': 'mail', 'type': 'A', 'value': ips['vip_mail']}, {'name': 'mail', 'type': 'A', 'value': ips['vip_mail']},
+39 -74
View File
@@ -1,92 +1,57 @@
# Personal Internet Cell - Caddy Configuration
# This serves as the main reverse proxy and TLS termination point
# Global settings
{ {
# Auto-generate certificates for .cell domains auto_https off
auto_https disable_redirects
} }
# Main cell domain - replace 'mycell' with your cell name # Main cell domain — no service-IP restriction needed
mycell.cell { http://pic0.dev, http://172.20.0.2:80 {
# TLS with internal CA
tls internal
# API endpoints
handle /api/* { handle /api/* {
reverse_proxy cell-api:3000 reverse_proxy cell-api:3000
} }
handle /calendar* {
# Web UI
handle / {
reverse_proxy cell-webui:80
}
# Email web interface
handle /mail {
reverse_proxy cell-mail:80
}
# Calendar and contacts
handle /calendar {
reverse_proxy cell-radicale:5232 reverse_proxy cell-radicale:5232
} }
handle /files* {
# File storage
handle /files {
reverse_proxy cell-webdav:80
}
# DNS management interface
handle /dns {
reverse_proxy cell-dns:8080
}
# RainLoop Webmail
handle_path /webmail/* {
reverse_proxy cell-rainloop:8888
}
# FileGator File Browser
handle /files-ui* {
reverse_proxy cell-filegator:8080 reverse_proxy cell-filegator:8080
} }
handle /webmail* {
reverse_proxy cell-rainloop:8888
}
handle {
reverse_proxy cell-webui:80
}
} }
# Peer cell domains (will be dynamically added) # Per-service virtual IPs — each gets its own IP so iptables can target them
# Example: bob.cell { http://calendar.dev, http://172.20.0.21:80 {
# reverse_proxy cell-wireguard:51820 reverse_proxy cell-radicale:5232
# } }
# Local development http://files.dev, http://172.20.0.22:80 {
localhost { reverse_proxy cell-filegator:8080
# API endpoints }
http://mail.dev, http://webmail.dev, http://172.20.0.23:80 {
reverse_proxy cell-rainloop:8888
}
http://webdav.dev, http://172.20.0.24:80 {
reverse_proxy cell-webdav:80
}
http://api.dev {
reverse_proxy cell-api:3000
}
http://webui.dev {
reverse_proxy cell-webui:80
}
# Catch-all for direct IP / localhost
:80 {
handle /api/* { handle /api/* {
reverse_proxy cell-api:3000 reverse_proxy cell-api:3000
} }
handle {
# Web UI
handle / {
reverse_proxy cell-webui:80 reverse_proxy cell-webui:80
} }
}
# Email web interface
handle /mail {
reverse_proxy cell-mail:80
}
# Calendar and contacts
handle /calendar {
reverse_proxy cell-radicale:5232
}
# File storage
handle /files {
reverse_proxy cell-webdav:80
}
# DNS management interface
handle /dns {
reverse_proxy cell-dns:8080
}
}
-4
View File
@@ -10,7 +10,3 @@ dev {
log log
} }
local.dev {
file /data/local.zone
log
}
+33 -1
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Wifi, ArrowDown, ArrowUp, Clock } from 'lucide-react'; import { Wifi, ArrowDown, ArrowUp, Clock, Calendar, FolderOpen, Mail, Globe } from 'lucide-react';
import { peerAPI } from '../services/api'; import { peerAPI } from '../services/api';
function formatBytes(bytes) { function formatBytes(bytes) {
@@ -114,6 +114,38 @@ export default function PeerDashboard() {
<div className="card"> <div className="card">
<h2 className="text-base font-semibold text-gray-900 mb-3">Quick Access</h2> <h2 className="text-base font-semibold text-gray-900 mb-3">Quick Access</h2>
{peer.service_urls && Object.keys(peer.service_urls).length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
{peer.service_urls.calendar && (
<a href={peer.service_urls.calendar} target="_blank" rel="noopener noreferrer"
className="flex flex-col items-center gap-1.5 p-3 rounded-lg border border-gray-200 hover:border-primary-400 hover:bg-primary-50 transition-colors text-sm text-gray-700 hover:text-primary-700">
<Calendar className="h-6 w-6" />
Calendar
</a>
)}
{peer.service_urls.files && (
<a href={peer.service_urls.files} target="_blank" rel="noopener noreferrer"
className="flex flex-col items-center gap-1.5 p-3 rounded-lg border border-gray-200 hover:border-primary-400 hover:bg-primary-50 transition-colors text-sm text-gray-700 hover:text-primary-700">
<FolderOpen className="h-6 w-6" />
Files
</a>
)}
{peer.service_urls.mail && (
<a href={peer.service_urls.mail} target="_blank" rel="noopener noreferrer"
className="flex flex-col items-center gap-1.5 p-3 rounded-lg border border-gray-200 hover:border-primary-400 hover:bg-primary-50 transition-colors text-sm text-gray-700 hover:text-primary-700">
<Mail className="h-6 w-6" />
Mail
</a>
)}
{peer.service_urls.webdav && (
<a href={peer.service_urls.webdav} target="_blank" rel="noopener noreferrer"
className="flex flex-col items-center gap-1.5 p-3 rounded-lg border border-gray-200 hover:border-primary-400 hover:bg-primary-50 transition-colors text-sm text-gray-700 hover:text-primary-700">
<Globe className="h-6 w-6" />
WebDAV
</a>
)}
</div>
) : null}
<Link <Link
to="/my-services" to="/my-services"
className="inline-flex items-center gap-2 btn btn-primary" className="inline-flex items-center gap-2 btn btn-primary"