feat: peer access config, DNS fix, real routing table, reinstall notifications

Peer creation/edit form now configures:
- Tunnel mode: full (0.0.0.0/0) or split (PIC only)
- Per-service access toggles (calendar, files, mail, webdav)
- Peer-to-peer communication toggle
- Optional calendar account creation
- Access capability badges in peer list

Bug fixes:
- DNS in client configs was 8.8.8.8 / 172.20.0.2 — now 172.20.0.3 (CoreDNS)
  This was why .cell domains didn't resolve on connected VPN peers
- get_peer_config API uses stored internet_access to set AllowedIPs
- New PUT /api/peers/<name> endpoint with config_changed detection
- POST /api/peers/<name>/clear-reinstall clears reinstall flag after download
- Routing page reads real host routes via /proc/1/net/route (pid: host)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 15:40:19 -04:00
parent 9d7d74f3f4
commit 8e41568964
4 changed files with 800 additions and 1029 deletions
+53 -2
View File
@@ -890,8 +890,8 @@ def get_peer_config():
# Look up peer details from registry if not supplied # Look up peer details from registry if not supplied
peer_ip = data.get('ip', '') peer_ip = data.get('ip', '')
peer_private_key = data.get('private_key', '') peer_private_key = data.get('private_key', '')
registered = peer_registry.get_peer(peer_name) if peer_name else {}
if peer_name and (not peer_ip or not peer_private_key): if peer_name and (not peer_ip or not peer_private_key):
registered = peer_registry.get_peer(peer_name)
if registered: if registered:
peer_ip = peer_ip or registered.get('ip', '') peer_ip = peer_ip or registered.get('ip', '')
peer_private_key = peer_private_key or registered.get('private_key', '') peer_private_key = peer_private_key or registered.get('private_key', '')
@@ -902,7 +902,12 @@ def get_peer_config():
srv = wireguard_manager.get_server_config() srv = wireguard_manager.get_server_config()
server_endpoint = srv.get('endpoint') or '<SERVER_IP>' server_endpoint = srv.get('endpoint') or '<SERVER_IP>'
# Determine AllowedIPs: explicit > peer's stored internet_access > default full tunnel
allowed_ips = data.get('allowed_ips') or None allowed_ips = data.get('allowed_ips') or None
if not allowed_ips and registered:
internet_access = registered.get('internet_access', True)
allowed_ips = wireguard_manager.FULL_TUNNEL_IPS if internet_access else wireguard_manager.SPLIT_TUNNEL_IPS
result = wireguard_manager.get_peer_config( result = wireguard_manager.get_peer_config(
peer_name=peer_name, peer_name=peer_name,
peer_ip=peer_ip, peer_ip=peer_ip,
@@ -980,7 +985,11 @@ def add_peer():
'server_endpoint': data.get('server_endpoint'), 'server_endpoint': data.get('server_endpoint'),
'allowed_ips': data.get('allowed_ips'), 'allowed_ips': data.get('allowed_ips'),
'persistent_keepalive': data.get('persistent_keepalive'), 'persistent_keepalive': data.get('persistent_keepalive'),
'description': data.get('description') 'description': data.get('description'),
'internet_access': data.get('internet_access', True),
'service_access': data.get('service_access', ['calendar', 'files', 'mail', 'webdav']),
'peer_access': data.get('peer_access', True),
'config_needs_reinstall': False,
} }
success = peer_registry.add_peer(peer_info) success = peer_registry.add_peer(peer_info)
@@ -993,6 +1002,48 @@ def add_peer():
logger.error(f"Error adding peer: {e}") logger.error(f"Error adding peer: {e}")
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@app.route('/api/peers/<peer_name>', methods=['PUT'])
def update_peer(peer_name):
"""Update peer settings. Marks config_needs_reinstall if VPN config changed."""
try:
data = request.get_json(silent=True) or {}
existing = peer_registry.get_peer(peer_name)
if not existing:
return jsonify({"error": "Peer not found"}), 404
# Detect changes that require client to reinstall tunnel config
config_changed = (
('internet_access' in data and data['internet_access'] != existing.get('internet_access', True)) or
('ip' in data and data['ip'] != existing.get('ip')) or
('persistent_keepalive' in data and data['persistent_keepalive'] != existing.get('persistent_keepalive'))
)
updates = {k: v for k, v in data.items()}
if config_changed:
updates['config_needs_reinstall'] = True
success = peer_registry.update_peer(peer_name, updates)
if success:
result = {"message": f"Peer {peer_name} updated", "config_changed": config_changed}
return jsonify(result)
else:
return jsonify({"error": "Update failed"}), 500
except Exception as e:
logger.error(f"Error updating peer {peer_name}: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/peers/<peer_name>/clear-reinstall', methods=['POST'])
def clear_peer_reinstall(peer_name):
"""Clear the config_needs_reinstall flag once user has downloaded new config."""
try:
peer_registry.clear_reinstall_flag(peer_name)
return jsonify({"message": "Reinstall flag cleared"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/peers/<peer_name>', methods=['DELETE']) @app.route('/api/peers/<peer_name>', methods=['DELETE'])
def remove_peer(peer_name): def remove_peer(peer_name):
"""Remove a peer.""" """Remove a peer."""
+21
View File
@@ -266,6 +266,27 @@ class PeerRegistry(BaseServiceManager):
self.logger.error(f"Error removing peer {name}: {e}") self.logger.error(f"Error removing peer {name}: {e}")
return False return False
def update_peer(self, name: str, fields: Dict[str, Any]) -> bool:
"""Update arbitrary fields on a peer."""
try:
with self.lock:
for peer in self.peers:
if peer.get('peer') == name:
peer.update(fields)
peer['updated_at'] = datetime.utcnow().isoformat()
self._save_peers()
self.logger.info(f"Updated peer {name}: {list(fields.keys())}")
return True
self.logger.warning(f"Peer {name} not found for update")
return False
except Exception as e:
self.logger.error(f"Error updating peer {name}: {e}")
return False
def clear_reinstall_flag(self, name: str) -> bool:
"""Clear the config_needs_reinstall flag after user downloads new config."""
return self.update_peer(name, {'config_needs_reinstall': False})
def update_peer_ip(self, name: str, new_ip: str) -> bool: def update_peer_ip(self, name: str, new_ip: str) -> bool:
"""Update peer IP address""" """Update peer IP address"""
try: try:
+573 -874
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -181,7 +181,7 @@ function WireGuard() {
return { public_key: '', endpoint: '<SERVER_IP>:51820' }; return { public_key: '', endpoint: '<SERVER_IP>:51820' };
}; };
const CELL_DNS = '172.20.0.2'; const CELL_DNS = '172.20.0.3';
const SPLIT_TUNNEL_IPS = '10.0.0.0/24, 172.20.0.0/16'; const SPLIT_TUNNEL_IPS = '10.0.0.0/24, 172.20.0.0/16';
const FULL_TUNNEL_IPS = '0.0.0.0/0, ::/0'; const FULL_TUNNEL_IPS = '0.0.0.0/0, ::/0';