mirror of https://github.com/wg-easy/wg-easy
2 changed files with 126 additions and 0 deletions
@ -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, |
||||
|
}; |
||||
@ -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…
Reference in new issue