feat(network): add configurable VM egress firewall policies and WireGuard-based host routing
This commit is contained in:
+147
@@ -23,6 +23,15 @@ async function getRejectedError(promise: Promise<unknown>): Promise<unknown> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getThrownError(fn: () => unknown): unknown {
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sha256Buffer(buffer: Buffer): string {
|
||||
return crypto.createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
@@ -656,6 +665,118 @@ tap.test('NetworkManager - getGuestNetworkBootArgs() should format correctly', a
|
||||
expect(bootArgs).toEqual('ip=172.30.0.2::172.30.0.1:255.255.255.0::eth0:off');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should accept valid egress firewall config', async () => {
|
||||
const nm = new NetworkManager({
|
||||
firewall: {
|
||||
egress: {
|
||||
defaultAction: 'deny',
|
||||
rules: [
|
||||
{ action: 'allow', to: '1.1.1.1', protocol: 'tcp', ports: [80, 443] },
|
||||
{ action: 'deny', to: '10.0.0.0/8' },
|
||||
{ action: 'allow', protocol: 'icmp' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(nm).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should reject invalid firewall CIDRs and ports', async () => {
|
||||
const invalidCidrError = getThrownError(() => new NetworkManager({
|
||||
firewall: {
|
||||
egress: {
|
||||
rules: [{ action: 'allow', to: '300.1.1.1/32' }],
|
||||
},
|
||||
},
|
||||
}));
|
||||
expect(invalidCidrError).toBeInstanceOf(SmartVMError);
|
||||
expect((invalidCidrError as SmartVMError).code).toEqual('INVALID_FIREWALL_CONFIG');
|
||||
|
||||
const invalidPortError = getThrownError(() => new NetworkManager({
|
||||
firewall: {
|
||||
egress: {
|
||||
rules: [{ action: 'allow', protocol: 'icmp', ports: 53 }],
|
||||
},
|
||||
},
|
||||
}));
|
||||
expect(invalidPortError).toBeInstanceOf(SmartVMError);
|
||||
expect((invalidPortError as SmartVMError).code).toEqual('INVALID_FIREWALL_CONFIG');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should accept valid WireGuard managed config', async () => {
|
||||
const nm = new NetworkManager({
|
||||
wireguard: {
|
||||
interfaceName: 'svwgtest0',
|
||||
routeTable: 51821,
|
||||
config: `
|
||||
# comments are ignored
|
||||
[Interface]
|
||||
PrivateKey = test-private-key
|
||||
Address = 10.70.0.2/32
|
||||
DNS = 1.1.1.1
|
||||
MTU = 1420
|
||||
Table = off
|
||||
|
||||
[Peer]
|
||||
PublicKey = test-public-key
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = 203.0.113.10:51820
|
||||
PersistentKeepalive = 25
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(nm).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should reject unsafe WireGuard hook fields', async () => {
|
||||
const error = getThrownError(() => new NetworkManager({
|
||||
wireguard: {
|
||||
config: `
|
||||
[Interface]
|
||||
PrivateKey = test-private-key
|
||||
Address = 10.70.0.2/32
|
||||
PostUp = iptables -A OUTPUT -j ACCEPT
|
||||
`,
|
||||
},
|
||||
}));
|
||||
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_WIREGUARD_CONFIG');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should reject IPv6 WireGuard AllowedIPs', async () => {
|
||||
const error = getThrownError(() => new NetworkManager({
|
||||
wireguard: {
|
||||
config: `
|
||||
[Interface]
|
||||
PrivateKey = test-private-key
|
||||
Address = 10.70.0.2/32
|
||||
|
||||
[Peer]
|
||||
PublicKey = test-public-key
|
||||
AllowedIPs = ::/0
|
||||
`,
|
||||
},
|
||||
}));
|
||||
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_WIREGUARD_CONFIG');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should reject mixed WireGuard modes', async () => {
|
||||
const error = getThrownError(() => new NetworkManager({
|
||||
wireguard: {
|
||||
existingInterface: 'wg0',
|
||||
config: '[Interface]\nPrivateKey = test-private-key\nAddress = 10.70.0.2/32\n',
|
||||
} as any,
|
||||
}));
|
||||
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_WIREGUARD_CONFIG');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// MicroVM Tests
|
||||
// ============================================================
|
||||
@@ -783,6 +904,32 @@ tap.test('SmartVM - instantiation with custom options', async () => {
|
||||
expect(smartvm).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('SmartVM - should forward firewall and WireGuard options to NetworkManager', async () => {
|
||||
const firewall = {
|
||||
egress: {
|
||||
defaultAction: 'deny' as const,
|
||||
rules: [{ action: 'allow' as const, to: '1.1.1.1', protocol: 'tcp' as const, ports: 443 }],
|
||||
},
|
||||
};
|
||||
const wireguard = {
|
||||
existingInterface: 'wgsmartvm0',
|
||||
routeAllVmTraffic: false,
|
||||
};
|
||||
const smartvm = new SmartVM({
|
||||
dataDir: '/tmp/smartvm-test',
|
||||
firecrackerBinaryPath: '/bin/false',
|
||||
firewall,
|
||||
wireguard,
|
||||
});
|
||||
|
||||
try {
|
||||
expect((smartvm.networkManager as any).firewall).toEqual(firewall);
|
||||
expect((smartvm.networkManager as any).wireguard).toEqual(wireguard);
|
||||
} finally {
|
||||
await smartvm.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('SmartVM - createVM() should track created VMs', async () => {
|
||||
const smartvm = new SmartVM({
|
||||
dataDir: '/tmp/smartvm-test',
|
||||
|
||||
Reference in New Issue
Block a user