fix: prevent test runs from corrupting live WG state; sync wg0.conf on IP change

Three fixes:

1. Extend the docker-exec safety guard in wireguard_manager to also check
   for 'wg_confs' in the config path.  When running unit tests on the host
   the API uses /app/config/wireguard/wg0.conf (no wg_confs subdir), so the
   old '/tmp/' | 'pytest' check didn't fire — _syncconf and friends were
   executing live 'docker exec cell-wireguard wg set' calls against the
   running container, removing real VPN peers that didn't appear in the
   test config.  The wg_confs subdir only exists inside the container mount,
   so its presence reliably gates live calls.

2. Fix get_split_tunnel_ips() wrong path: self.data_dir + 'api/cell_links.json'
   → self.data_dir + 'cell_links.json'.  The extra 'api/' segment produced
   /app/data/api/cell_links.json inside the container instead of the real
   /app/data/cell_links.json, so connected cells were silently excluded from
   split-tunnel CIDRs.

3. update_peer_ip_registry and ip_update now also call
   wireguard_manager.update_peer_ip so wg0.conf AllowedIPs stay in sync when
   a peer's VPN IP changes at runtime (previously only peers.json was updated).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 07:45:28 -04:00
parent 99c1d9cd92
commit 0e16d6968a
3 changed files with 26 additions and 12 deletions
+8 -8
View File
@@ -296,7 +296,7 @@ class WireGuardManager(BaseServiceManager):
Docker bridge subnet would cause routing conflicts when cells share the same range.
"""
local_net = self._get_configured_network()
cell_links_file = os.path.join(self.data_dir, 'api', 'cell_links.json')
cell_links_file = os.path.join(self.data_dir, 'cell_links.json')
cell_nets = []
try:
with open(cell_links_file) as f:
@@ -449,8 +449,8 @@ class WireGuardManager(BaseServiceManager):
"""
import subprocess, re
real_conf = self._config_file()
if '/tmp/' in real_conf or 'pytest' in real_conf:
logger.debug('_syncconf: skipping — config path looks like a test dir')
if '/tmp/' in real_conf or 'pytest' in real_conf or 'wg_confs' not in real_conf:
logger.debug('_syncconf: skipping — not running inside container')
return
try:
# Parse desired peers from config file
@@ -634,7 +634,7 @@ class WireGuardManager(BaseServiceManager):
wg-quick would do this automatically, but we manage WG live via 'wg set'.
"""
real_conf = self._config_file()
if '/tmp/' in real_conf or 'pytest' in real_conf:
if '/tmp/' in real_conf or 'pytest' in real_conf or 'wg_confs' not in real_conf:
return
try:
subprocess.run(
@@ -653,7 +653,7 @@ class WireGuardManager(BaseServiceManager):
are ephemeral; only the WG peer config in wg0.conf persists).
"""
real_conf = self._config_file()
if '/tmp/' in real_conf or 'pytest' in real_conf:
if '/tmp/' in real_conf or 'pytest' in real_conf or 'wg_confs' not in real_conf:
return
try:
content = self._read_config()
@@ -715,7 +715,7 @@ class WireGuardManager(BaseServiceManager):
treated as success.
"""
real_conf = self._config_file()
if '/tmp/' in real_conf or 'pytest' in real_conf:
if '/tmp/' in real_conf or 'pytest' in real_conf or 'wg_confs' not in real_conf:
return True
try:
def _wg(cmd):
@@ -738,7 +738,7 @@ class WireGuardManager(BaseServiceManager):
def remove_peer_route_via(self, peer_ip: str, table: int = 100) -> None:
"""Remove the ip rule for peer_ip added by apply_peer_route_via. Non-fatal."""
real_conf = self._config_file()
if '/tmp/' in real_conf or 'pytest' in real_conf:
if '/tmp/' in real_conf or 'pytest' in real_conf or 'wg_confs' not in real_conf:
return
try:
subprocess.run(
@@ -836,7 +836,7 @@ class WireGuardManager(BaseServiceManager):
def _apply_peer_allowed_ips_live(self, public_key: str, new_ips: str) -> None:
"""Apply AllowedIPs for one peer via wg set (no spaces — wg rejects them)."""
real_conf = self._config_file()
if '/tmp/' in real_conf or 'pytest' in real_conf:
if '/tmp/' in real_conf or 'pytest' in real_conf or 'wg_confs' not in real_conf:
return
try:
ips = new_ips.replace(' ', '')