Files
smartproxy/test/test.sni-requirement.node.ts

386 lines
11 KiB
TypeScript
Raw Normal View History

/**
* 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();