159 lines
5.1 KiB
TypeScript
159 lines
5.1 KiB
TypeScript
![]() |
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
import { SharedSecurityManager } from '../ts/core/utils/shared-security-manager.js';
|
||
|
import type { IRouteConfig, IRouteContext } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||
|
|
||
|
let securityManager: SharedSecurityManager;
|
||
|
|
||
|
tap.test('Setup SharedSecurityManager', async () => {
|
||
|
securityManager = new SharedSecurityManager({
|
||
|
maxConnectionsPerIP: 5,
|
||
|
connectionRateLimitPerMinute: 10,
|
||
|
cleanupIntervalMs: 1000 // 1 second for faster testing
|
||
|
});
|
||
|
});
|
||
|
|
||
|
tap.test('IP connection tracking', async () => {
|
||
|
const testIP = '192.168.1.100';
|
||
|
|
||
|
// Track multiple connections
|
||
|
securityManager.trackConnectionByIP(testIP, 'conn1');
|
||
|
securityManager.trackConnectionByIP(testIP, 'conn2');
|
||
|
securityManager.trackConnectionByIP(testIP, 'conn3');
|
||
|
|
||
|
// Verify connection count
|
||
|
expect(securityManager.getConnectionCountByIP(testIP)).toEqual(3);
|
||
|
|
||
|
// Remove a connection
|
||
|
securityManager.removeConnectionByIP(testIP, 'conn2');
|
||
|
expect(securityManager.getConnectionCountByIP(testIP)).toEqual(2);
|
||
|
|
||
|
// Remove remaining connections
|
||
|
securityManager.removeConnectionByIP(testIP, 'conn1');
|
||
|
securityManager.removeConnectionByIP(testIP, 'conn3');
|
||
|
expect(securityManager.getConnectionCountByIP(testIP)).toEqual(0);
|
||
|
});
|
||
|
|
||
|
tap.test('Per-IP connection limits validation', async () => {
|
||
|
const testIP = '192.168.1.101';
|
||
|
|
||
|
// Track connections up to limit
|
||
|
for (let i = 1; i <= 5; i++) {
|
||
|
securityManager.trackConnectionByIP(testIP, `conn${i}`);
|
||
|
const result = securityManager.validateIP(testIP);
|
||
|
expect(result.allowed).toBeTrue();
|
||
|
}
|
||
|
|
||
|
// Verify we're at the limit
|
||
|
expect(securityManager.getConnectionCountByIP(testIP)).toEqual(5);
|
||
|
|
||
|
// Next connection should be rejected
|
||
|
const result = securityManager.validateIP(testIP);
|
||
|
expect(result.allowed).toBeFalse();
|
||
|
expect(result.reason).toInclude('Maximum connections per IP');
|
||
|
|
||
|
// Clean up
|
||
|
for (let i = 1; i <= 5; i++) {
|
||
|
securityManager.removeConnectionByIP(testIP, `conn${i}`);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('Connection rate limiting', async () => {
|
||
|
const testIP = '192.168.1.102';
|
||
|
|
||
|
// Make connections at the rate limit
|
||
|
for (let i = 0; i < 10; i++) {
|
||
|
const result = securityManager.validateIP(testIP);
|
||
|
expect(result.allowed).toBeTrue();
|
||
|
securityManager.trackConnectionByIP(testIP, `conn${i}`);
|
||
|
}
|
||
|
|
||
|
// Next connection should exceed rate limit
|
||
|
const result = securityManager.validateIP(testIP);
|
||
|
expect(result.allowed).toBeFalse();
|
||
|
expect(result.reason).toInclude('Connection rate limit');
|
||
|
|
||
|
// Clean up connections
|
||
|
for (let i = 0; i < 10; i++) {
|
||
|
securityManager.removeConnectionByIP(testIP, `conn${i}`);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('Route-level connection limits', async () => {
|
||
|
const route: IRouteConfig = {
|
||
|
name: 'test-route',
|
||
|
match: { ports: 443 },
|
||
|
action: { type: 'forward', target: { host: 'localhost', port: 8080 } },
|
||
|
security: {
|
||
|
maxConnections: 3
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const context: IRouteContext = {
|
||
|
port: 443,
|
||
|
clientIp: '192.168.1.103',
|
||
|
serverIp: '0.0.0.0',
|
||
|
timestamp: Date.now(),
|
||
|
connectionId: 'test-conn'
|
||
|
};
|
||
|
|
||
|
// Test with connection counts below limit
|
||
|
expect(securityManager.isAllowed(route, context, 0)).toBeTrue();
|
||
|
expect(securityManager.isAllowed(route, context, 2)).toBeTrue();
|
||
|
|
||
|
// Test at limit
|
||
|
expect(securityManager.isAllowed(route, context, 3)).toBeFalse();
|
||
|
|
||
|
// Test above limit
|
||
|
expect(securityManager.isAllowed(route, context, 5)).toBeFalse();
|
||
|
});
|
||
|
|
||
|
tap.test('IPv4/IPv6 normalization', async () => {
|
||
|
const ipv4 = '127.0.0.1';
|
||
|
const ipv4Mapped = '::ffff:127.0.0.1';
|
||
|
|
||
|
// Track connection with IPv4
|
||
|
securityManager.trackConnectionByIP(ipv4, 'conn1');
|
||
|
|
||
|
// Both representations should show the same connection
|
||
|
expect(securityManager.getConnectionCountByIP(ipv4)).toEqual(1);
|
||
|
expect(securityManager.getConnectionCountByIP(ipv4Mapped)).toEqual(1);
|
||
|
|
||
|
// Track another connection with IPv6 representation
|
||
|
securityManager.trackConnectionByIP(ipv4Mapped, 'conn2');
|
||
|
|
||
|
// Both should show 2 connections
|
||
|
expect(securityManager.getConnectionCountByIP(ipv4)).toEqual(2);
|
||
|
expect(securityManager.getConnectionCountByIP(ipv4Mapped)).toEqual(2);
|
||
|
|
||
|
// Clean up
|
||
|
securityManager.removeConnectionByIP(ipv4, 'conn1');
|
||
|
securityManager.removeConnectionByIP(ipv4Mapped, 'conn2');
|
||
|
});
|
||
|
|
||
|
tap.test('Automatic cleanup of expired data', async (tools) => {
|
||
|
const testIP = '192.168.1.104';
|
||
|
|
||
|
// Track a connection and then remove it
|
||
|
securityManager.trackConnectionByIP(testIP, 'temp-conn');
|
||
|
securityManager.removeConnectionByIP(testIP, 'temp-conn');
|
||
|
|
||
|
// Add some rate limit entries (they expire after 1 minute)
|
||
|
for (let i = 0; i < 5; i++) {
|
||
|
securityManager.validateIP(testIP);
|
||
|
}
|
||
|
|
||
|
// Wait for cleanup interval (set to 1 second in our test)
|
||
|
await tools.delayFor(1500);
|
||
|
|
||
|
// The IP should be cleaned up since it has no connections
|
||
|
// Note: We can't directly check the internal map, but we can verify
|
||
|
// that a new connection is allowed (fresh rate limit)
|
||
|
const result = securityManager.validateIP(testIP);
|
||
|
expect(result.allowed).toBeTrue();
|
||
|
});
|
||
|
|
||
|
tap.test('Cleanup SharedSecurityManager', async () => {
|
||
|
securityManager.clearIPTracking();
|
||
|
});
|
||
|
|
||
|
tap.start();
|