fix: is_local_request rejects non-RFC1918 cell subnets; helper image hardcoded

Two bugs triggered when ip_range is set to a subnet outside 172.16.0.0/12
(e.g. 172.0.0.0/24):

1. is_local_request() used ip.is_private which returns False for 172.0.x.x,
   causing Caddy reverse-proxy requests to get 403 on the containers endpoint.
   Fix: also accept IPs in the configured cell-network subnet.

2. apply_pending_config() hardcoded 'pic_api:latest' as the helper container
   image. docker-compose v1 builds pic_api:latest (underscore) but compose v2+
   builds pic-api:latest (hyphen). On a v2 install the helper would fail to
   start silently, leaving the network unreconstructed after an ip_range change.
   Fix: read the actual image tag from cell-api's own container metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 16:15:58 -04:00
parent 2c11db6cc1
commit 60cf223293
+33 -23
View File
@@ -320,31 +320,35 @@ health_monitor_thread = threading.Thread(target=health_monitor_loop, daemon=True
health_monitor_thread.start()
def is_local_request():
# Allow requests from localhost, Docker networks, and internal IPs
remote_addr = request.remote_addr
forwarded_for = request.headers.get('X-Forwarded-For', '')
# Check direct remote address
if remote_addr in ('127.0.0.1', '::1', 'localhost'):
return True
# Check forwarded address (for reverse proxy scenarios)
if forwarded_for:
forwarded_ips = [ip.strip() for ip in forwarded_for.split(',')]
for ip in forwarded_ips:
if ip in ('127.0.0.1', '::1', 'localhost'):
return True
# Allow Docker internal networks (172.x.x.x, 192.168.x.x, 10.x.x.x)
if remote_addr:
def _allowed(addr):
if not addr:
return False
if addr in ('127.0.0.1', '::1', 'localhost'):
return True
try:
import ipaddress
ip = ipaddress.ip_address(remote_addr)
import ipaddress as _ipa
ip = _ipa.ip_address(addr)
if ip.is_private or ip.is_loopback:
return True
except:
# Also allow IPs in the configured cell-network, which may fall outside
# RFC-1918 (e.g. 172.0.0.0/24 is not in 172.16.0.0/12).
cell_net = config_manager.configs.get('_identity', {}).get(
'ip_range', os.environ.get('CELL_IP_RANGE', '172.20.0.0/16'))
if ip in _ipa.ip_network(cell_net, strict=False):
return True
except Exception:
pass
return False
if _allowed(remote_addr):
return True
if forwarded_for:
for addr in forwarded_for.split(','):
if _allowed(addr.strip()):
return True
return False
@app.route('/health', methods=['GET'])
@@ -692,13 +696,19 @@ def apply_pending_config():
if not pending.get('needs_restart'):
return jsonify({'message': 'No pending changes to apply'})
# Get project working dir from our own container labels (set by docker-compose)
# Get project working dir and image name from our own container labels
project_dir = '/home/roof/pic'
api_image = 'pic_api:latest' # fallback (docker-compose v1 naming)
try:
import docker as _docker_sdk
_client = _docker_sdk.from_env()
_self = _client.containers.get('cell-api')
project_dir = _self.labels.get('com.docker.compose.project.working_dir', project_dir)
# Use the actual image tag so the helper works regardless of compose version
# (docker-compose v1 builds pic_api:latest, compose v2+ builds pic-api:latest)
tags = _self.image.tags
if tags:
api_image = tags[0]
except Exception:
pass
@@ -717,8 +727,8 @@ def apply_pending_config():
if '*' in containers:
# All-services restart: `docker compose down` or `up -d` may stop/recreate the
# API container itself, killing this background thread mid-operation.
# Spawn an independent helper container using pic_api:latest that has docker CLI
# and survives cell-api being stopped/recreated.
# Spawn an independent helper container (same image as cell-api) that has docker
# CLI and survives cell-api being stopped/recreated.
if needs_network_recreate:
helper_script = (
f'sleep 2'
@@ -741,7 +751,7 @@ def apply_pending_config():
'-v', '/var/run/docker.sock:/var/run/docker.sock',
'-v', f'{project_dir}:{project_dir}',
'--entrypoint', 'sh',
'pic_api:latest',
api_image,
'-c', helper_script],
close_fds=True,
stdout=_subprocess.DEVNULL,