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:
2026-04-21 07:47:19 -04:00
parent 5c89687fab
commit 3912452fd6
5 changed files with 198 additions and 41 deletions
+54 -6
View File
@@ -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]: