diff --git a/api/connectivity_manager.py b/api/connectivity_manager.py index 45260e1..ba372d0 100644 --- a/api/connectivity_manager.py +++ b/api/connectivity_manager.py @@ -1437,6 +1437,18 @@ class ConnectivityManager(BaseServiceManager): except Exception as e: logger.warning(f"delete_connection: killswitch cleanup failed " f"(non-fatal): {e}") + elif (record.get('type') == self.CELL_RELAY_TYPE + and isinstance(table, int) + and self.wireguard_manager is not None): + # A cell_relay policy-routes peers via a source ip rule + a + # shared default route in its table inside cell-wireguard. Per-peer + # detach removes the rules; the table's default route only goes + # away here, when the connection is gone — otherwise it leaks. + try: + self.wireguard_manager.teardown_route_table(table) + except Exception as e: + logger.warning(f"delete_connection: cell_relay route table " + f"cleanup failed (non-fatal): {e}") for secret_ref in record.get('secret_refs', []): if self.vault_manager is not None: @@ -1554,6 +1566,18 @@ class ConnectivityManager(BaseServiceManager): f"{cell_name!r} no longer offered but still " f"referenced; keeping") continue + # Flush the relay's policy-routing table (shared default route) + # before forgetting the record — this path deletes the config + # entry directly rather than via delete_connection, so it must + # do the same host-routing teardown or the route leaks. + rtable = rec.get('table') + if self.wireguard_manager is not None and isinstance(rtable, int): + try: + self.wireguard_manager.teardown_route_table(rtable) + except Exception as e: + logger.warning(f"reconcile_cell_relays: route table " + f"cleanup for {cell_name!r} failed " + f"(non-fatal): {e}") try: self.config_manager.delete_connection(rec.get('id')) removed.append(rec.get('id')) diff --git a/api/routes/peers.py b/api/routes/peers.py index 09dd485..0d09cc6 100644 --- a/api/routes/peers.py +++ b/api/routes/peers.py @@ -357,6 +357,13 @@ def remove_peer(peer_name): if success: if peer_ip: firewall_manager.clear_peer_rules(peer_ip) + # Clear any cell_relay / route-via policy rule for this peer so a + # deleted-while-assigned peer doesn't leave a stale source ip rule + # (which could later misroute a new peer that reuses the IP). + try: + wireguard_manager.remove_peer_route_via(peer_ip) + except Exception as wg_err: + logger.warning(f"Peer {peer_name}: relay route cleanup failed (non-fatal): {wg_err}") _dns_primary, _dns_szones = _configured_dns_params() firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH, _dns_primary, cell_links=cell_link_manager.list_connections(), diff --git a/api/wireguard_manager.py b/api/wireguard_manager.py index e4524e5..8c60780 100644 --- a/api/wireguard_manager.py +++ b/api/wireguard_manager.py @@ -786,21 +786,60 @@ class WireGuardManager(BaseServiceManager): logger.error(f'apply_peer_route_via failed: {e}') return False - def remove_peer_route_via(self, peer_ip: str, table: int = 100) -> None: - """Remove the ip rule for peer_ip added by apply_peer_route_via. Non-fatal.""" + def remove_peer_route_via(self, peer_ip: str) -> None: + """Remove the policy-routing ip rule(s) for peer_ip. Non-fatal. + + Deletes every `ip rule from peer_ip/32` regardless of which table it + points at: the v2 cell_relay path adds the rule with the connection's + own table (1000+) while the legacy route-via path uses table 100, so a + caller clearing a peer's exit does not reliably know the table. Matching + by source alone removes the rule in both cases (and any duplicate). The + shared routing *table* itself is torn down separately at connection + teardown — see teardown_route_table. + """ real_conf = self._config_file() if '/tmp/' in real_conf or 'pytest' in real_conf or 'wg_confs' not in real_conf: return try: - subprocess.run( - ['docker', 'exec', 'cell-wireguard', - 'ip', 'rule', 'del', 'from', f'{peer_ip}/32', - 'pref', str(table), 'lookup', str(table)], - capture_output=True, timeout=5 - ) + for _ in range(32): + r = subprocess.run( + ['docker', 'exec', 'cell-wireguard', + 'ip', 'rule', 'del', 'from', f'{peer_ip}/32'], + capture_output=True, timeout=5 + ) + if r.returncode != 0: + break except Exception: pass + def teardown_route_table(self, table: int) -> None: + """Tear down a relay routing table when its connection is removed. Non-fatal. + + Removes any remaining `ip rule ... lookup