Serve a master key's devices from several servers
A master key now has a set of servers (the panel's own and/or nodes). A device's single WireGuard key becomes a peer on every server of its key, each with its own IP and PSK, and config.servers[] lists them all. - ui.db: master_servers (the key's set, never empty), device_peers (which key is on which server, with the ip/psk that server issued), peer_tombstones (peers to delete) and server_profiles (last known profile of each server). - Nothing is pushed blindly: reconcile() brings one server to the desired state - deletes tombstones, adds peers, rekeys with PUT, fills ip/psk, rotates a pending PSK - one pass per server at a time. It runs right after every action, when a node connects and once a minute, so a node that was offline catches up on rekeys, PSK changes and removals. - config is built from ui.db, so an offline node stays in it and rev does not flap; a server is listed once its peer carries the device's current key. An empty config is never sent (502 instead); register and rekey roll back if no server accepted the key. - Existing keys and devices migrate on start. With one server the config and its rev are byte-identical to 0.3.1 (checked by running both versions on the same ui.db), so existing clients notice nothing. - Panel: server checkboxes in the key settings instead of the placeholder (the last one cannot be removed), "на N из M серверов" on devices that have not reached every server yet, device stats summed over servers. - SUB_HOST: the panel's public address for sen:// links and endpoints; install.sh writes it. Without it a panel with no VPN of its own used the address it was opened at. - A node referenced by a key's server set cannot be deleted.
This commit is contained in:
+21
-3
@@ -138,6 +138,16 @@ export function createNodes(deps: { uidb: Database.Database; ctrlLocal: Ctrl; ba
|
||||
|
||||
const exists = (id: number) => (id === 0 ? LOCAL : q.byId.get(id) !== undefined);
|
||||
|
||||
/** Есть ли сейчас связь с сервером (локальный считаем доступным всегда — до первого отказа awg-ctrl). */
|
||||
const isOnline = (id: number) => (id === 0 ? LOCAL : conns.has(id));
|
||||
|
||||
/** Все серверы реестра: локальный (если есть) и ноды. */
|
||||
const ids = (): number[] => [...(LOCAL ? [0] : []), ...(q.all.all() as NodeRow[]).map(n => n.id)];
|
||||
|
||||
// Подписчики на «сервер снова на связи» — подписка по этому сигналу догоняет пропущенное.
|
||||
const onlineHandlers: ((id: number) => void)[] = [];
|
||||
const onOnline = (cb: (id: number) => void) => { onlineHandlers.push(cb); };
|
||||
|
||||
/** Сервер по умолчанию: локальный, а без него — первая нода. */
|
||||
function defaultId(): number {
|
||||
if (LOCAL) return 0;
|
||||
@@ -216,6 +226,7 @@ export function createNodes(deps: { uidb: Database.Database; ctrlLocal: Ctrl; ba
|
||||
ws.send(JSON.stringify({ t: "ready" }));
|
||||
console.log(`nodes: «${row.name}» (#${row.id}) подключена${joining ? " (первое подключение)" : ""}`);
|
||||
void refreshHealth(row.id);
|
||||
for (const cb of onlineHandlers) { try { cb(row.id); } catch { /* подписчик сам разберётся */ } }
|
||||
}
|
||||
|
||||
ws.on("message", raw => {
|
||||
@@ -241,7 +252,10 @@ export function createNodes(deps: { uidb: Database.Database; ctrlLocal: Ctrl; ba
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (LOCAL) void refreshHealth(0);
|
||||
if (LOCAL) {
|
||||
void refreshHealth(0);
|
||||
for (const cb of onlineHandlers) { try { cb(0); } catch { /* подписчик сам разберётся */ } }
|
||||
}
|
||||
if (!NODE_PORT || !tls) return;
|
||||
const server = https.createServer(tls);
|
||||
const wss = new WebSocketServer({ server, path: "/node/v1", maxPayload: 4 * 1024 * 1024 });
|
||||
@@ -330,7 +344,7 @@ export function createNodes(deps: { uidb: Database.Database; ctrlLocal: Ctrl; ba
|
||||
const n = validId(id) ? q.byId.get(id) as NodeRow | undefined : undefined;
|
||||
if (!n) { res.status(404).json({ error: "Не найден" }); return; }
|
||||
// Ключи API и мастер-ключи ссылаются на server_id — не оставляем их висеть в воздухе.
|
||||
const used = (["api_keys", "master_keys"] as const).some(t => {
|
||||
const used = (["api_keys", "master_servers"] as const).some(t => {
|
||||
try { return (uidb.prepare(`SELECT COUNT(*) AS n FROM ${t} WHERE server_id = ?`).get(id) as { n: number }).n > 0; }
|
||||
catch { return false; }
|
||||
});
|
||||
@@ -338,8 +352,12 @@ export function createNodes(deps: { uidb: Database.Database; ctrlLocal: Ctrl; ba
|
||||
const c = conns.get(id);
|
||||
if (c) { dropConn(id, c); c.ws.close(4004, "removed"); }
|
||||
q.delete.run(id);
|
||||
// Хвосты подписки для этого сервера больше некому доделать.
|
||||
for (const t of ["peer_tombstones", "server_profiles"]) {
|
||||
try { uidb.prepare(`DELETE FROM ${t} WHERE server_id = ?`).run(id); } catch { /* таблицы ещё нет */ }
|
||||
}
|
||||
res.json({ success: true, id });
|
||||
});
|
||||
|
||||
return { router, ctrlFor, exists, defaultId, start };
|
||||
return { router, ctrlFor, exists, isOnline, ids, onOnline, defaultId, start };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user