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
+26 -20
View File
@@ -263,22 +263,29 @@ class TestApplyPeerRules(unittest.TestCase):
self.assertIn('DROP', targets)
self.assertIn('ACCEPT', targets)
def test_service_access_restriction_uses_caddy_rule(self):
"""service_access controls access via a single Caddy ACCEPT/DROP rule, not per-VIP rules."""
calls = self._run_apply('10.0.0.4', {'internet_access': False,
'service_access': ['calendar'],
'peer_access': True})
iptables_calls = [c for c in calls if 'iptables' in c]
# Caddy rule should be ACCEPT (any non-empty service_access)
caddy_rules = [c for c in iptables_calls
if '-d' in c and self._FAKE_CADDY_IP in c
and '--dport' in c and '80' in c]
self.assertTrue(caddy_rules, "Expected a Caddy port-80 rule for service access")
target = caddy_rules[-1][caddy_rules[-1].index('-j') + 1]
self.assertEqual(target, 'ACCEPT', "Non-empty service_access should ACCEPT Caddy")
# No per-VIP rules — per-service control is at DNS ACL level
def test_service_access_has_no_caddy_iptables_rule(self):
"""service_access is enforced by CoreDNS ACL only — no per-peer Caddy iptables rule.
The PIC UI is served through Caddy:80; blocking it at the iptables level
would prevent peers from accessing the management UI even if service_access=[].
"""
for sa in (['calendar'], []):
calls = self._run_apply('10.0.0.4', {'internet_access': False,
'service_access': sa,
'peer_access': True})
iptables_calls = [c for c in calls if 'iptables' in c]
caddy_rules = [c for c in iptables_calls
if '-d' in c and self._FAKE_CADDY_IP in c
and '--dport' in c and '80' in c]
self.assertFalse(caddy_rules,
f"No Caddy port-80 iptables rule expected (service_access={sa!r}); "
f"service access is DNS-ACL only so the PIC UI remains accessible")
# No per-VIP rules either — per-service control is at DNS ACL level
for svc_ip in firewall_manager.SERVICE_IPS.values():
vip_rules = [c for c in iptables_calls if '-d' in c and svc_ip in c]
calls = self._run_apply('10.0.0.4', {'internet_access': True,
'service_access': ['calendar'],
'peer_access': True})
vip_rules = [c for c in calls if 'iptables' in c and '-d' in c and svc_ip in c]
self.assertFalse(vip_rules, f"No per-VIP FORWARD rules expected for {svc_ip}")
def test_all_rules_tagged_with_peer_comment(self):
@@ -404,8 +411,8 @@ class TestUpdateServiceIps(unittest.TestCase):
self.assertEqual(set(firewall_manager.SERVICE_IPS.keys()),
{'calendar', 'files', 'mail', 'webdav'})
def test_apply_peer_rules_uses_caddy_not_vips(self):
"""Service access uses Caddy IP for FORWARD rules, not SERVICE_IPS VIPs."""
def test_apply_peer_rules_no_caddy_or_vip_rules(self):
"""Service access is DNS-ACL only — no Caddy or per-VIP FORWARD rules in apply_peer_rules."""
firewall_manager.update_service_ips('10.0.0.0/24')
called_with = []
_CADDY_IP = '172.20.0.2'
@@ -427,9 +434,8 @@ class TestUpdateServiceIps(unittest.TestCase):
iptables_calls = [c for c in called_with if c and c[0] == 'iptables']
dest_ips = [c[c.index('-d') + 1] for c in iptables_calls if '-d' in c]
# Caddy IP should appear for service access
self.assertIn(_CADDY_IP, dest_ips)
# VIPs (old or updated) must not appear — service access is via Caddy
# No Caddy or VIP rules — service access is purely DNS-ACL based
self.assertNotIn(_CADDY_IP, dest_ips)
self.assertNotIn('10.0.0.21', dest_ips)
self.assertNotIn('172.20.0.21', dest_ips)