386 lines
11 KiB
TypeScript
386 lines
11 KiB
TypeScript
/**
|
|
* Tests for smart SNI requirement calculation
|
|
*
|
|
* These tests verify that the calculateSniRequirement() method correctly determines
|
|
* when SNI (Server Name Indication) is required for routing decisions.
|
|
*/
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
|
|
|
// Use unique high ports for each test to avoid conflicts
|
|
let testPort = 20000;
|
|
const getNextPort = () => testPort++;
|
|
|
|
// --------------------------------- Single Route, No Domain Restriction ---------------------------------
|
|
|
|
tap.test('SNI Requirement: Single passthrough, no domains, static target - should allow session tickets', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'passthrough-no-domains',
|
|
match: { ports: port },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend-server', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(routesOnPort[0].action.tls?.mode).toEqual('passthrough');
|
|
expect(routesOnPort[0].match.domains).toBeUndefined();
|
|
expect(typeof routesOnPort[0].action.targets?.[0].host).toEqual('string');
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('SNI Requirement: Single passthrough, domains: "*", static target - should allow session tickets', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'passthrough-wildcard-domain',
|
|
match: { ports: port, domains: '*' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend-server', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(routesOnPort[0].match.domains).toEqual('*');
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('SNI Requirement: Single passthrough, domains: ["*"], static target - should allow session tickets', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'passthrough-wildcard-array',
|
|
match: { ports: port, domains: ['*'] },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend-server', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(routesOnPort[0].match.domains).toEqual(['*']);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
// --------------------------------- Single Route, Specific Domain ---------------------------------
|
|
|
|
tap.test('SNI Requirement: Single passthrough, specific domain - should require SNI (block session tickets)', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'passthrough-specific-domain',
|
|
match: { ports: port, domains: 'api.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend-server', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(routesOnPort[0].match.domains).toEqual('api.example.com');
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('SNI Requirement: Single passthrough, multiple specific domains - should require SNI', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'passthrough-multiple-domains',
|
|
match: { ports: port, domains: ['a.example.com', 'b.example.com'] },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend-server', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(routesOnPort[0].match.domains).toEqual(['a.example.com', 'b.example.com']);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('SNI Requirement: Single passthrough, pattern domain - should require SNI', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'passthrough-pattern-domain',
|
|
match: { ports: port, domains: '*.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend-server', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(routesOnPort[0].match.domains).toEqual('*.example.com');
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
// --------------------------------- Single Route, Dynamic Target ---------------------------------
|
|
|
|
tap.test('SNI Requirement: Single passthrough, dynamic host function - should require SNI', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'passthrough-dynamic-host',
|
|
match: { ports: port },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{
|
|
host: (context) => {
|
|
if (context.domain === 'api.example.com') return 'api-backend';
|
|
return 'web-backend';
|
|
},
|
|
port: 9443
|
|
}],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(typeof routesOnPort[0].action.targets?.[0].host).toEqual('function');
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
// --------------------------------- Multiple Routes on Same Port ---------------------------------
|
|
|
|
tap.test('SNI Requirement: Multiple passthrough routes on same port - should require SNI', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [
|
|
{
|
|
name: 'passthrough-api',
|
|
match: { ports: port, domains: 'api.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'api-backend', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
},
|
|
{
|
|
name: 'passthrough-web',
|
|
match: { ports: port, domains: 'web.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'web-backend', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}
|
|
];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(2);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
// --------------------------------- TLS Termination Routes (route config only, no actual cert provisioning) ---------------------------------
|
|
|
|
tap.test('SNI Requirement: Terminate route config is correctly identified', async () => {
|
|
const port = getNextPort();
|
|
// Test route configuration without starting the proxy (avoids cert provisioning)
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'terminate-route',
|
|
match: { ports: port, domains: 'secure.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend', port: 8080 }],
|
|
tls: {
|
|
mode: 'terminate',
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
}];
|
|
|
|
// Just verify route config is valid without starting (no ACME timeout)
|
|
const proxy = new SmartProxy({
|
|
routes,
|
|
acme: { email: 'test@example.com', useProduction: false }
|
|
});
|
|
|
|
// Check route manager directly (before start)
|
|
expect(routes[0].action.tls?.mode).toEqual('terminate');
|
|
expect(routes.length).toEqual(1);
|
|
});
|
|
|
|
tap.test('SNI Requirement: Mixed terminate + passthrough config is correctly identified', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [
|
|
{
|
|
name: 'terminate-secure',
|
|
match: { ports: port, domains: 'secure.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'secure-backend', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' }
|
|
}
|
|
},
|
|
{
|
|
name: 'passthrough-raw',
|
|
match: { ports: port, domains: 'passthrough.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'passthrough-backend', port: 9443 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}
|
|
];
|
|
|
|
// Verify route configs without starting
|
|
const hasTerminate = routes.some(r => r.action.tls?.mode === 'terminate');
|
|
const hasPassthrough = routes.some(r => r.action.tls?.mode === 'passthrough');
|
|
|
|
expect(hasTerminate).toBeTrue();
|
|
expect(hasPassthrough).toBeTrue();
|
|
expect(routes.length).toEqual(2);
|
|
});
|
|
|
|
tap.test('SNI Requirement: terminate-and-reencrypt config is correctly identified', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'reencrypt-route',
|
|
match: { ports: port, domains: 'reencrypt.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend', port: 9443 }],
|
|
tls: {
|
|
mode: 'terminate-and-reencrypt',
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
}];
|
|
|
|
// Verify route config without starting
|
|
expect(routes[0].action.tls?.mode).toEqual('terminate-and-reencrypt');
|
|
});
|
|
|
|
// --------------------------------- Edge Cases ---------------------------------
|
|
|
|
tap.test('SNI Requirement: No routes on port - should not require SNI', async () => {
|
|
const routePort = getNextPort();
|
|
const queryPort = getNextPort();
|
|
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'different-port-route',
|
|
match: { ports: routePort },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: 'backend', port: 8080 }],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnQueryPort = proxy.routeManager.getRoutesForPort(queryPort);
|
|
|
|
expect(routesOnQueryPort.length).toEqual(0);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('SNI Requirement: Multiple static targets in single route - should not require SNI', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'multiple-static-targets',
|
|
match: { ports: port },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [
|
|
{ host: 'backend1', port: 9443 },
|
|
{ host: 'backend2', port: 9443 }
|
|
],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(routesOnPort[0].action.targets?.length).toEqual(2);
|
|
expect(typeof routesOnPort[0].action.targets?.[0].host).toEqual('string');
|
|
expect(typeof routesOnPort[0].action.targets?.[1].host).toEqual('string');
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('SNI Requirement: Host array (load balancing) is still static - should not require SNI', async () => {
|
|
const port = getNextPort();
|
|
const routes: IRouteConfig[] = [{
|
|
name: 'host-array-static',
|
|
match: { ports: port },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{
|
|
host: ['backend1', 'backend2', 'backend3'],
|
|
port: 9443
|
|
}],
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}];
|
|
|
|
const proxy = new SmartProxy({ routes });
|
|
await proxy.start();
|
|
|
|
const routesOnPort = proxy.routeManager.getRoutesForPort(port);
|
|
|
|
expect(routesOnPort.length).toEqual(1);
|
|
expect(Array.isArray(routesOnPort[0].action.targets?.[0].host)).toBeTrue();
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
export default tap.start();
|