1bb8a5eb59
Unit Tests / test (push) Successful in 9m50s
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>
299 lines
12 KiB
Python
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
|