From 94a9967dd11759ae3e971966e90e0b52d5b463b8 Mon Sep 17 00:00:00 2001 From: potatosips <118026260+potatosips@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:55:04 +0600 Subject: [PATCH] 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 --- src/server/api/setup/migrate.post.ts | 10 +++-- .../repositories/interface/service.ts | 27 ++++++++++--- src/server/utils/ip.ts | 17 ++++++-- src/test/unit/ip.spec.ts | 40 +++++++++++++++++++ 4 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 src/test/unit/ip.spec.ts diff --git a/src/server/api/setup/migrate.post.ts b/src/server/api/setup/migrate.post.ts index f73abad7..0edbde8c 100644 --- a/src/server/api/setup/migrate.post.ts +++ b/src/server/api/setup/migrate.post.ts @@ -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); diff --git a/src/server/database/repositories/interface/service.ts b/src/server/database/repositories/interface/service.ts index d6866cbb..7100835d 100644 --- a/src/server/database/repositories/interface/service.ts +++ b/src/server/database/repositories/interface/service.ts @@ -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 diff --git a/src/server/utils/ip.ts b/src/server/utils/ip.ts index 557cc344..480519a8 100644 --- a/src/server/utils/ip.ts +++ b/src/server/utils/ip.ts @@ -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 ) { 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; } diff --git a/src/test/unit/ip.spec.ts b/src/test/unit/ip.spec.ts new file mode 100644 index 00000000..99bdafd9 --- /dev/null +++ b/src/test/unit/ip.spec.ts @@ -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'); + }); +});