Files
pic/api/routes/wireguard.py
T
roof c696ca9ef6
Unit Tests / test (push) Successful in 7m32s
fix: DNS split-horizon in DDNS mode, service access filter, health check, verbosity persistence
- DNS (critical): add _configured_dns_params() that returns (primary_domain,
  split_horizon_zones) from config_manager so all apply_all_dns_rules() callers
  pass the correct primary zone (e.g. 'pic.ngo') and split-horizon list
  (e.g. ['pic1.pic.ngo']) instead of the FQDN as the primary — fixes
  DNS_PROBE_FINISHED_BAD_CONFIG for all external domains when on VPN

- firewall_manager: add split_horizon_zones param to apply_all_dns_rules()
  and forward it to generate_corefile()

- Peers: filter service_access list to installed services only; peers.py
  derives valid services from config_manager.get_installed_services() with
  the email→mail ID mapping; Peers.jsx fetches from /api/store/installed
  and filters the checkboxes and defaults accordingly

- Health check: fix file_manager→'files' ID mapping so files service health
  is checked when installed (was silently skipped due to 'file' vs 'files')

- Verbosity persistence: move log_levels.json from non-mounted
  /app/api/config/ to CONFIG_DIR (/app/config/) which maps to config/api/
  on the host; both load (managers.py) and save (routes/services.py) updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 13:05:58 -04:00

294 lines
12 KiB
Python

import logging
import ipaddress
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('wireguard', __name__)
def _effective_endpoint(wireguard_manager, config_manager) -> str:
"""Return the WireGuard endpoint to embed in peer configs.
Uses wireguard_endpoint from identity config when set (admin override),
falling back to get_external_ip() detection.
"""
srv = wireguard_manager.get_server_config()
override = (config_manager.get_identity().get('wireguard_endpoint') or '').strip()
if override:
port = srv.get('port', 51820)
return override if ':' in override else f'{override}:{port}'
return srv.get('endpoint') or '<SERVER_IP>'
@bp.route('/api/wireguard/keys', methods=['GET'])
def get_wireguard_keys():
try:
from app import wireguard_manager
keys = wireguard_manager.get_keys()
return jsonify({
'public_key': keys.get('public_key', ''),
'has_private_key': bool(keys.get('private_key')),
})
except Exception as e:
logger.error(f"Error getting WireGuard keys: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/keys/peer', methods=['POST'])
def generate_peer_keys():
try:
from app import wireguard_manager
data = request.get_json(silent=True) or {}
name = data.get('name') or data.get('peer_name')
if not name:
return jsonify({"error": "Missing peer name"}), 400
return jsonify(wireguard_manager.generate_peer_keys(name))
except Exception as e:
logger.error(f"Error generating peer keys: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/config', methods=['GET'])
def get_wireguard_config():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_config())
except Exception as e:
logger.error(f"Error getting WireGuard config: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers', methods=['GET'])
def get_wireguard_peers():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_peers())
except Exception as e:
logger.error(f"Error getting WireGuard peers: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers', methods=['POST'])
def add_wireguard_peer():
try:
from app import wireguard_manager
data = request.get_json(silent=True) or {}
result = wireguard_manager.add_peer(
name=data.get('name', ''),
public_key=data.get('public_key', ''),
endpoint_ip=data.get('endpoint', data.get('endpoint_ip', '')),
allowed_ips=data.get('allowed_ips', ''),
persistent_keepalive=data.get('persistent_keepalive', 25)
)
return jsonify({"success": result})
except Exception as e:
logger.error(f"Error adding WireGuard peer: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers', methods=['DELETE'])
def remove_wireguard_peer():
try:
from app import wireguard_manager
data = request.get_json(silent=True) or {}
public_key = data.get('public_key') or data.get('name', '')
return jsonify({"success": wireguard_manager.remove_peer(public_key)})
except Exception as e:
logger.error(f"Error removing WireGuard peer: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/status', methods=['GET'])
def get_wireguard_status():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_status())
except Exception as e:
logger.error(f"Error getting WireGuard status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/connectivity', methods=['POST'])
def test_wireguard_connectivity():
try:
from app import wireguard_manager
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
return jsonify(wireguard_manager.test_connectivity(data))
except Exception as e:
logger.error(f"Error testing WireGuard connectivity: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers/ip', methods=['PUT'])
def update_peer_ip():
try:
from app import wireguard_manager
data = request.get_json(silent=True) or {}
result = wireguard_manager.update_peer_ip(
data.get('public_key', data.get('peer', '')),
data.get('ip', '')
)
return jsonify({"success": result})
except Exception as e:
logger.error(f"Error updating peer IP: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers/status', methods=['POST'])
def get_peer_status():
try:
from app import wireguard_manager
data = request.get_json(silent=True) or {}
public_key = data.get('public_key', '')
if not public_key:
return jsonify({"error": "Missing public_key"}), 400
return jsonify(wireguard_manager.get_peer_status(public_key))
except Exception as e:
logger.error(f"Error getting peer status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers/statuses', methods=['GET'])
def get_all_peer_statuses():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_all_peer_statuses())
except Exception as e:
logger.error(f"Error getting peer statuses: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/network/setup', methods=['POST'])
def setup_network():
try:
from app import wireguard_manager
success = wireguard_manager.setup_network_configuration()
if success:
return jsonify({"message": "Network configuration setup completed successfully"})
return jsonify({"error": "Failed to setup network configuration"}), 500
except Exception as e:
logger.error(f"Error setting up network configuration: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/network/status', methods=['GET'])
def get_network_status():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_network_status())
except Exception as e:
logger.error(f"Error getting network status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers/config', methods=['POST'])
def get_peer_config():
try:
from app import wireguard_manager, peer_registry
data = request.get_json(silent=True) or {}
peer_name = data.get('name', data.get('peer', ''))
peer_ip = data.get('ip', '')
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 registered:
peer_ip = peer_ip or registered.get('ip', '')
peer_private_key = peer_private_key or registered.get('private_key', '')
server_endpoint = data.get('server_endpoint', '')
if not server_endpoint:
from app import config_manager
server_endpoint = _effective_endpoint(wireguard_manager, config_manager)
allowed_ips = data.get('allowed_ips') or None
if not allowed_ips and registered:
internet_access = registered.get('internet_access', True)
route_via = registered.get('route_via')
# Full tunnel when internet is allowed OR when route_via is set
# (route_via exits via a remote cell — all traffic must go through the tunnel)
use_full = internet_access or bool(route_via)
allowed_ips = wireguard_manager.FULL_TUNNEL_IPS if use_full else wireguard_manager.get_split_tunnel_ips()
result = 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,
)
return jsonify({"config": result})
except Exception as e:
logger.error(f"Error getting peer config: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/server-config', methods=['GET'])
def get_server_config():
try:
from app import wireguard_manager, config_manager
cfg = wireguard_manager.get_server_config()
cfg['endpoint_override'] = (config_manager.get_identity().get('wireguard_endpoint') or '').strip()
cfg['effective_endpoint'] = _effective_endpoint(wireguard_manager, config_manager)
return jsonify(cfg)
except Exception as e:
logger.error(f"Error getting server config: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/endpoint', methods=['GET'])
def get_wireguard_endpoint():
try:
from app import wireguard_manager, config_manager
return jsonify({
'endpoint_override': (config_manager.get_identity().get('wireguard_endpoint') or '').strip(),
'detected_endpoint': wireguard_manager.get_server_config().get('endpoint'),
'effective_endpoint': _effective_endpoint(wireguard_manager, config_manager),
})
except Exception as e:
logger.error(f"Error getting wireguard endpoint: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/endpoint', methods=['PUT'])
def set_wireguard_endpoint():
try:
from app import config_manager
data = request.get_json(silent=True) or {}
override = (data.get('endpoint_override') or '').strip()
config_manager.set_identity_field('wireguard_endpoint', override)
return jsonify({'endpoint_override': override, 'ok': True})
except Exception as e:
logger.error(f"Error setting wireguard endpoint: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/refresh-ip', methods=['GET', 'POST'])
def refresh_external_ip():
try:
from app import wireguard_manager
ip = wireguard_manager.get_external_ip(force_refresh=True)
port = wireguard_manager._get_configured_port()
return jsonify({
'external_ip': ip,
'port': port,
'endpoint': f'{ip}:{port}' if ip else None,
})
except Exception as e:
logger.error(f"Error refreshing external IP: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/apply-enforcement', methods=['POST'])
def apply_wireguard_enforcement():
try:
from app import (peer_registry, wireguard_manager, firewall_manager,
cell_link_manager, _configured_dns_params, COREFILE_PATH)
peers = peer_registry.list_peers()
try:
_wg_addr = wireguard_manager._get_configured_address()
_wg_subnet = str(ipaddress.ip_network(_wg_addr, strict=False)) if _wg_addr else '10.0.0.0/24'
except Exception:
_wg_subnet = '10.0.0.0/24'
_cell_links = cell_link_manager.list_connections()
_cell_subnets = [l['vpn_subnet'] for l in _cell_links if l.get('vpn_subnet')]
firewall_manager.apply_all_peer_rules(peers, wg_subnet=_wg_subnet, cell_subnets=_cell_subnets)
_dns_primary, _dns_szones = _configured_dns_params()
firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH, _dns_primary,
cell_links=_cell_links,
split_horizon_zones=_dns_szones)
return jsonify({'ok': True, 'peers': len(peers)})
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/wireguard/check-port', methods=['GET', 'POST'])
def check_wireguard_port():
try:
from app import wireguard_manager
port_open = wireguard_manager.check_port_open()
return jsonify({'port_open': port_open, 'port': wireguard_manager._get_configured_port()})
except Exception as e:
return jsonify({"error": str(e)}), 500