From 3ce45a8911bc1d5c1cd2d9f6208b695ee2deeaa3 Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Fri, 24 Apr 2026 04:54:47 -0400 Subject: [PATCH] fix: get_live_service_vips uses config API, require CIDR prefix for ip_range - tests/integration/conftest.py: get_live_service_vips() now reads from the config API's service_ips field instead of docker exec. The docker exec approach spawns a fresh Python process that imports firewall_manager with its hardcoded initial SERVICE_IPS, ignoring any update_service_ips() calls made at runtime. The config API always computes VIPs from the current ip_range, so it matches what the running app actually uses when writing iptables rules. - api/app.py: reject ip_range values without a CIDR prefix (e.g. '10.0.0.1') with a 400. Bare IPs are parsed as /32 by ipaddress.ip_network(strict=False), which shifts all VIP offsets and produces unusable Docker subnet configs. - tests/integration/test_config_api.py: update bare-ip test to expect 400 now that the API enforces the prefix requirement. Co-Authored-By: Claude Sonnet 4.6 --- api/app.py | 5 ++++- tests/integration/conftest.py | 20 +++++++------------- tests/integration/test_config_api.py | 8 ++++---- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/api/app.py b/api/app.py index 5165026..9cc4710 100644 --- a/api/app.py +++ b/api/app.py @@ -447,7 +447,10 @@ def update_config(): _ipa.ip_network('192.168.0.0/16'), ] try: - _net = _ipa.ip_network(identity_updates['ip_range'], strict=False) + _raw = str(identity_updates['ip_range']) + if '/' not in _raw: + return jsonify({'error': 'ip_range must include a CIDR prefix (e.g. 172.20.0.0/16)'}), 400 + _net = _ipa.ip_network(_raw, strict=False) if not any(_net.subnet_of(r) for r in _rfc1918): return jsonify({'error': ( 'ip_range must be within an RFC-1918 private range ' diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index cc33d3c..6ce3cb4 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -73,20 +73,14 @@ def peer_rules(peer_ip: str) -> list[str]: def get_live_service_vips() -> dict: """ - Read SERVICE_IPS directly from the running API container. - More reliable than the config API since SERVICE_IPS may not match ip_range - when the container was built before an ip_range change. + Read virtual IPs from the config API. + + The config API computes service_ips from the current ip_range at request time, + so it always matches what the running firewall_manager will use when applying + peer rules. Using docker exec on the API container is NOT reliable because + it spawns a fresh Python process that imports firewall_manager with its initial + hardcoded SERVICE_IPS, ignoring any update_service_ips() calls made at runtime. """ - import json - result = subprocess.run( - ['docker', 'exec', 'cell-api', 'python3', '-c', - 'import sys; sys.path.insert(0,"/app/api");' - ' from firewall_manager import SERVICE_IPS; import json; print(json.dumps(SERVICE_IPS))'], - capture_output=True, text=True, timeout=10, - ) - if result.returncode == 0 and result.stdout.strip(): - return json.loads(result.stdout) - # Fallback: derive from config API cfg = requests.get(f"{API_BASE}/api/config").json() sips = cfg.get('service_ips', {}) return { diff --git a/tests/integration/test_config_api.py b/tests/integration/test_config_api.py index 29e7eb8..ff7249e 100644 --- a/tests/integration/test_config_api.py +++ b/tests/integration/test_config_api.py @@ -176,11 +176,11 @@ class TestPutConfigValidation: r = put('/api/config', json={'ip_range': 'not-an-ip'}) assert r.status_code == 400 - def test_put_config_ip_range_bare_ip_behavior(self): - # Bare IP is interpreted as /32 — the API may accept or reject it, - # but it must not crash (no 500). + def test_put_config_ip_range_bare_ip_returns_400(self): + # Bare IP without CIDR prefix must be rejected — /32 networks are + # accepted by Python but useless as a Docker subnet. r = put('/api/config', json={'ip_range': '10.0.0.1'}) - assert r.status_code in (200, 400) + assert r.status_code == 400 def test_put_config_calendar_port_zero_returns_400(self): r = put('/api/config', json={'calendar': {'port': 0}})