Browse Source

feat(routes): add CIDR normalization and manageability helpers

pull/2679/head
noderaven 2 weeks ago
parent
commit
7b7adefd64
  1. 73
      src/server/utils/routes.ts
  2. 53
      src/test/unit/routes.spec.ts

73
src/server/utils/routes.ts

@ -0,0 +1,73 @@
import { parseCidr, containsCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
import { createDebug } from 'obug';
import { exec } from '#server/utils/cmd';
import type { ClientType } from '#db/repositories/client/types';
import type { InterfaceType } from '#db/repositories/interface/types';
const ROUTES_DEBUG = createDebug('Routes');
// Mutex to prevent concurrent route reconciles
let reconcileInProgress = false;
let reconcileQueued = false;
type IpVersion = 4 | 6;
type NormalizedRoute = {
cidr: string;
version: IpVersion;
};
type DeviceRoute = {
cidr: string;
managed: boolean;
};
type DesiredRoutes = {
4: Set<string>;
6: Set<string>;
};
type RoutesClient = Pick<ClientType, 'enabled' | 'serverAllowedIps'>;
/**
* Canonicalize a Server Allowed IPs entry to `network/prefix` form.
* `192.168.120.5/22` -> `192.168.120.0/22`; a bare IP gains `/32` or `/128`.
* Returns null for input cidr-tools cannot parse (serverAllowedIps is validated
* only as a permissive string).
*/
function normalizeRoute(entry: string): NormalizedRoute | null {
try {
const parsed = parseCidr(entry);
const network = stringifyIp({
number: parsed.start,
version: parsed.version,
});
return { cidr: `${network}/${parsed.prefix}`, version: parsed.version };
} catch {
return null;
}
}
/**
* Whether wg-easy should add/remove this route. Excludes default routes (they
* need wg-quick's policy routing) and IPv6 link-local / multicast.
*/
function isManageable(cidr: string, version: IpVersion): boolean {
if (cidr.endsWith('/0')) {
return false;
}
if (
version === 6 &&
(containsCidr('fe80::/10', cidr) || containsCidr('ff00::/8', cidr))
) {
return false;
}
return true;
}
export const routesTestExports = {
normalizeRoute,
isManageable,
};

53
src/test/unit/routes.spec.ts

@ -0,0 +1,53 @@
import { describe, expect, test } from 'vitest';
import { routesTestExports } from '#server/utils/routes';
const { normalizeRoute, isManageable } = routesTestExports;
describe('routes', () => {
describe('normalizeRoute', () => {
test('canonicalizes IPv4 host bits to the network address', () => {
expect(normalizeRoute('192.168.120.5/22')).toEqual({
cidr: '192.168.120.0/22',
version: 4,
});
});
test('adds /32 to a bare IPv4 address', () => {
expect(normalizeRoute('10.0.0.5')).toEqual({
cidr: '10.0.0.5/32',
version: 4,
});
});
test('canonicalizes IPv6 host bits to the network address', () => {
expect(normalizeRoute('2001:db8::5/64')).toEqual({
cidr: '2001:db8::/64',
version: 6,
});
});
test('adds /128 to a bare IPv6 address', () => {
expect(normalizeRoute('2001:db8::1')).toEqual({
cidr: '2001:db8::1/128',
version: 6,
});
});
test('returns null for unparseable input', () => {
expect(normalizeRoute('garbage')).toBeNull();
expect(normalizeRoute('')).toBeNull();
});
});
describe('isManageable', () => {
test('rejects default routes', () => {
expect(isManageable('0.0.0.0/0', 4)).toBe(false);
expect(isManageable('::/0', 6)).toBe(false);
});
test('rejects IPv6 link-local and multicast', () => {
expect(isManageable('fe80::/64', 6)).toBe(false);
expect(isManageable('ff00::/8', 6)).toBe(false);
});
test('accepts ordinary subnets', () => {
expect(isManageable('192.168.120.0/22', 4)).toBe(true);
expect(isManageable('2001:db8::/64', 6)).toBe(true);
});
});
});
Loading…
Cancel
Save