Add subnet conflict validation for wireguard.address and ip_range changes

When a cell is connected to others, changing the local WireGuard address
or Docker ip_range to a subnet that overlaps a connected cell's vpn_subnet
would break routing. Both now return 409 with the conflicting cell name.

- wireguard.address: derive network from new address, check all connected
  cells' vpn_subnet for overlap (after existing format validation)
- ip_range: check all connected cells' vpn_subnet for overlap (after
  existing RFC-1918 validation)

Tests: 4 cases each (overlap → 409, no overlap → ok, no cells → ok,
format error still fires first → 400).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 10:00:58 -04:00
parent c658d2b16c
commit 8ee1d88e37
2 changed files with 106 additions and 1 deletions
+26 -1
View File
@@ -184,6 +184,18 @@ def update_config():
)}), 400
except ValueError as _e:
return jsonify({'error': f'Invalid ip_range: {_e}'}), 400
from app import cell_link_manager as _clm
for _link in _clm.list_connections():
try:
_cell_net = ipaddress.ip_network(_link['vpn_subnet'], strict=False)
if _net.overlaps(_cell_net):
return jsonify({'error': (
f"ip_range {str(_net)!r} overlaps connected cell "
f"'{_link['cell_name']}' ({_link['vpn_subnet']!r}) — "
f"use a non-overlapping range"
)}), 409
except Exception:
pass
_port_fields = {
'network': ['dns_port'],
@@ -221,9 +233,22 @@ def update_config():
if '/' not in str(_addr):
return jsonify({'error': 'wireguard.address must include a prefix length (e.g. 10.0.0.1/24)'}), 400
try:
ipaddress.ip_interface(_addr)
_iface = ipaddress.ip_interface(_addr)
except ValueError as _e:
return jsonify({'error': f'wireguard.address is not a valid IP/CIDR: {_e}'}), 400
_new_net = _iface.network
from app import cell_link_manager as _clm
for _link in _clm.list_connections():
try:
_cell_net = ipaddress.ip_network(_link['vpn_subnet'], strict=False)
if _new_net.overlaps(_cell_net):
return jsonify({'error': (
f"WireGuard subnet {str(_new_net)!r} overlaps connected cell "
f"'{_link['cell_name']}' ({_link['vpn_subnet']!r}) — "
f"use a non-overlapping address"
)}), 409
except Exception:
pass
old_identity = dict(config_manager.configs.get('_identity', {}))
old_svc_configs = {