initial
This commit is contained in:
140
test/test.rulebuilder-firewall.ts
Normal file
140
test/test.rulebuilder-firewall.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
buildFirewallRule,
|
||||
buildIPSetCreate,
|
||||
buildIPSetAddElements,
|
||||
buildIPSetRemoveElements,
|
||||
buildIPSetDelete,
|
||||
buildIPSetMatchRule,
|
||||
} from '../ts/nft.rulebuilder.firewall.js';
|
||||
|
||||
tap.test('should build basic firewall drop rule', async () => {
|
||||
const rules = buildFirewallRule('mytable', 'ip', {
|
||||
direction: 'input',
|
||||
action: 'drop',
|
||||
sourceIP: '192.168.1.100',
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(1);
|
||||
expect(rules[0]).toInclude('mytable input');
|
||||
expect(rules[0]).toInclude('ip saddr 192.168.1.100');
|
||||
expect(rules[0]).toInclude('drop');
|
||||
});
|
||||
|
||||
tap.test('should build firewall rule with port and protocol', async () => {
|
||||
const rules = buildFirewallRule('mytable', 'ip', {
|
||||
direction: 'input',
|
||||
action: 'accept',
|
||||
destPort: 22,
|
||||
protocol: 'tcp',
|
||||
sourceIP: '10.0.0.0/8',
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(1);
|
||||
expect(rules[0]).toInclude('tcp');
|
||||
expect(rules[0]).toInclude('ip saddr 10.0.0.0/8');
|
||||
expect(rules[0]).toInclude('tcp dport 22');
|
||||
expect(rules[0]).toInclude('accept');
|
||||
});
|
||||
|
||||
tap.test('should build firewall rule with ct state', async () => {
|
||||
const rules = buildFirewallRule('mytable', 'ip', {
|
||||
direction: 'input',
|
||||
action: 'accept',
|
||||
ctStates: ['established', 'related'],
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(1);
|
||||
expect(rules[0]).toInclude('ct state { established, related }');
|
||||
expect(rules[0]).toInclude('accept');
|
||||
});
|
||||
|
||||
tap.test('should build firewall rule with comment', async () => {
|
||||
const rules = buildFirewallRule('mytable', 'ip', {
|
||||
direction: 'input',
|
||||
action: 'drop',
|
||||
sourceIP: '1.2.3.4',
|
||||
comment: 'block bad actor',
|
||||
});
|
||||
|
||||
expect(rules[0]).toInclude('comment "block bad actor"');
|
||||
});
|
||||
|
||||
tap.test('should build firewall rule for both protocols', async () => {
|
||||
const rules = buildFirewallRule('mytable', 'ip', {
|
||||
direction: 'forward',
|
||||
action: 'accept',
|
||||
destPort: 80,
|
||||
protocol: 'both',
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(2);
|
||||
expect(rules[0]).toInclude('tcp');
|
||||
expect(rules[1]).toInclude('udp');
|
||||
});
|
||||
|
||||
tap.test('should build IP set create command', async () => {
|
||||
const cmds = buildIPSetCreate('mytable', 'ip', {
|
||||
name: 'blocklist',
|
||||
type: 'ipv4_addr',
|
||||
});
|
||||
|
||||
expect(cmds.length).toEqual(1);
|
||||
expect(cmds[0]).toInclude('add set ip mytable blocklist');
|
||||
expect(cmds[0]).toInclude('type ipv4_addr');
|
||||
});
|
||||
|
||||
tap.test('should build IP set with initial elements', async () => {
|
||||
const cmds = buildIPSetCreate('mytable', 'ip', {
|
||||
name: 'trusted',
|
||||
type: 'ipv4_addr',
|
||||
elements: ['10.0.0.1', '10.0.0.2', '10.0.0.3'],
|
||||
});
|
||||
|
||||
expect(cmds.length).toEqual(2);
|
||||
expect(cmds[1]).toInclude('add element');
|
||||
expect(cmds[1]).toInclude('10.0.0.1, 10.0.0.2, 10.0.0.3');
|
||||
});
|
||||
|
||||
tap.test('should build IP set add elements command', async () => {
|
||||
const cmds = buildIPSetAddElements('mytable', 'ip', 'blocklist', ['1.2.3.4', '5.6.7.8']);
|
||||
|
||||
expect(cmds.length).toEqual(1);
|
||||
expect(cmds[0]).toInclude('add element ip mytable blocklist');
|
||||
expect(cmds[0]).toInclude('1.2.3.4, 5.6.7.8');
|
||||
});
|
||||
|
||||
tap.test('should return empty for add with no elements', async () => {
|
||||
const cmds = buildIPSetAddElements('mytable', 'ip', 'blocklist', []);
|
||||
expect(cmds.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should build IP set remove elements command', async () => {
|
||||
const cmds = buildIPSetRemoveElements('mytable', 'ip', 'blocklist', ['1.2.3.4']);
|
||||
|
||||
expect(cmds.length).toEqual(1);
|
||||
expect(cmds[0]).toInclude('delete element ip mytable blocklist');
|
||||
expect(cmds[0]).toInclude('1.2.3.4');
|
||||
});
|
||||
|
||||
tap.test('should build IP set delete command', async () => {
|
||||
const cmds = buildIPSetDelete('mytable', 'ip', 'blocklist');
|
||||
|
||||
expect(cmds.length).toEqual(1);
|
||||
expect(cmds[0]).toInclude('delete set ip mytable blocklist');
|
||||
});
|
||||
|
||||
tap.test('should build IP set match rule', async () => {
|
||||
const cmds = buildIPSetMatchRule('mytable', 'ip', {
|
||||
setName: 'blocklist',
|
||||
direction: 'input',
|
||||
matchField: 'saddr',
|
||||
action: 'drop',
|
||||
});
|
||||
|
||||
expect(cmds.length).toEqual(1);
|
||||
expect(cmds[0]).toInclude('ip saddr @blocklist');
|
||||
expect(cmds[0]).toInclude('drop');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
103
test/test.rulebuilder-nat.ts
Normal file
103
test/test.rulebuilder-nat.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
buildDnatRules,
|
||||
buildSnatRule,
|
||||
buildMasqueradeRule,
|
||||
} from '../ts/nft.rulebuilder.nat.js';
|
||||
|
||||
tap.test('should build basic DNAT rule with masquerade', async () => {
|
||||
const rules = buildDnatRules('mytable', 'ip', {
|
||||
sourcePort: 443,
|
||||
targetHost: '10.0.0.1',
|
||||
targetPort: 8443,
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(2);
|
||||
expect(rules[0]).toInclude('dnat to 10.0.0.1:8443');
|
||||
expect(rules[0]).toInclude('dport 443');
|
||||
expect(rules[0]).toInclude('tcp');
|
||||
expect(rules[1]).toInclude('masquerade');
|
||||
expect(rules[1]).toInclude('dport 8443');
|
||||
});
|
||||
|
||||
tap.test('should skip masquerade when preserveSourceIP is true', async () => {
|
||||
const rules = buildDnatRules('mytable', 'ip', {
|
||||
sourcePort: 443,
|
||||
targetHost: '10.0.0.1',
|
||||
targetPort: 8443,
|
||||
preserveSourceIP: true,
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(1);
|
||||
expect(rules[0]).toInclude('dnat to 10.0.0.1:8443');
|
||||
for (const r of rules) {
|
||||
expect(r).not.toInclude('masquerade');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should generate TCP and UDP rules for protocol both', async () => {
|
||||
const rules = buildDnatRules('mytable', 'ip', {
|
||||
sourcePort: 53,
|
||||
targetHost: '10.0.0.53',
|
||||
targetPort: 53,
|
||||
protocol: 'both',
|
||||
});
|
||||
|
||||
// TCP DNAT + masquerade + UDP DNAT + masquerade = 4
|
||||
expect(rules.length).toEqual(4);
|
||||
const tcpRules = rules.filter(r => r.includes('tcp'));
|
||||
const udpRules = rules.filter(r => r.includes('udp'));
|
||||
expect(tcpRules.length).toEqual(2);
|
||||
expect(udpRules.length).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('should generate UDP-only rules', async () => {
|
||||
const rules = buildDnatRules('mytable', 'ip', {
|
||||
sourcePort: 53,
|
||||
targetHost: '10.0.0.53',
|
||||
targetPort: 53,
|
||||
protocol: 'udp',
|
||||
});
|
||||
|
||||
for (const r of rules) {
|
||||
expect(r).not.toInclude('tcp');
|
||||
}
|
||||
expect(rules.some(r => r.includes('udp dport 53 dnat'))).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should build SNAT rule', async () => {
|
||||
const rules = buildSnatRule('mytable', 'ip', {
|
||||
sourceAddress: '192.168.1.1',
|
||||
targetPort: 80,
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(1);
|
||||
expect(rules[0]).toInclude('snat to 192.168.1.1');
|
||||
expect(rules[0]).toInclude('dport 80');
|
||||
});
|
||||
|
||||
tap.test('should build masquerade rule', async () => {
|
||||
const rules = buildMasqueradeRule('mytable', 'ip', {
|
||||
targetPort: 8080,
|
||||
protocol: 'both',
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(2);
|
||||
expect(rules[0]).toInclude('tcp');
|
||||
expect(rules[0]).toInclude('masquerade');
|
||||
expect(rules[1]).toInclude('udp');
|
||||
});
|
||||
|
||||
tap.test('should use correct family in commands', async () => {
|
||||
const rules = buildDnatRules('mytable', 'inet', {
|
||||
sourcePort: 80,
|
||||
targetHost: '10.0.0.1',
|
||||
targetPort: 8080,
|
||||
});
|
||||
|
||||
for (const r of rules) {
|
||||
expect(r).toInclude('inet mytable');
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
114
test/test.rulebuilder-ratelimit.ts
Normal file
114
test/test.rulebuilder-ratelimit.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
buildRateLimitRule,
|
||||
buildPerIpRateLimitRule,
|
||||
buildConnectionRateRule,
|
||||
} from '../ts/nft.rulebuilder.ratelimit.js';
|
||||
|
||||
tap.test('should build global rate limit rule', async () => {
|
||||
const rules = buildRateLimitRule('mytable', 'ip', {
|
||||
port: 80,
|
||||
protocol: 'tcp',
|
||||
rate: '100/second',
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(1);
|
||||
expect(rules[0]).toInclude('limit rate over 100/second');
|
||||
expect(rules[0]).toInclude('tcp dport 80');
|
||||
expect(rules[0]).toInclude('drop');
|
||||
});
|
||||
|
||||
tap.test('should build rate limit with burst', async () => {
|
||||
const rules = buildRateLimitRule('mytable', 'ip', {
|
||||
port: 8080,
|
||||
rate: '50/second',
|
||||
burst: 20,
|
||||
});
|
||||
|
||||
expect(rules[0]).toInclude('burst 20 packets');
|
||||
});
|
||||
|
||||
tap.test('should build rate limit with custom action', async () => {
|
||||
const rules = buildRateLimitRule('mytable', 'ip', {
|
||||
port: 80,
|
||||
rate: '100/second',
|
||||
action: 'reject',
|
||||
});
|
||||
|
||||
expect(rules[0]).toInclude('reject');
|
||||
expect(rules[0]).not.toInclude('drop');
|
||||
});
|
||||
|
||||
tap.test('should build per-IP rate limit using meters', async () => {
|
||||
const rules = buildRateLimitRule('mytable', 'ip', {
|
||||
port: 80,
|
||||
protocol: 'tcp',
|
||||
rate: '10/second',
|
||||
perSourceIP: true,
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(1);
|
||||
expect(rules[0]).toInclude('meter');
|
||||
expect(rules[0]).toInclude('ip saddr');
|
||||
expect(rules[0]).toInclude('limit rate over 10/second');
|
||||
});
|
||||
|
||||
tap.test('should build per-IP rate limit via convenience function', async () => {
|
||||
const rules = buildPerIpRateLimitRule('mytable', 'ip', {
|
||||
port: 443,
|
||||
rate: '50/second',
|
||||
});
|
||||
|
||||
expect(rules[0]).toInclude('meter');
|
||||
expect(rules[0]).toInclude('ip saddr');
|
||||
});
|
||||
|
||||
tap.test('should build rate limit on custom chain', async () => {
|
||||
const rules = buildRateLimitRule('mytable', 'ip', {
|
||||
port: 80,
|
||||
rate: '100/second',
|
||||
chain: 'forward',
|
||||
});
|
||||
|
||||
expect(rules[0]).toInclude('mytable forward');
|
||||
});
|
||||
|
||||
tap.test('should build connection rate rule', async () => {
|
||||
const rules = buildConnectionRateRule('mytable', 'ip', {
|
||||
port: 22,
|
||||
protocol: 'tcp',
|
||||
rate: '5/second',
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(1);
|
||||
expect(rules[0]).toInclude('ct state new');
|
||||
expect(rules[0]).toInclude('tcp dport 22');
|
||||
expect(rules[0]).toInclude('limit rate over 5/second');
|
||||
expect(rules[0]).toInclude('drop');
|
||||
});
|
||||
|
||||
tap.test('should build per-IP connection rate rule', async () => {
|
||||
const rules = buildConnectionRateRule('mytable', 'ip', {
|
||||
port: 80,
|
||||
rate: '10/second',
|
||||
perSourceIP: true,
|
||||
});
|
||||
|
||||
expect(rules[0]).toInclude('meter connrate');
|
||||
expect(rules[0]).toInclude('ip saddr');
|
||||
expect(rules[0]).toInclude('ct state new');
|
||||
});
|
||||
|
||||
tap.test('should build rate limit for both protocols', async () => {
|
||||
const rules = buildRateLimitRule('mytable', 'ip', {
|
||||
port: 53,
|
||||
protocol: 'both',
|
||||
rate: '100/second',
|
||||
});
|
||||
|
||||
expect(rules.length).toEqual(2);
|
||||
expect(rules[0]).toInclude('tcp');
|
||||
expect(rules[1]).toInclude('udp');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
45
test/test.rulebuilder-table.ts
Normal file
45
test/test.rulebuilder-table.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
buildTableSetup,
|
||||
buildFilterChains,
|
||||
buildTableCleanup,
|
||||
} from '../ts/nft.rulebuilder.table.js';
|
||||
|
||||
tap.test('should build table setup commands', async () => {
|
||||
const commands = buildTableSetup('mytest');
|
||||
expect(commands.length).toEqual(3);
|
||||
expect(commands[0]).toInclude('add table ip mytest');
|
||||
expect(commands[1]).toInclude('prerouting');
|
||||
expect(commands[1]).toInclude('nat hook prerouting');
|
||||
expect(commands[2]).toInclude('postrouting');
|
||||
expect(commands[2]).toInclude('nat hook postrouting');
|
||||
});
|
||||
|
||||
tap.test('should build table setup with custom family', async () => {
|
||||
const commands = buildTableSetup('mytest', 'inet');
|
||||
for (const cmd of commands) {
|
||||
expect(cmd).toInclude('inet mytest');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should build filter chains', async () => {
|
||||
const commands = buildFilterChains('mytest');
|
||||
expect(commands.length).toEqual(3);
|
||||
expect(commands[0]).toInclude('input');
|
||||
expect(commands[0]).toInclude('filter hook input');
|
||||
expect(commands[1]).toInclude('forward');
|
||||
expect(commands[2]).toInclude('output');
|
||||
});
|
||||
|
||||
tap.test('should build table cleanup command', async () => {
|
||||
const commands = buildTableCleanup('mytest');
|
||||
expect(commands.length).toEqual(1);
|
||||
expect(commands[0]).toInclude('delete table ip mytest');
|
||||
});
|
||||
|
||||
tap.test('should build table cleanup with custom family', async () => {
|
||||
const commands = buildTableCleanup('mytest', 'ip6');
|
||||
expect(commands[0]).toInclude('delete table ip6 mytest');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
155
test/test.smartnftables.ts
Normal file
155
test/test.smartnftables.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { SmartNftables } from '../ts/nft.manager.js';
|
||||
|
||||
tap.test('should create SmartNftables with default options', async () => {
|
||||
const nft = new SmartNftables();
|
||||
expect(nft.tableName).toEqual('smartnftables');
|
||||
expect(nft.family).toEqual('ip');
|
||||
expect(nft.nat).toBeDefined();
|
||||
expect(nft.firewall).toBeDefined();
|
||||
expect(nft.rateLimit).toBeDefined();
|
||||
});
|
||||
|
||||
tap.test('should create SmartNftables with custom options', async () => {
|
||||
const nft = new SmartNftables({
|
||||
tableName: 'myapp',
|
||||
family: 'inet',
|
||||
});
|
||||
expect(nft.tableName).toEqual('myapp');
|
||||
expect(nft.family).toEqual('inet');
|
||||
});
|
||||
|
||||
tap.test('should track rule groups when not root', async () => {
|
||||
const nft = new SmartNftables({ tableName: 'test' });
|
||||
await nft.initialize();
|
||||
|
||||
// NAT port forwarding
|
||||
await nft.nat.addPortForwarding('web', {
|
||||
sourcePort: 443,
|
||||
targetHost: '10.0.0.5',
|
||||
targetPort: 8443,
|
||||
});
|
||||
|
||||
const status = nft.status();
|
||||
expect(status.initialized).toBeTrue();
|
||||
expect(status.activeGroups).toBeGreaterThan(0);
|
||||
expect(status.groups['nat:web']).toBeDefined();
|
||||
expect(status.groups['nat:web'].ruleCount).toEqual(2); // DNAT + masquerade
|
||||
|
||||
await nft.cleanup();
|
||||
const statusAfter = nft.status();
|
||||
expect(statusAfter.initialized).toBeFalse();
|
||||
expect(statusAfter.activeGroups).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should track firewall rules when not root', async () => {
|
||||
const nft = new SmartNftables({ tableName: 'test' });
|
||||
await nft.initialize();
|
||||
|
||||
await nft.firewall.addRule('block-badguy', {
|
||||
direction: 'input',
|
||||
action: 'drop',
|
||||
sourceIP: '192.168.1.100',
|
||||
});
|
||||
|
||||
const status = nft.status();
|
||||
expect(status.groups['fw:block-badguy']).toBeDefined();
|
||||
expect(status.groups['fw:block-badguy'].ruleCount).toEqual(1);
|
||||
|
||||
await nft.firewall.removeRule('block-badguy');
|
||||
const statusAfter = nft.status();
|
||||
expect(statusAfter.groups['fw:block-badguy']).toBeUndefined();
|
||||
|
||||
await nft.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should track rate limit rules when not root', async () => {
|
||||
const nft = new SmartNftables({ tableName: 'test' });
|
||||
await nft.initialize();
|
||||
|
||||
await nft.rateLimit.addRateLimit('api-throttle', {
|
||||
port: 8080,
|
||||
protocol: 'tcp',
|
||||
rate: '100/second',
|
||||
burst: 50,
|
||||
perSourceIP: true,
|
||||
});
|
||||
|
||||
const status = nft.status();
|
||||
expect(status.groups['ratelimit:api-throttle']).toBeDefined();
|
||||
|
||||
await nft.rateLimit.removeRateLimit('api-throttle');
|
||||
const statusAfter = nft.status();
|
||||
expect(statusAfter.groups['ratelimit:api-throttle']).toBeUndefined();
|
||||
|
||||
await nft.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should handle blockIP convenience method', async () => {
|
||||
const nft = new SmartNftables({ tableName: 'test' });
|
||||
await nft.initialize();
|
||||
|
||||
await nft.firewall.blockIP('1.2.3.4');
|
||||
|
||||
const status = nft.status();
|
||||
expect(status.groups['fw:block-1_2_3_4']).toBeDefined();
|
||||
|
||||
await nft.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should handle stateful tracking convenience', async () => {
|
||||
const nft = new SmartNftables({ tableName: 'test' });
|
||||
await nft.initialize();
|
||||
|
||||
await nft.firewall.enableStatefulTracking('input');
|
||||
|
||||
const status = nft.status();
|
||||
expect(status.groups['fw:stateful:input']).toBeDefined();
|
||||
expect(status.groups['fw:stateful:input'].ruleCount).toEqual(2); // established+related, invalid
|
||||
|
||||
await nft.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should handle connection rate limiting', async () => {
|
||||
const nft = new SmartNftables({ tableName: 'test' });
|
||||
await nft.initialize();
|
||||
|
||||
await nft.rateLimit.addConnectionRateLimit('ssh-limit', {
|
||||
port: 22,
|
||||
protocol: 'tcp',
|
||||
rate: '5/second',
|
||||
perSourceIP: true,
|
||||
});
|
||||
|
||||
const status = nft.status();
|
||||
expect(status.groups['connrate:ssh-limit']).toBeDefined();
|
||||
|
||||
await nft.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should handle port range forwarding', async () => {
|
||||
const nft = new SmartNftables({ tableName: 'test' });
|
||||
await nft.initialize();
|
||||
|
||||
await nft.nat.addPortRange('gameservers', 27015, 27020, '10.0.0.10', 27015, 'udp');
|
||||
|
||||
const status = nft.status();
|
||||
const group = status.groups['nat:range:gameservers'];
|
||||
expect(group).toBeDefined();
|
||||
// 6 ports * 2 commands each (DNAT + masquerade) = 12
|
||||
expect(group.ruleCount).toEqual(12);
|
||||
|
||||
await nft.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should report correct status', async () => {
|
||||
const nft = new SmartNftables({ tableName: 'statustest', family: 'inet' });
|
||||
|
||||
const status = nft.status();
|
||||
expect(status.tableName).toEqual('statustest');
|
||||
expect(status.family).toEqual('inet');
|
||||
expect(status.initialized).toBeFalse();
|
||||
expect(status.activeGroups).toEqual(0);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user