fix: UI always accessible; fix exit-relay AllowedIPs not updating

**PIC UI always accessible (service_access=[])**
Remove the per-peer Caddy:80 ACCEPT/DROP rule from apply_peer_rules.
Service access was enforced at two layers (iptables DROP + CoreDNS ACL),
but the iptables layer also blocked the PIC web UI served through Caddy.
CoreDNS ACL alone is sufficient — DNS blocks service hostnames; the UI
path through Caddy remains reachable regardless of service_access value.

**Exit-relay internet routing (route_via another cell)**
update_peer_ip validated new_ip as a single ip_network, rejecting the
comma-separated '10.0.1.0/24, 0.0.0.0/0' string passed by
update_cell_peer_allowed_ips(add_default_route=True). The AllowedIPs
in wg0.conf was never updated, so WireGuard never routed internet traffic
through the exit cell's tunnel. Fix: validate each CIDR individually and
apply the change live via wg set without a container restart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 05:41:22 -04:00
parent c521fab1cb
commit 1a611e0474
5 changed files with 97 additions and 46 deletions
+3 -10
View File
@@ -192,16 +192,9 @@ def apply_peer_rules(peer_ip: str, settings: Dict[str, Any]) -> bool:
_iptables(['-I', 'FORWARD', '-s', peer_ip, '-d', '10.0.0.0/24',
'-m', 'comment', '--comment', comment, '-j', target])
# --- Step 3 (inserted last → ends up at TOP of chain) ---
# Service access via Caddy: DNS returns WG server IP for all services;
# ensure_service_dnat() routes wg0:80 to Caddy. One ACCEPT/DROP rule
# controls service access; CoreDNS ACL enforces per-name granularity.
caddy_ip = _get_caddy_container_ip()
if caddy_ip:
target = 'ACCEPT' if service_access else 'DROP'
_iptables(['-I', 'FORWARD', '-s', peer_ip, '-d', caddy_ip,
'-p', 'tcp', '--dport', '80',
'-m', 'comment', '--comment', comment, '-j', target])
# Service access restriction is done entirely by CoreDNS ACL.
# No per-peer iptables rule for Caddy:80 — blocking it would also
# prevent the peer from reaching the PIC web UI and API.
logger.info(f"Applied rules for {peer_ip}: internet={internet_access} "
f"services={service_access} peers={peer_access}")
+27 -4
View File
@@ -798,14 +798,20 @@ class WireGuardManager(BaseServiceManager):
return peers
def update_peer_ip(self, public_key: str, new_ip: str) -> bool:
"""Update AllowedIPs for the peer with the given public key."""
"""Update AllowedIPs for the peer with the given public key.
new_ip may be a single CIDR or a comma-separated list of CIDRs
(e.g. '10.0.1.0/24, 0.0.0.0/0' for exit-relay peers).
Writes to wg0.conf and applies the change live via wg set.
"""
import ipaddress
# Reject whitespace/newlines that ip_network() may tolerate but would inject config
if not isinstance(new_ip, str) or any(c.isspace() for c in new_ip):
if not isinstance(new_ip, str) or '\n' in new_ip or '\r' in new_ip:
logger.error(f'update_peer_ip: invalid new_ip {new_ip!r}')
return False
# Validate each CIDR individually (new_ip may be comma-separated)
try:
ipaddress.ip_network(new_ip, strict=False)
for cidr in new_ip.split(','):
ipaddress.ip_network(cidr.strip(), strict=False)
except ValueError as e:
logger.error(f'update_peer_ip: invalid new_ip {new_ip!r}: {e}')
return False
@@ -823,8 +829,25 @@ class WireGuardManager(BaseServiceManager):
in_target = False
new_lines.append(line)
self._write_config('\n'.join(new_lines))
# Apply live so WireGuard routing takes effect without a restart
self._apply_peer_allowed_ips_live(public_key, new_ip)
return True
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:
return
try:
ips = new_ips.replace(' ', '')
subprocess.run(
['docker', 'exec', 'cell-wireguard', 'wg', 'set', 'wg0',
'peer', public_key, 'allowed-ips', ips],
capture_output=True, timeout=5
)
except Exception as e:
logger.warning(f'_apply_peer_allowed_ips_live failed (non-fatal): {e}')
SPLIT_TUNNEL_IPS = '10.0.0.0/24, 172.20.0.0/16' # legacy fallback; use get_split_tunnel_ips()
FULL_TUNNEL_IPS = '0.0.0.0/0, ::/0'