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:
+33
-23
@@ -320,31 +320,35 @@ health_monitor_thread = threading.Thread(target=health_monitor_loop, daemon=True
|
|||||||
health_monitor_thread.start()
|
health_monitor_thread.start()
|
||||||
|
|
||||||
def is_local_request():
|
def is_local_request():
|
||||||
# Allow requests from localhost, Docker networks, and internal IPs
|
|
||||||
remote_addr = request.remote_addr
|
remote_addr = request.remote_addr
|
||||||
forwarded_for = request.headers.get('X-Forwarded-For', '')
|
forwarded_for = request.headers.get('X-Forwarded-For', '')
|
||||||
|
|
||||||
# Check direct remote address
|
def _allowed(addr):
|
||||||
if remote_addr in ('127.0.0.1', '::1', 'localhost'):
|
if not addr:
|
||||||
return True
|
return False
|
||||||
|
if addr in ('127.0.0.1', '::1', 'localhost'):
|
||||||
# Check forwarded address (for reverse proxy scenarios)
|
return True
|
||||||
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:
|
|
||||||
try:
|
try:
|
||||||
import ipaddress
|
import ipaddress as _ipa
|
||||||
ip = ipaddress.ip_address(remote_addr)
|
ip = _ipa.ip_address(addr)
|
||||||
if ip.is_private or ip.is_loopback:
|
if ip.is_private or ip.is_loopback:
|
||||||
return True
|
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
|
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
|
return False
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
@app.route('/health', methods=['GET'])
|
||||||
@@ -692,13 +696,19 @@ def apply_pending_config():
|
|||||||
if not pending.get('needs_restart'):
|
if not pending.get('needs_restart'):
|
||||||
return jsonify({'message': 'No pending changes to apply'})
|
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'
|
project_dir = '/home/roof/pic'
|
||||||
|
api_image = 'pic_api:latest' # fallback (docker-compose v1 naming)
|
||||||
try:
|
try:
|
||||||
import docker as _docker_sdk
|
import docker as _docker_sdk
|
||||||
_client = _docker_sdk.from_env()
|
_client = _docker_sdk.from_env()
|
||||||
_self = _client.containers.get('cell-api')
|
_self = _client.containers.get('cell-api')
|
||||||
project_dir = _self.labels.get('com.docker.compose.project.working_dir', project_dir)
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -717,8 +727,8 @@ def apply_pending_config():
|
|||||||
if '*' in containers:
|
if '*' in containers:
|
||||||
# All-services restart: `docker compose down` or `up -d` may stop/recreate the
|
# All-services restart: `docker compose down` or `up -d` may stop/recreate the
|
||||||
# API container itself, killing this background thread mid-operation.
|
# API container itself, killing this background thread mid-operation.
|
||||||
# Spawn an independent helper container using pic_api:latest that has docker CLI
|
# Spawn an independent helper container (same image as cell-api) that has docker
|
||||||
# and survives cell-api being stopped/recreated.
|
# CLI and survives cell-api being stopped/recreated.
|
||||||
if needs_network_recreate:
|
if needs_network_recreate:
|
||||||
helper_script = (
|
helper_script = (
|
||||||
f'sleep 2'
|
f'sleep 2'
|
||||||
@@ -741,7 +751,7 @@ def apply_pending_config():
|
|||||||
'-v', '/var/run/docker.sock:/var/run/docker.sock',
|
'-v', '/var/run/docker.sock:/var/run/docker.sock',
|
||||||
'-v', f'{project_dir}:{project_dir}',
|
'-v', f'{project_dir}:{project_dir}',
|
||||||
'--entrypoint', 'sh',
|
'--entrypoint', 'sh',
|
||||||
'pic_api:latest',
|
api_image,
|
||||||
'-c', helper_script],
|
'-c', helper_script],
|
||||||
close_fds=True,
|
close_fds=True,
|
||||||
stdout=_subprocess.DEVNULL,
|
stdout=_subprocess.DEVNULL,
|
||||||
|
|||||||
Reference in New Issue
Block a user