From 94559c8fc62f89c4167195a2cfbd3e44f6e8cc71 Mon Sep 17 00:00:00 2001 From: noderaven <130021665+noderaven@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:47:17 +0000 Subject: [PATCH] feat(routes): parse device routes from ip route output --- src/server/utils/routes.ts | 32 ++++++++++++++++++++++++++++++ src/test/unit/routes.spec.ts | 38 ++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/server/utils/routes.ts b/src/server/utils/routes.ts index f268bcb6..178389bd 100644 --- a/src/server/utils/routes.ts +++ b/src/server/utils/routes.ts @@ -99,8 +99,40 @@ function collectDesiredRoutes( return desired; } +/** + * Parse `ip route show dev ` output into routes tagged with whether wg-easy + * manages them. Kernel-installed routes (the connected subnet, link-local, + * multicast) are present but flagged unmanaged, so they are never removed and + * never re-added. + */ +function parseDeviceRoutes(output: string): DeviceRoute[] { + const result: DeviceRoute[] = []; + + for (const line of output.trim().split('\n')) { + const trimmed = line.trim(); + if (!trimmed) { + continue; + } + const dest = trimmed.split(/\s+/)[0]; + if (!dest || dest === 'default') { + continue; + } + const normalized = normalizeRoute(dest); + if (!normalized) { + continue; + } + const managed = + !trimmed.includes('proto kernel') && + isManageable(normalized.cidr, normalized.version); + result.push({ cidr: normalized.cidr, managed }); + } + + return result; +} + export const routesTestExports = { normalizeRoute, isManageable, collectDesiredRoutes, + parseDeviceRoutes, }; diff --git a/src/test/unit/routes.spec.ts b/src/test/unit/routes.spec.ts index 353e5002..8b39ffc6 100644 --- a/src/test/unit/routes.spec.ts +++ b/src/test/unit/routes.spec.ts @@ -2,8 +2,12 @@ import { describe, expect, test } from 'vitest'; import { routesTestExports } from '#server/utils/routes'; -const { normalizeRoute, isManageable, collectDesiredRoutes } = - routesTestExports; +const { + normalizeRoute, + isManageable, + collectDesiredRoutes, + parseDeviceRoutes, +} = routesTestExports; describe('routes', () => { describe('normalizeRoute', () => { @@ -110,4 +114,34 @@ describe('routes', () => { expect([...result[4]]).toEqual(['10.0.0.0/24']); }); }); + + describe('parseDeviceRoutes', () => { + test('flags the kernel connected route as unmanaged', () => { + const output = [ + '100.255.1.0/24 proto kernel scope link src 100.255.1.1', + '192.168.120.0/22 scope link', + ].join('\n'); + expect(parseDeviceRoutes(output)).toEqual([ + { cidr: '100.255.1.0/24', managed: false }, + { cidr: '192.168.120.0/22', managed: true }, + ]); + }); + test('skips default routes', () => { + expect(parseDeviceRoutes('default via 10.0.0.1')).toEqual([]); + }); + test('handles empty output', () => { + expect(parseDeviceRoutes('')).toEqual([]); + expect(parseDeviceRoutes('\n \n')).toEqual([]); + }); + test('flags IPv6 link-local kernel route as unmanaged', () => { + const output = [ + 'fe80::/64 proto kernel metric 256 pref medium', + '2001:db8::/64 metric 1024 pref medium', + ].join('\n'); + expect(parseDeviceRoutes(output)).toEqual([ + { cidr: 'fe80::/64', managed: false }, + { cidr: '2001:db8::/64', managed: true }, + ]); + }); + }); });