fix: wireguard port/subnet/domain propagate to peer configs and new peer IPs
Backend: - wireguard_manager: _get_configured_port/address/network() read from wg0.conf instead of module-level constants; get_split_tunnel_ips() derives VPN network from configured Address; get_server_config() returns configured port, dns_ip, split_tunnel_ips, vpn_network - add_peer() and get_peer_config() use configured port (not hardcoded 51820) - _next_peer_ip() derives subnet from wireguard_manager._get_configured_address() so new peers are allocated IPs from the correct VPN range after address change - refresh-ip and check-port API endpoints return configured port, not 51820 - PUT /api/config: when wireguard port/address changes, all peers are marked config_needs_reinstall so users know to re-download tunnel configs - get_peer_config endpoint: uses configured split tunnel IPs (not hardcoded) Frontend: - Peers.jsx: SERVICES domains use live domain from ConfigContext; generateConfig() uses serverConf.dns_ip and serverConf.split_tunnel_ips; vpn_network shown in peer-access description; DNS hint uses live domain; server config loaded at mount time so it is available without re-fetching on every peer action; handleUpdatePeer uses /32 for server-side AllowedIPs (was incorrectly using full/split tunnel CIDRs which the backend rejects) - WireGuard.jsx: generateWireGuardConfig() uses serverConfig.dns_ip, split_tunnel_ips from server-config API; split-tunnel description shows live IPs Tests: 9 new tests in TestWireGuardConfigReads verify all config reads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+19
-9
@@ -434,6 +434,13 @@ def update_config():
|
||||
'service': service,
|
||||
'config': config
|
||||
})
|
||||
# VPN port or subnet change → all peer client configs are stale
|
||||
if service == 'wireguard' and ('port' in config or 'address' in config):
|
||||
for p in peer_registry.list_peers():
|
||||
peer_registry.update_peer(p['peer'], {'config_needs_reinstall': True})
|
||||
n = len(peer_registry.list_peers())
|
||||
if n:
|
||||
all_warnings.append(f'WireGuard endpoint changed — {n} peer(s) must reinstall VPN config')
|
||||
|
||||
# Apply cell identity domain to network and email services
|
||||
if identity_updates.get('domain'):
|
||||
@@ -1028,7 +1035,7 @@ def get_peer_config():
|
||||
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
|
||||
allowed_ips = wireguard_manager.FULL_TUNNEL_IPS if internet_access else wireguard_manager.get_split_tunnel_ips()
|
||||
|
||||
result = wireguard_manager.get_peer_config(
|
||||
peer_name=peer_name,
|
||||
@@ -1055,10 +1062,11 @@ def get_server_config():
|
||||
def refresh_external_ip():
|
||||
try:
|
||||
ip = wireguard_manager.get_external_ip(force_refresh=True)
|
||||
port = wireguard_manager._get_configured_port()
|
||||
return jsonify({
|
||||
'external_ip': ip,
|
||||
'port': 51820,
|
||||
'endpoint': f'{ip}:51820' if ip else None,
|
||||
'port': port,
|
||||
'endpoint': f'{ip}:{port}' if ip else None,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing external IP: {e}")
|
||||
@@ -1079,7 +1087,7 @@ def apply_wireguard_enforcement():
|
||||
def check_wireguard_port():
|
||||
try:
|
||||
port_open = wireguard_manager.check_port_open()
|
||||
return jsonify({'port_open': port_open, 'port': 51820})
|
||||
return jsonify({'port_open': port_open, 'port': wireguard_manager._get_configured_port()})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -1095,17 +1103,19 @@ def get_peers():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
def _next_peer_ip() -> str:
|
||||
"""Auto-assign the next free 10.0.0.x address (starts at .2, skips .1 = server)."""
|
||||
"""Auto-assign the next free host address from the configured VPN subnet."""
|
||||
import ipaddress
|
||||
server_addr = wireguard_manager._get_configured_address() # e.g. '10.0.0.1/24'
|
||||
network = ipaddress.ip_network(server_addr, strict=False)
|
||||
server_ip = str(ipaddress.ip_interface(server_addr).ip)
|
||||
used = {p.get('ip', '').split('/')[0] for p in peer_registry.list_peers()}
|
||||
network = ipaddress.ip_network('10.0.0.0/24')
|
||||
for host in network.hosts():
|
||||
ip = str(host)
|
||||
if ip == '10.0.0.1':
|
||||
continue # server address
|
||||
if ip == server_ip:
|
||||
continue
|
||||
if ip not in used:
|
||||
return ip
|
||||
raise ValueError('No free IPs left in 10.0.0.0/24')
|
||||
raise ValueError(f'No free IPs left in {network}')
|
||||
|
||||
|
||||
@app.route('/api/peers', methods=['POST'])
|
||||
|
||||
@@ -158,6 +158,49 @@ class WireGuardManager(BaseServiceManager):
|
||||
f.write(content)
|
||||
self._syncconf()
|
||||
|
||||
# ── Config value readers (always read from wg0.conf, never hardcode) ─────
|
||||
|
||||
def _read_iface_field(self, key: str) -> Optional[str]:
|
||||
"""Return the value of a field from the [Interface] section of wg0.conf."""
|
||||
cf = self._config_file()
|
||||
if not os.path.exists(cf):
|
||||
return None
|
||||
with open(cf) as f:
|
||||
in_iface = False
|
||||
for line in f:
|
||||
stripped = line.strip()
|
||||
if stripped == '[Interface]':
|
||||
in_iface = True
|
||||
elif stripped.startswith('[') and stripped.endswith(']'):
|
||||
in_iface = False
|
||||
elif in_iface and '=' in stripped:
|
||||
k, _, v = stripped.partition('=')
|
||||
if k.strip() == key:
|
||||
return v.strip()
|
||||
return None
|
||||
|
||||
def _get_configured_port(self) -> int:
|
||||
val = self._read_iface_field('ListenPort')
|
||||
try:
|
||||
return int(val) if val else DEFAULT_PORT
|
||||
except (ValueError, TypeError):
|
||||
return DEFAULT_PORT
|
||||
|
||||
def _get_configured_address(self) -> str:
|
||||
return self._read_iface_field('Address') or SERVER_ADDRESS
|
||||
|
||||
def _get_configured_network(self) -> str:
|
||||
import ipaddress
|
||||
addr = self._get_configured_address()
|
||||
try:
|
||||
return str(ipaddress.ip_network(addr, strict=False))
|
||||
except Exception:
|
||||
return SERVER_NETWORK
|
||||
|
||||
def get_split_tunnel_ips(self) -> str:
|
||||
"""Return split-tunnel AllowedIPs: VPN subnet + Docker bridge."""
|
||||
return f'{self._get_configured_network()}, 172.20.0.0/16'
|
||||
|
||||
def apply_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update wg0.conf interface fields and restart cell-wireguard."""
|
||||
restarted = []
|
||||
@@ -304,7 +347,7 @@ class WireGuardManager(BaseServiceManager):
|
||||
f'PersistentKeepalive = {persistent_keepalive}\n'
|
||||
)
|
||||
if endpoint_ip:
|
||||
peer_block += f'Endpoint = {endpoint_ip}:{DEFAULT_PORT}\n'
|
||||
peer_block += f'Endpoint = {endpoint_ip}:{self._get_configured_port()}\n'
|
||||
self._write_config(content + peer_block)
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -376,7 +419,7 @@ class WireGuardManager(BaseServiceManager):
|
||||
self._write_config('\n'.join(new_lines))
|
||||
return True
|
||||
|
||||
SPLIT_TUNNEL_IPS = '10.0.0.0/24, 172.20.0.0/16'
|
||||
SPLIT_TUNNEL_IPS = '10.0.0.0/24, 172.20.0.0/16' # legacy fallback; use get_split_tunnel_ips()
|
||||
FULL_TUNNEL_IPS = '0.0.0.0/0, ::/0'
|
||||
|
||||
def get_peer_config(self, peer_name: str, peer_ip: str,
|
||||
@@ -388,7 +431,8 @@ class WireGuardManager(BaseServiceManager):
|
||||
allowed_ips = self.FULL_TUNNEL_IPS
|
||||
server_keys = self.get_keys()
|
||||
peer_dns = _resolve_peer_dns()
|
||||
endpoint = server_endpoint if ':' in server_endpoint else f'{server_endpoint}:{DEFAULT_PORT}'
|
||||
port = self._get_configured_port()
|
||||
endpoint = server_endpoint if ':' in server_endpoint else f'{server_endpoint}:{port}'
|
||||
addr = peer_ip if '/' in peer_ip else f'{peer_ip}/32'
|
||||
return (
|
||||
f'[Interface]\n'
|
||||
@@ -468,16 +512,20 @@ class WireGuardManager(BaseServiceManager):
|
||||
return False
|
||||
|
||||
def get_server_config(self) -> Dict[str, Any]:
|
||||
"""Return server public key, external IP, endpoint, and port status."""
|
||||
"""Return server public key, external IP, endpoint, port, and tunnel info."""
|
||||
keys = self.get_keys()
|
||||
external_ip = self.get_external_ip()
|
||||
endpoint = f'{external_ip}:{DEFAULT_PORT}' if external_ip else None
|
||||
port = self._get_configured_port()
|
||||
endpoint = f'{external_ip}:{port}' if external_ip else None
|
||||
return {
|
||||
'public_key': keys['public_key'],
|
||||
'external_ip': external_ip,
|
||||
'endpoint': endpoint,
|
||||
'port': DEFAULT_PORT,
|
||||
'port': port,
|
||||
'port_open': None,
|
||||
'dns_ip': _resolve_peer_dns(),
|
||||
'split_tunnel_ips': self.get_split_tunnel_ips(),
|
||||
'vpn_network': self._get_configured_network(),
|
||||
}
|
||||
|
||||
def get_peer_status(self, public_key: str) -> Dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user