fix: cross-cell routing for split-tunnel peers
Three related fixes for split-tunnel peers that need to reach connected cells: 1. apply_peer_rules/apply_all_peer_rules now accept wg_subnet (actual local VPN subnet) and cell_subnets (connected cells' vpn_subnets) parameters instead of hardcoding 10.0.0.0/24. All callers (startup, add_peer, update_peer, apply-enforcement endpoint) pass the real values. 2. Explicit ACCEPT rules are inserted in FORWARD for each connected cell's subnet so split-tunnel peers (internet_access=False) can still reach connected cells via the wg0→wg0 path. 3. apply_ip_range in network_manager now loads cell_links.json and passes it to generate_corefile(), fixing a race where the bootstrap DNS thread could overwrite the Corefile and wipe cross-cell DNS forwarding zones on startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+29
-10
@@ -153,18 +153,27 @@ def clear_peer_rules(peer_ip: str) -> None:
|
||||
logger.error(f"clear_peer_rules({peer_ip}): {e}")
|
||||
|
||||
|
||||
def apply_peer_rules(peer_ip: str, settings: Dict[str, Any]) -> bool:
|
||||
def apply_peer_rules(peer_ip: str, settings: Dict[str, Any],
|
||||
wg_subnet: str = '10.0.0.0/24',
|
||||
cell_subnets: Optional[List[str]] = None) -> bool:
|
||||
"""
|
||||
Apply iptables FORWARD rules for a peer based on their access settings.
|
||||
|
||||
wg_subnet: the local cell's WireGuard VPN subnet (e.g. '10.0.2.0/24').
|
||||
Used for the peer-to-peer ACCEPT/DROP rule. Defaults to the
|
||||
legacy hardcoded value so callers that don't yet pass it are safe.
|
||||
cell_subnets: list of connected cells' vpn_subnet strings. When provided,
|
||||
explicit ACCEPT rules are added so split-tunnel peers can reach
|
||||
connected cells regardless of the internet_access setting.
|
||||
|
||||
Each rule is inserted at position 1 (-I), so the LAST call ends up at the TOP.
|
||||
We insert in reverse-priority order: lowest-priority rules first, highest last.
|
||||
|
||||
Desired final chain order (top = highest priority):
|
||||
1. Per-service DROP/ACCEPT (most specific — must beat private-net ACCEPT)
|
||||
2. Peer-to-peer ACCEPT/DROP (10.0.0.0/24)
|
||||
3. Private-net ACCEPTs (for no-internet peers to reach local resources)
|
||||
4. Internet DROP or ACCEPT (lowest priority catch-all)
|
||||
1. Connected-cell subnet ACCEPTs (explicit cross-cell routing)
|
||||
2. Peer-to-peer ACCEPT/DROP (local VPN subnet)
|
||||
3. Private-net ACCEPTs (for no-internet peers to reach local resources)
|
||||
4. Internet DROP or ACCEPT (lowest priority catch-all)
|
||||
"""
|
||||
try:
|
||||
comment = _peer_comment(peer_ip)
|
||||
@@ -187,24 +196,34 @@ def apply_peer_rules(peer_ip: str, settings: Dict[str, Any]) -> bool:
|
||||
_iptables(['-I', 'FORWARD', '-s', peer_ip, '-d', net,
|
||||
'-m', 'comment', '--comment', comment, '-j', 'ACCEPT'])
|
||||
|
||||
# --- Step 2 --- Peer-to-peer (10.0.0.0/24)
|
||||
# --- Step 2 --- Peer-to-peer: use the actual local VPN subnet
|
||||
target = 'ACCEPT' if peer_access else 'DROP'
|
||||
_iptables(['-I', 'FORWARD', '-s', peer_ip, '-d', '10.0.0.0/24',
|
||||
_iptables(['-I', 'FORWARD', '-s', peer_ip, '-d', wg_subnet,
|
||||
'-m', 'comment', '--comment', comment, '-j', target])
|
||||
|
||||
# --- Step 3 --- Explicit ACCEPT for each connected cell's subnet so
|
||||
# split-tunnel peers can route to connected cells (wg0 → wg0 forwarding).
|
||||
if cell_subnets:
|
||||
for subnet in reversed(cell_subnets):
|
||||
_iptables(['-I', 'FORWARD', '-s', peer_ip, '-d', subnet,
|
||||
'-m', 'comment', '--comment', comment, '-j', 'ACCEPT'])
|
||||
|
||||
# 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}")
|
||||
f"services={service_access} peers={peer_access} "
|
||||
f"wg_subnet={wg_subnet} cell_subnets={cell_subnets}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"apply_peer_rules({peer_ip}): {e}")
|
||||
return False
|
||||
|
||||
|
||||
def apply_all_peer_rules(peers: List[Dict[str, Any]]) -> None:
|
||||
def apply_all_peer_rules(peers: List[Dict[str, Any]],
|
||||
wg_subnet: str = '10.0.0.0/24',
|
||||
cell_subnets: Optional[List[str]] = None) -> None:
|
||||
"""Re-apply rules for all peers (called on startup)."""
|
||||
ensure_caddy_virtual_ips()
|
||||
for peer in peers:
|
||||
@@ -215,7 +234,7 @@ def apply_all_peer_rules(peers: List[Dict[str, Any]]) -> None:
|
||||
'internet_access': peer.get('internet_access', True),
|
||||
'service_access': peer.get('service_access', list(SERVICE_IPS.keys())),
|
||||
'peer_access': peer.get('peer_access', True),
|
||||
})
|
||||
}, wg_subnet=wg_subnet, cell_subnets=cell_subnets)
|
||||
|
||||
|
||||
def reconcile_stale_peer_rules(peers: List[Dict[str, Any]]) -> int:
|
||||
|
||||
Reference in New Issue
Block a user