Files
pic/api/routes/wireguard.py
T
roof 1bb8a5eb59
Unit Tests / test (push) Successful in 9m50s
fix: advertise WireGuard endpoint by domain, and reach linked cells over HTTPS
Three related cell-link/peer-config fixes (the peer and cell endpoints were
showing the raw external IP, which confused public-vs-internal addressing):

1. Peer WireGuard configs now embed the cell's effective domain (DDNS/ACME
   modes) instead of the detected external IP, via the new
   WireGuardManager.get_advertised_endpoint(). A name that resolves to the
   public IP survives IP changes and lets the datacenter forward each cell's
   WG port to the right host. LAN mode still falls back to the IP; an admin
   wireguard_endpoint override still wins.

2. Cell invites advertise <effective-domain>:<this cell's WG port> (was the
   external IP + a default/possibly-wrong port), so a remote cell pairs to the
   right host and port over the public path.

3. Cross-cell peer-sync no longer targets http://<ip>:3000 (the API binds
   127.0.0.1 and is unreachable across cells). It targets the remote's Caddy on
   HTTPS/443 — which the WireGuard server already DNATs over the tunnel — and the
   initial pre-tunnel invite push goes to https://<endpoint-host>/... ; legacy
   http://<ip>:3000 link URLs migrate to https on load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 04:21:16 -04:00

299 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.
Prefers the cell's public domain (DDNS/ACME modes) or an admin override over
the raw external IP, so a peer config points at a name that resolves to the
cell rather than a bare IP. See WireGuardManager.get_advertised_endpoint.
"""
return wireguard_manager.get_advertised_endpoint(config_manager) 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()
configured_port = wireguard_manager._get_configured_port()
listening_port = wireguard_manager._kernel_listening_port()
return jsonify({
'port_open': port_open,
'port': configured_port,
'listening_port': listening_port,
'port_mismatch': (
listening_port is not None and listening_port != configured_port
),
})
except Exception as e:
return jsonify({"error": str(e)}), 500