feat(wireguard): add WireGuard transport support with management APIs and config generation
This commit is contained in:
353
test/test.wireguard.node.ts
Normal file
353
test/test.wireguard.node.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
VpnConfig,
|
||||
VpnServer,
|
||||
WgConfigGenerator,
|
||||
} from '../ts/index.js';
|
||||
import type {
|
||||
IVpnClientConfig,
|
||||
IVpnServerConfig,
|
||||
IVpnServerOptions,
|
||||
IWgPeerConfig,
|
||||
} from '../ts/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// WireGuard config validation — client
|
||||
// ============================================================================
|
||||
|
||||
// A valid 32-byte key in base64 (44 chars)
|
||||
const VALID_KEY = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
|
||||
const VALID_KEY_2 = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=';
|
||||
|
||||
tap.test('WG client config: valid wireguard config passes validation', async () => {
|
||||
const config: IVpnClientConfig = {
|
||||
serverUrl: '', // not needed for WG
|
||||
serverPublicKey: VALID_KEY,
|
||||
transport: 'wireguard',
|
||||
wgPrivateKey: VALID_KEY_2,
|
||||
wgAddress: '10.8.0.2',
|
||||
wgEndpoint: 'vpn.example.com:51820',
|
||||
wgAllowedIps: ['0.0.0.0/0'],
|
||||
};
|
||||
VpnConfig.validateClientConfig(config);
|
||||
});
|
||||
|
||||
tap.test('WG client config: rejects missing wgPrivateKey', async () => {
|
||||
const config: IVpnClientConfig = {
|
||||
serverUrl: '',
|
||||
serverPublicKey: VALID_KEY,
|
||||
transport: 'wireguard',
|
||||
wgAddress: '10.8.0.2',
|
||||
wgEndpoint: 'vpn.example.com:51820',
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
VpnConfig.validateClientConfig(config);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect((e as Error).message).toContain('wgPrivateKey');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('WG client config: rejects missing wgAddress', async () => {
|
||||
const config: IVpnClientConfig = {
|
||||
serverUrl: '',
|
||||
serverPublicKey: VALID_KEY,
|
||||
transport: 'wireguard',
|
||||
wgPrivateKey: VALID_KEY_2,
|
||||
wgEndpoint: 'vpn.example.com:51820',
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
VpnConfig.validateClientConfig(config);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect((e as Error).message).toContain('wgAddress');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('WG client config: rejects missing wgEndpoint', async () => {
|
||||
const config: IVpnClientConfig = {
|
||||
serverUrl: '',
|
||||
serverPublicKey: VALID_KEY,
|
||||
transport: 'wireguard',
|
||||
wgPrivateKey: VALID_KEY_2,
|
||||
wgAddress: '10.8.0.2',
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
VpnConfig.validateClientConfig(config);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect((e as Error).message).toContain('wgEndpoint');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('WG client config: rejects invalid key length', async () => {
|
||||
const config: IVpnClientConfig = {
|
||||
serverUrl: '',
|
||||
serverPublicKey: VALID_KEY,
|
||||
transport: 'wireguard',
|
||||
wgPrivateKey: 'tooshort',
|
||||
wgAddress: '10.8.0.2',
|
||||
wgEndpoint: 'vpn.example.com:51820',
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
VpnConfig.validateClientConfig(config);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect((e as Error).message).toContain('44 characters');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('WG client config: rejects invalid CIDR in allowedIps', async () => {
|
||||
const config: IVpnClientConfig = {
|
||||
serverUrl: '',
|
||||
serverPublicKey: VALID_KEY,
|
||||
transport: 'wireguard',
|
||||
wgPrivateKey: VALID_KEY_2,
|
||||
wgAddress: '10.8.0.2',
|
||||
wgEndpoint: 'vpn.example.com:51820',
|
||||
wgAllowedIps: ['not-a-cidr'],
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
VpnConfig.validateClientConfig(config);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect((e as Error).message).toContain('CIDR');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// WireGuard config validation — server
|
||||
// ============================================================================
|
||||
|
||||
tap.test('WG server config: valid config passes validation', async () => {
|
||||
const config: IVpnServerConfig = {
|
||||
listenAddr: '',
|
||||
privateKey: VALID_KEY,
|
||||
publicKey: VALID_KEY_2,
|
||||
subnet: '10.8.0.0/24',
|
||||
transportMode: 'wireguard',
|
||||
wgPeers: [
|
||||
{
|
||||
publicKey: VALID_KEY_2,
|
||||
allowedIps: ['10.8.0.2/32'],
|
||||
},
|
||||
],
|
||||
};
|
||||
VpnConfig.validateServerConfig(config);
|
||||
});
|
||||
|
||||
tap.test('WG server config: rejects empty wgPeers', async () => {
|
||||
const config: IVpnServerConfig = {
|
||||
listenAddr: '',
|
||||
privateKey: VALID_KEY,
|
||||
publicKey: VALID_KEY_2,
|
||||
subnet: '10.8.0.0/24',
|
||||
transportMode: 'wireguard',
|
||||
wgPeers: [],
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
VpnConfig.validateServerConfig(config);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect((e as Error).message).toContain('wgPeers');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('WG server config: rejects peer without publicKey', async () => {
|
||||
const config: IVpnServerConfig = {
|
||||
listenAddr: '',
|
||||
privateKey: VALID_KEY,
|
||||
publicKey: VALID_KEY_2,
|
||||
subnet: '10.8.0.0/24',
|
||||
transportMode: 'wireguard',
|
||||
wgPeers: [
|
||||
{
|
||||
publicKey: '',
|
||||
allowedIps: ['10.8.0.2/32'],
|
||||
},
|
||||
],
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
VpnConfig.validateServerConfig(config);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect((e as Error).message).toContain('publicKey');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('WG server config: rejects invalid wgListenPort', async () => {
|
||||
const config: IVpnServerConfig = {
|
||||
listenAddr: '',
|
||||
privateKey: VALID_KEY,
|
||||
publicKey: VALID_KEY_2,
|
||||
subnet: '10.8.0.0/24',
|
||||
transportMode: 'wireguard',
|
||||
wgListenPort: 0,
|
||||
wgPeers: [
|
||||
{
|
||||
publicKey: VALID_KEY_2,
|
||||
allowedIps: ['10.8.0.2/32'],
|
||||
},
|
||||
],
|
||||
};
|
||||
let threw = false;
|
||||
try {
|
||||
VpnConfig.validateServerConfig(config);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect((e as Error).message).toContain('wgListenPort');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// WireGuard keypair generation via daemon
|
||||
// ============================================================================
|
||||
|
||||
let server: VpnServer;
|
||||
|
||||
tap.test('WG: spawn server daemon for keypair generation', async () => {
|
||||
const options: IVpnServerOptions = {
|
||||
transport: { transport: 'stdio' },
|
||||
};
|
||||
server = new VpnServer(options);
|
||||
const started = await server['bridge'].start();
|
||||
expect(started).toBeTrue();
|
||||
expect(server.running).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('WG: generateWgKeypair returns valid keypair', async () => {
|
||||
const keypair = await server.generateWgKeypair();
|
||||
expect(keypair.publicKey).toBeTypeofString();
|
||||
expect(keypair.privateKey).toBeTypeofString();
|
||||
// WireGuard keys: base64 of 32 bytes = 44 characters
|
||||
expect(keypair.publicKey.length).toEqual(44);
|
||||
expect(keypair.privateKey.length).toEqual(44);
|
||||
// Verify they decode to 32 bytes
|
||||
const pubBuf = Buffer.from(keypair.publicKey, 'base64');
|
||||
const privBuf = Buffer.from(keypair.privateKey, 'base64');
|
||||
expect(pubBuf.length).toEqual(32);
|
||||
expect(privBuf.length).toEqual(32);
|
||||
});
|
||||
|
||||
tap.test('WG: generateWgKeypair returns unique keys each time', async () => {
|
||||
const kp1 = await server.generateWgKeypair();
|
||||
const kp2 = await server.generateWgKeypair();
|
||||
expect(kp1.publicKey).not.toEqual(kp2.publicKey);
|
||||
expect(kp1.privateKey).not.toEqual(kp2.privateKey);
|
||||
});
|
||||
|
||||
tap.test('WG: stop server daemon', async () => {
|
||||
server.stop();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
expect(server.running).toBeFalse();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// WireGuard config file generation
|
||||
// ============================================================================
|
||||
|
||||
tap.test('WgConfigGenerator: generate client config', async () => {
|
||||
const conf = WgConfigGenerator.generateClientConfig({
|
||||
privateKey: 'clientPrivateKeyBase64====================',
|
||||
address: '10.8.0.2/24',
|
||||
dns: ['1.1.1.1', '8.8.8.8'],
|
||||
mtu: 1420,
|
||||
peer: {
|
||||
publicKey: 'serverPublicKeyBase64====================',
|
||||
endpoint: 'vpn.example.com:51820',
|
||||
allowedIps: ['0.0.0.0/0', '::/0'],
|
||||
persistentKeepalive: 25,
|
||||
},
|
||||
});
|
||||
expect(conf).toContain('[Interface]');
|
||||
expect(conf).toContain('PrivateKey = clientPrivateKeyBase64====================');
|
||||
expect(conf).toContain('Address = 10.8.0.2/24');
|
||||
expect(conf).toContain('DNS = 1.1.1.1, 8.8.8.8');
|
||||
expect(conf).toContain('MTU = 1420');
|
||||
expect(conf).toContain('[Peer]');
|
||||
expect(conf).toContain('PublicKey = serverPublicKeyBase64====================');
|
||||
expect(conf).toContain('Endpoint = vpn.example.com:51820');
|
||||
expect(conf).toContain('AllowedIPs = 0.0.0.0/0, ::/0');
|
||||
expect(conf).toContain('PersistentKeepalive = 25');
|
||||
});
|
||||
|
||||
tap.test('WgConfigGenerator: generate client config without optional fields', async () => {
|
||||
const conf = WgConfigGenerator.generateClientConfig({
|
||||
privateKey: 'key1',
|
||||
address: '10.0.0.2/32',
|
||||
peer: {
|
||||
publicKey: 'key2',
|
||||
endpoint: 'server:51820',
|
||||
allowedIps: ['10.0.0.0/24'],
|
||||
},
|
||||
});
|
||||
expect(conf).toContain('[Interface]');
|
||||
expect(conf).not.toContain('DNS');
|
||||
expect(conf).not.toContain('MTU');
|
||||
expect(conf).not.toContain('PresharedKey');
|
||||
expect(conf).not.toContain('PersistentKeepalive');
|
||||
});
|
||||
|
||||
tap.test('WgConfigGenerator: generate server config with NAT', async () => {
|
||||
const conf = WgConfigGenerator.generateServerConfig({
|
||||
privateKey: 'serverPrivKey',
|
||||
address: '10.8.0.1/24',
|
||||
listenPort: 51820,
|
||||
dns: ['1.1.1.1'],
|
||||
enableNat: true,
|
||||
natInterface: 'ens3',
|
||||
peers: [
|
||||
{
|
||||
publicKey: 'peer1PubKey',
|
||||
allowedIps: ['10.8.0.2/32'],
|
||||
presharedKey: 'psk1',
|
||||
persistentKeepalive: 25,
|
||||
},
|
||||
{
|
||||
publicKey: 'peer2PubKey',
|
||||
allowedIps: ['10.8.0.3/32'],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(conf).toContain('[Interface]');
|
||||
expect(conf).toContain('ListenPort = 51820');
|
||||
expect(conf).toContain('PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE');
|
||||
expect(conf).toContain('PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ens3 -j MASQUERADE');
|
||||
// Two [Peer] sections
|
||||
const peerCount = (conf.match(/\[Peer\]/g) || []).length;
|
||||
expect(peerCount).toEqual(2);
|
||||
expect(conf).toContain('PresharedKey = psk1');
|
||||
});
|
||||
|
||||
tap.test('WgConfigGenerator: generate server config without NAT', async () => {
|
||||
const conf = WgConfigGenerator.generateServerConfig({
|
||||
privateKey: 'serverPrivKey',
|
||||
address: '10.8.0.1/24',
|
||||
listenPort: 51820,
|
||||
peers: [
|
||||
{
|
||||
publicKey: 'peerKey',
|
||||
allowedIps: ['10.8.0.2/32'],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(conf).not.toContain('PostUp');
|
||||
expect(conf).not.toContain('PostDown');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user