feat(auth,client-registry): add Noise IK client authentication with managed client registry and per-client ACL controls

This commit is contained in:
2026-03-29 17:04:27 +00:00
parent 187a69028b
commit 01a0d8b9f4
20 changed files with 1930 additions and 897 deletions

View File

@@ -2,10 +2,17 @@ import { tap, expect } from '@git.zone/tstest/tapbundle';
import { VpnConfig } from '../ts/index.js';
import type { IVpnClientConfig, IVpnServerConfig } from '../ts/index.js';
// Valid 32-byte base64 keys for testing
const TEST_KEY_A = 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=';
const TEST_KEY_B = 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI=';
const TEST_KEY_C = 'Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2M=';
tap.test('VpnConfig: validate valid client config', async () => {
const config: IVpnClientConfig = {
serverUrl: 'wss://vpn.example.com/tunnel',
serverPublicKey: 'dGVzdHB1YmxpY2tleQ==',
serverPublicKey: TEST_KEY_A,
clientPrivateKey: TEST_KEY_B,
clientPublicKey: TEST_KEY_C,
dns: ['1.1.1.1', '8.8.8.8'],
mtu: 1420,
keepaliveIntervalSecs: 30,
@@ -16,7 +23,9 @@ tap.test('VpnConfig: validate valid client config', async () => {
tap.test('VpnConfig: reject client config without serverUrl', async () => {
const config = {
serverPublicKey: 'dGVzdHB1YmxpY2tleQ==',
serverPublicKey: TEST_KEY_A,
clientPrivateKey: TEST_KEY_B,
clientPublicKey: TEST_KEY_C,
} as IVpnClientConfig;
let threw = false;
try {
@@ -31,7 +40,9 @@ tap.test('VpnConfig: reject client config without serverUrl', async () => {
tap.test('VpnConfig: reject client config with invalid serverUrl scheme', async () => {
const config: IVpnClientConfig = {
serverUrl: 'http://vpn.example.com/tunnel',
serverPublicKey: 'dGVzdHB1YmxpY2tleQ==',
serverPublicKey: TEST_KEY_A,
clientPrivateKey: TEST_KEY_B,
clientPublicKey: TEST_KEY_C,
};
let threw = false;
try {
@@ -43,10 +54,28 @@ tap.test('VpnConfig: reject client config with invalid serverUrl scheme', async
expect(threw).toBeTrue();
});
tap.test('VpnConfig: reject client config without clientPrivateKey', async () => {
const config = {
serverUrl: 'wss://vpn.example.com/tunnel',
serverPublicKey: TEST_KEY_A,
clientPublicKey: TEST_KEY_C,
} as IVpnClientConfig;
let threw = false;
try {
VpnConfig.validateClientConfig(config);
} catch (e) {
threw = true;
expect((e as Error).message).toContain('clientPrivateKey');
}
expect(threw).toBeTrue();
});
tap.test('VpnConfig: reject client config with invalid MTU', async () => {
const config: IVpnClientConfig = {
serverUrl: 'wss://vpn.example.com/tunnel',
serverPublicKey: 'dGVzdHB1YmxpY2tleQ==',
serverPublicKey: TEST_KEY_A,
clientPrivateKey: TEST_KEY_B,
clientPublicKey: TEST_KEY_C,
mtu: 100,
};
let threw = false;
@@ -62,7 +91,9 @@ tap.test('VpnConfig: reject client config with invalid MTU', async () => {
tap.test('VpnConfig: reject client config with invalid DNS', async () => {
const config: IVpnClientConfig = {
serverUrl: 'wss://vpn.example.com/tunnel',
serverPublicKey: 'dGVzdHB1YmxpY2tleQ==',
serverPublicKey: TEST_KEY_A,
clientPrivateKey: TEST_KEY_B,
clientPublicKey: TEST_KEY_C,
dns: ['not-an-ip'],
};
let threw = false;
@@ -78,12 +109,15 @@ tap.test('VpnConfig: reject client config with invalid DNS', async () => {
tap.test('VpnConfig: validate valid server config', async () => {
const config: IVpnServerConfig = {
listenAddr: '0.0.0.0:443',
privateKey: 'dGVzdHByaXZhdGVrZXk=',
publicKey: 'dGVzdHB1YmxpY2tleQ==',
privateKey: TEST_KEY_A,
publicKey: TEST_KEY_B,
subnet: '10.8.0.0/24',
dns: ['1.1.1.1'],
mtu: 1420,
enableNat: true,
clients: [
{ clientId: 'test-client', publicKey: TEST_KEY_C },
],
};
// Should not throw
VpnConfig.validateServerConfig(config);
@@ -92,8 +126,8 @@ tap.test('VpnConfig: validate valid server config', async () => {
tap.test('VpnConfig: reject server config with invalid subnet', async () => {
const config: IVpnServerConfig = {
listenAddr: '0.0.0.0:443',
privateKey: 'dGVzdHByaXZhdGVrZXk=',
publicKey: 'dGVzdHB1YmxpY2tleQ==',
privateKey: TEST_KEY_A,
publicKey: TEST_KEY_B,
subnet: 'invalid',
};
let threw = false;
@@ -109,7 +143,7 @@ tap.test('VpnConfig: reject server config with invalid subnet', async () => {
tap.test('VpnConfig: reject server config without privateKey', async () => {
const config = {
listenAddr: '0.0.0.0:443',
publicKey: 'dGVzdHB1YmxpY2tleQ==',
publicKey: TEST_KEY_B,
subnet: '10.8.0.0/24',
} as IVpnServerConfig;
let threw = false;
@@ -122,4 +156,24 @@ tap.test('VpnConfig: reject server config without privateKey', async () => {
expect(threw).toBeTrue();
});
tap.test('VpnConfig: reject server config with invalid client publicKey', async () => {
const config: IVpnServerConfig = {
listenAddr: '0.0.0.0:443',
privateKey: TEST_KEY_A,
publicKey: TEST_KEY_B,
subnet: '10.8.0.0/24',
clients: [
{ clientId: 'bad-client', publicKey: 'short-key' },
],
};
let threw = false;
try {
VpnConfig.validateServerConfig(config);
} catch (e) {
threw = true;
expect((e as Error).message).toContain('publicKey');
}
expect(threw).toBeTrue();
});
export default tap.start();