fix(mesh): peer removal now cleans up its kernel routes

wg_iface.removePeer() previously just did `wg set ... remove` -- the
kernel routes setPeer() adds for a peer's AllowedIPs (since wg itself
only configures crypto-routing, not kernel routes -- see setPeer's own
comment) were never cleaned up, a real TODO flagged in code but never
exercised because nothing removed a mesh peer at all.

- removePeer() now queries the peer's current AllowedIPs (`wg show
  <iface> allowed-ips`) BEFORE removing it -- once gone, wg no longer
  knows what to clean up -- and issues `ip route del` for each.
- New DELETE /api/mesh/gateways/:id (models/mesh_gateway.js gained
  remove()) actually calls removePeer(), so the fix has a real caller;
  previously there was no removal path anywhere in the mesh feature at
  all. Refuses to remove the local "(self)" entry. Does not reach out
  to the remote gateway to remove the reciprocal peer -- that side
  needs the same action taken independently.
- Mesh UI: remove button per non-self peer row, using app.messages.confirm
  (not native confirm() -- caught by this repo's own no-native-dialogs
  test, which failed on first pass and is now green).

Verified for real with a live WireGuard interface in a container: routes
for a peer's AllowedIPs present after setPeer, confirmed gone after
removePeer, while the interface's own local route correctly survives.
This commit is contained in:
2026-08-10 20:18:39 -04:00
parent 29029914d2
commit b6efcff25e
4 changed files with 67 additions and 6 deletions
+22 -4
View File
@@ -137,12 +137,30 @@ function setPeer(name, { publicKey, endpoint, allowedIPs, keepalive }) {
}
}
// TODO: doesn't clean up the kernel routes setPeer added for this peer's
// AllowedIPs (would need to record or query them first) -- not exercised by
// any caller yet (nothing in this codebase removes a mesh peer today), but
// flagging so whoever adds that doesn't get bitten by stale routes.
// Removes the peer AND the kernel routes setPeer() added for its
// AllowedIPs -- query them BEFORE removing the peer (once gone, `wg` no
// longer knows what to clean up, and nothing else tracks these routes,
// since they were added by us directly, not by wg-quick).
//
// Safe to assume none of a peer's AllowedIPs collide with this gateway's
// own local address range: mesh indexes are unique per gateway
// (models/mesh_gateway.js's nextFreeMeshIndex), so a peer's
// 172.24.<peerIndex>.0/24 can never equal our own 172.24.<ownIndex>.0/24.
function removePeer(name, publicKey) {
const show = tryRun('wg', ['show', name, 'allowed-ips']);
let allowedIPs = [];
if (show.ok) {
const line = show.out.split('\n').find((l) => l.startsWith(publicKey + '\t'));
if (line) {
allowedIPs = (line.split('\t')[1] || '').split(/\s+/).filter((ip) => ip && ip !== '(none)');
}
}
tryRun('wg', ['set', name, 'peer', publicKey, 'remove']);
for (const cidr of allowedIPs) {
tryRun('ip', ['route', 'del', cidr, 'dev', name]);
}
}
module.exports = {