feat(vpn): use authenticated VPN route grants

This commit is contained in:
2026-05-24 05:11:48 +00:00
parent ac118397f9
commit 37adcc9ddc
10 changed files with 138 additions and 334 deletions
+27 -30
View File
@@ -77,7 +77,7 @@ tap.test('DcRouter.updateVpnConfig swaps the runtime VPN resolver and restarts V
},
} as any;
(dcRouter as any).routeConfigManager = {
setVpnClientIpsResolver: (resolver: unknown) => {
setVpnClientAccessResolver: (resolver: unknown) => {
resolverValues.push(resolver);
},
applyRoutes: async () => {
@@ -121,15 +121,15 @@ tap.test('RouteConfigManager makes vpnOnly routes fail closed without VPN client
const prepared = (manager as any).injectVpnSecurity(route);
expect(prepared.security.ipAllowList).toEqual([]);
expect(prepared.security.ipBlockList).toContain('*');
expect(prepared.security.ipAllowList).toEqual(['*']);
expect(prepared.security.vpn).toEqual({ required: true, allowedClients: [] });
});
tap.test('RouteConfigManager replaces public allow lists for vpnOnly routes', async () => {
tap.test('RouteConfigManager adds VPN client grants for vpnOnly routes', async () => {
const manager = new RouteConfigManager(
() => undefined,
undefined,
() => ['10.8.0.2'],
() => ['client-1'],
);
const route = {
name: 'private-route',
@@ -144,15 +144,16 @@ tap.test('RouteConfigManager replaces public allow lists for vpnOnly routes', as
const prepared = (manager as any).injectVpnSecurity(route);
expect(prepared.security.ipAllowList).toEqual(['10.8.0.2']);
expect(prepared.security.ipAllowList).toEqual(['*', '203.0.113.10']);
expect(prepared.security.ipBlockList).toEqual(['198.51.100.5']);
expect(prepared.security.vpn).toEqual({ required: true, allowedClients: ['client-1'] });
});
tap.test('RouteConfigManager adds matching VPN clients to restricted non-vpnOnly routes', async () => {
const manager = new RouteConfigManager(
() => undefined,
undefined,
() => ['10.8.0.2'],
() => ['client-1'],
);
const route = {
name: 'shared-private-route',
@@ -166,8 +167,9 @@ tap.test('RouteConfigManager adds matching VPN clients to restricted non-vpnOnly
const prepared = (manager as any).injectVpnSecurity(route);
expect(prepared.security.ipAllowList).toEqual(['203.0.113.10', '10.8.0.2']);
expect(prepared.security.ipAllowList).toEqual(['203.0.113.10']);
expect(prepared.security.ipBlockList).toEqual(['198.51.100.5']);
expect(prepared.security.vpn).toEqual({ required: undefined, allowedClients: ['client-1'] });
});
tap.test('TargetProfileManager matches wildcard profiles against string route domains', async () => {
@@ -181,17 +183,17 @@ tap.test('TargetProfileManager matches wildcard profiles against string route do
createdBy: 'test',
});
const entries = manager.getMatchingClientIps(
const entries = manager.getMatchingVpnClients(
{
name: 'hagen-app',
match: { domains: 'app.hagen.team', ports: [443] },
action: { type: 'forward', targets: [{ host: '10.0.0.5', port: 443 }] },
} as any,
'route-1',
[{ enabled: true, assignedIp: '10.8.0.2', targetProfileIds: ['profile-1'] }] as any,
[{ clientId: 'client-1', enabled: true, assignedIp: '10.8.0.2', targetProfileIds: ['profile-1'] }] as any,
);
expect(entries).toEqual(['10.8.0.2']);
expect(entries).toEqual(['client-1']);
});
tap.test('TargetProfileManager expands wildcard profile domains to matching concrete route domains', async () => {
@@ -238,7 +240,7 @@ tap.test('TargetProfileManager allows source-IP reachable routes for opted-in pr
createdBy: 'test',
});
const entries = manager.getMatchingClientIps(
const entries = manager.getMatchingVpnClients(
{
name: 'restricted-public-route',
match: { domains: 'app.example.com', ports: [443] },
@@ -248,13 +250,12 @@ tap.test('TargetProfileManager allows source-IP reachable routes for opted-in pr
'route-1',
[{ clientId: 'client-1', enabled: true, assignedIp: '10.8.0.2', targetProfileIds: ['profile-1'] }] as any,
new Map(),
new Map([['client-1', '203.0.113.10']]),
);
expect(entries).toEqual(['10.8.0.2']);
expect(entries).toEqual(['client-1']);
});
tap.test('TargetProfileManager does not allow non-matching client source IPs', async () => {
tap.test('TargetProfileManager leaves real source-IP enforcement to SmartProxy', async () => {
const manager = new TargetProfileManager();
(manager as any).profiles.set('profile-1', {
id: 'profile-1',
@@ -265,7 +266,7 @@ tap.test('TargetProfileManager does not allow non-matching client source IPs', a
createdBy: 'test',
});
const entries = manager.getMatchingClientIps(
const entries = manager.getMatchingVpnClients(
{
name: 'restricted-public-route',
match: { domains: 'app.example.com', ports: [443] },
@@ -275,13 +276,12 @@ tap.test('TargetProfileManager does not allow non-matching client source IPs', a
'route-1',
[{ clientId: 'client-1', enabled: true, assignedIp: '10.8.0.2', targetProfileIds: ['profile-1'] }] as any,
new Map(),
new Map([['client-1', '198.51.100.10']]),
);
expect(entries).toEqual([]);
expect(entries).toEqual(['client-1']);
});
tap.test('TargetProfileManager source-IP matching respects route block lists', async () => {
tap.test('TargetProfileManager does not grant routes with wildcard source block', async () => {
const manager = new TargetProfileManager();
(manager as any).profiles.set('profile-1', {
id: 'profile-1',
@@ -292,20 +292,19 @@ tap.test('TargetProfileManager source-IP matching respects route block lists', a
createdBy: 'test',
});
const entries = manager.getMatchingClientIps(
const entries = manager.getMatchingVpnClients(
{
name: 'blocked-route',
match: { domains: 'app.example.com', ports: [443] },
action: { type: 'forward', targets: [{ host: '10.0.0.5', port: 443 }] },
security: {
ipAllowList: ['203.0.113.0/24'],
ipBlockList: ['203.0.113.10'],
ipBlockList: ['*'],
},
} as any,
'route-1',
[{ clientId: 'client-1', enabled: true, assignedIp: '10.8.0.2', targetProfileIds: ['profile-1'] }] as any,
new Map(),
new Map([['client-1', '203.0.113.10']]),
);
expect(entries).toEqual([]);
@@ -322,7 +321,7 @@ tap.test('TargetProfileManager treats public non-vpnOnly routes as source-IP rea
createdBy: 'test',
});
const entries = manager.getMatchingClientIps(
const entries = manager.getMatchingVpnClients(
{
name: 'public-route',
match: { domains: 'public.example.com', ports: [443] },
@@ -331,13 +330,12 @@ tap.test('TargetProfileManager treats public non-vpnOnly routes as source-IP rea
'route-1',
[{ clientId: 'client-1', enabled: true, assignedIp: '10.8.0.2', targetProfileIds: ['profile-1'] }] as any,
new Map(),
new Map([['client-1', '203.0.113.10']]),
);
expect(entries).toEqual(['10.8.0.2']);
expect(entries).toEqual(['client-1']);
});
tap.test('TargetProfileManager does not grant vpnOnly routes through source-IP matching alone', async () => {
tap.test('TargetProfileManager grants vpnOnly routes through source-policy profiles', async () => {
const manager = new TargetProfileManager();
(manager as any).profiles.set('profile-1', {
id: 'profile-1',
@@ -348,7 +346,7 @@ tap.test('TargetProfileManager does not grant vpnOnly routes through source-IP m
createdBy: 'test',
});
const entries = manager.getMatchingClientIps(
const entries = manager.getMatchingVpnClients(
{
name: 'vpn-only-route',
vpnOnly: true,
@@ -359,10 +357,9 @@ tap.test('TargetProfileManager does not grant vpnOnly routes through source-IP m
'route-1',
[{ clientId: 'client-1', enabled: true, assignedIp: '10.8.0.2', targetProfileIds: ['profile-1'] }] as any,
new Map(),
new Map([['client-1', '203.0.113.10']]),
);
expect(entries).toEqual([]);
expect(entries).toEqual(['client-1']);
});
tap.test('TargetProfileManager includes source-IP reachable route domains in client access specs', async () => {
@@ -393,7 +390,7 @@ tap.test('TargetProfileManager includes source-IP reachable route domains in cli
}],
]) as any;
const accessSpec = manager.getClientAccessSpec(['profile-1'], routes, '203.0.113.10');
const accessSpec = manager.getClientAccessSpec(['profile-1'], routes);
expect(accessSpec.domains).toContain('app.example.com');
});