b6efcff25e
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.
95 lines
3.1 KiB
JavaScript
95 lines
3.1 KiB
JavaScript
'use strict';
|
|
|
|
// Registry of peer theta-gateway instances this gateway has meshed with —
|
|
// raw Redis, same pattern as wg_site.js/audit_event.js. Each registration
|
|
// carries what's needed to configure a local WireGuard peer entry for them:
|
|
// public key, reachable endpoint, and the mesh IP this gateway assigned them
|
|
// (MULTI_SITE_SPEC.md's one-octet-per-site addressing, 172.24.<idx>.0/16 +
|
|
// 10.<idx>.0.0/16, idx 1-254).
|
|
//
|
|
// Redis keys:
|
|
// mesh_gateway:<id> — hash of gateway fields
|
|
// mesh_gateway_index — sorted set (score = createdAt, value = id)
|
|
|
|
const crypto = require('crypto');
|
|
const conf = require('@simpleworkjs/conf');
|
|
const { getRedis } = require('./index');
|
|
|
|
const MAX_MESH_INDEX = 254;
|
|
|
|
const P = () => conf.redis.prefix;
|
|
const idxKey = () => `${P()}mesh_gateway_index`;
|
|
const gatewayKey = (id) => `${P()}mesh_gateway:${id}`;
|
|
|
|
function serialize(obj) {
|
|
const out = {};
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
out[k] = String(v == null ? '' : v);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function deserialize(h) {
|
|
if (!h || !h.id) return null;
|
|
return { ...h, meshIndex: Number(h.meshIndex || 0), createdAt: Number(h.createdAt || 0), lastSeenAt: Number(h.lastSeenAt || 0) };
|
|
}
|
|
|
|
async function list() {
|
|
const redis = await getRedis();
|
|
const ids = await redis.zRange(idxKey(), 0, -1);
|
|
const out = [];
|
|
for (const id of ids) {
|
|
const g = deserialize(await redis.hGetAll(gatewayKey(id)));
|
|
if (g) out.push(g);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
async function findByPublicKey(publicKey) {
|
|
const all = await list();
|
|
return all.find((g) => g.publicKey === publicKey) || null;
|
|
}
|
|
|
|
function nextFreeMeshIndex(existing) {
|
|
const used = new Set(existing.map((g) => g.meshIndex).filter(Boolean));
|
|
for (let i = 1; i <= MAX_MESH_INDEX; i++) {
|
|
if (!used.has(i)) return i;
|
|
}
|
|
throw new Error(`Mesh index space exhausted (max ${MAX_MESH_INDEX} gateways)`);
|
|
}
|
|
|
|
// Register (or re-register, idempotent by publicKey) a peer gateway.
|
|
// Re-registering the same public key updates its endpoint/siteSlug but
|
|
// reuses its existing mesh index -- a gateway that re-registers after a
|
|
// restart must not get bumped to a new mesh subnet.
|
|
async function register({ publicKey, endpoint, siteSlug }) {
|
|
const redis = await getRedis();
|
|
const existing = await list();
|
|
const already = existing.find((g) => g.publicKey === publicKey);
|
|
|
|
const now = Date.now();
|
|
if (already) {
|
|
const updated = { ...already, endpoint, siteSlug: siteSlug || already.siteSlug, lastSeenAt: now };
|
|
await redis.hSet(gatewayKey(already.id), serialize(updated));
|
|
return updated;
|
|
}
|
|
|
|
const id = crypto.randomBytes(8).toString('hex');
|
|
const meshIndex = nextFreeMeshIndex(existing);
|
|
const gateway = { id, publicKey, endpoint, siteSlug: siteSlug || '', meshIndex, createdAt: now, lastSeenAt: now };
|
|
await redis.hSet(gatewayKey(id), serialize(gateway));
|
|
await redis.zAdd(idxKey(), { score: now, value: id });
|
|
return gateway;
|
|
}
|
|
|
|
async function remove(id) {
|
|
const redis = await getRedis();
|
|
const gw = deserialize(await redis.hGetAll(gatewayKey(id)));
|
|
if (!gw) return null;
|
|
await redis.del(gatewayKey(id));
|
|
await redis.zRem(idxKey(), id);
|
|
return gw;
|
|
}
|
|
|
|
module.exports = { list, findByPublicKey, register, remove, MAX_MESH_INDEX };
|