perf: avoid repeated client scans during CIDR changes (#2707)

* perf: avoid repeated client scans during CIDR changes (#2706)

* refactor: retain CIDR change guards

* perf: avoid repeated client reads during migration

---------

Co-authored-by: potatosips <potatosips@users.noreply.github.com>
This commit is contained in:
potatosips
2026-07-23 07:55:04 +02:00
committed by GitHub
co-authored by potatosips
parent f731135564
commit 94a9967dd1
4 changed files with 80 additions and 14 deletions
+6 -4
View File
@@ -5,7 +5,7 @@ import { z } from 'zod';
import Database from '#server/utils/Database';
import { defineSetupEventHandler } from '#server/utils/handler';
import { nextIP } from '#server/utils/ip';
import { nextIPFromUsedAddresses } from '#server/utils/ip';
import { FileSchema, validateZod } from '#server/utils/types';
export default defineSetupEventHandler('migrate', async ({ event }) => {
@@ -58,6 +58,9 @@ export default defineSetupEventHandler('migrate', async ({ event }) => {
`/${ipv4Cidr.prefix}`,
ipv6Cidr: ipv6Cidr.cidr,
});
const ipv6Addresses = new Set(
(await Database.clients.getAll()).map((client) => client.ipv6Address)
);
for (const clientId in oldConfig.clients) {
const clientConfig = oldConfig.clients[clientId];
@@ -66,15 +69,14 @@ export default defineSetupEventHandler('migrate', async ({ event }) => {
continue;
}
const clients = await Database.clients.getAll();
const ipv6Address = nextIP(6, ipv6Cidr, clients);
const ipv6Address = nextIPFromUsedAddresses(6, ipv6Cidr, ipv6Addresses);
await Database.clients.createFromExisting({
...clientConfig,
ipv4Address: clientConfig.address,
ipv6Address,
});
ipv6Addresses.add(ipv6Address);
}
await Database.general.setSetupStep(0);
@@ -4,7 +4,7 @@ import { parseCidr } from 'cidr-tools';
import { wgInterface } from './schema';
import type { InterfaceCidrUpdateType, InterfaceUpdateType } from './types';
import { nextIP } from '#server/utils/ip';
import { nextIPFromUsedAddresses } from '#server/utils/ip';
import { client as clientSchema } from '#db/schema';
import type { DBType } from '#db/sqlite';
@@ -93,21 +93,36 @@ export class InterfaceService {
.execute();
const clients = await tx.query.client.findMany().execute();
const ipv4Addresses = new Set(
clients.map((client) => client.ipv4Address)
);
const ipv6Addresses = new Set(
clients.map((client) => client.ipv6Address)
);
for (const client of clients) {
// TODO: optimize
const clients = await tx.query.client.findMany().execute();
// only calculate ip if cidr has changed
let nextIpv4 = client.ipv4Address;
if (data.ipv4Cidr !== oldCidr.ipv4Cidr) {
nextIpv4 = nextIP(4, parseCidr(data.ipv4Cidr), clients);
nextIpv4 = nextIPFromUsedAddresses(
4,
parseCidr(data.ipv4Cidr),
ipv4Addresses
);
ipv4Addresses.add(nextIpv4);
ipv4Addresses.delete(client.ipv4Address);
}
let nextIpv6 = client.ipv6Address;
if (data.ipv6Cidr !== oldCidr.ipv6Cidr) {
nextIpv6 = nextIP(6, parseCidr(data.ipv6Cidr), clients);
nextIpv6 = nextIPFromUsedAddresses(
6,
parseCidr(data.ipv6Cidr),
ipv6Addresses
);
ipv6Addresses.add(nextIpv6);
ipv6Addresses.delete(client.ipv6Address);
}
await tx
+13 -4
View File
@@ -13,15 +13,24 @@ export function nextIP(
version: 4 | 6,
cidr: ParsedCidr,
clients: ClientNextIpType[]
) {
const usedAddresses = new Set(
clients.map((client) => client[`ipv${version}Address`])
);
return nextIPFromUsedAddresses(version, cidr, usedAddresses);
}
export function nextIPFromUsedAddresses(
version: 4 | 6,
cidr: ParsedCidr,
usedAddresses: Set<string>
) {
let address;
for (let i = cidr.start + 2n; i <= cidr.end - 1n; i++) {
const currentIp = stringifyIp({ number: i, version: version });
const client = clients.find((client) => {
return client[`ipv${version}Address`] === currentIp;
});
if (!client) {
if (!usedAddresses.has(currentIp)) {
address = currentIp;
break;
}
+40
View File
@@ -0,0 +1,40 @@
import { parseCidr } from 'cidr-tools';
import { describe, expect, test } from 'vitest';
import { nextIPFromUsedAddresses } from '#server/utils/ip';
describe('nextIPFromUsedAddresses', () => {
test('returns the first available address without scanning client records', () => {
const usedAddresses = new Set(['10.0.0.2', '10.0.0.3']);
expect(
nextIPFromUsedAddresses(4, parseCidr('10.0.0.0/29'), usedAddresses)
).toBe('10.0.0.4');
});
test('throws when every usable address is allocated', () => {
const usedAddresses = new Set([
'10.0.0.2',
'10.0.0.3',
'10.0.0.4',
'10.0.0.5',
'10.0.0.6',
]);
expect(() =>
nextIPFromUsedAddresses(4, parseCidr('10.0.0.0/29'), usedAddresses)
).toThrow('Maximum number of clients reached');
});
test('supports replacing addresses while retaining the current allocation state', () => {
const cidr = parseCidr('10.0.0.0/29');
const usedAddresses = new Set(['10.0.0.2', '10.0.0.3']);
const firstReplacement = nextIPFromUsedAddresses(4, cidr, usedAddresses);
usedAddresses.add(firstReplacement);
usedAddresses.delete('10.0.0.2');
expect(firstReplacement).toBe('10.0.0.4');
expect(nextIPFromUsedAddresses(4, cidr, usedAddresses)).toBe('10.0.0.2');
});
});