Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65822278d5 | |||
| aa3955fc67 | |||
| d4605062bb | |||
| cd3f08d55f | |||
| 6d447f0086 | |||
| c7de3873d8 |
20
changelog.md
20
changelog.md
@@ -1,5 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-19 - 11.7.1 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.16.0
|
||||
|
||||
- updates the smartproxy dependency from ^25.15.0 to ^25.16.0
|
||||
|
||||
## 2026-03-19 - 11.7.0 - feat(readme)
|
||||
document HTTP/3 QUIC support and configuration options
|
||||
|
||||
- Add a dedicated README section explaining default HTTP/3 route augmentation, qualification rules, and opt-out behavior.
|
||||
- Document the new global `http3` configuration shape and re-exported `IHttp3Config` type.
|
||||
- Update TypeScript module documentation to include the built-in HTTP/3 augmentation module and exports.
|
||||
|
||||
## 2026-03-19 - 11.6.0 - feat(http3)
|
||||
add automatic HTTP/3 route augmentation for qualifying HTTPS routes
|
||||
|
||||
- introduce configurable HTTP/3 augmentation utilities for eligible SmartProxy routes on port 443
|
||||
- apply HTTP/3 settings to both constructor-defined and stored programmatic routes, with global and per-route opt-out support
|
||||
- export the HTTP/3 config type and add test coverage for qualification, augmentation behavior, and defaults
|
||||
- bump @push.rocks/smartproxy to ^25.15.0 for HTTP/3-related support
|
||||
|
||||
## 2026-03-19 - 11.5.1 - fix(project)
|
||||
no changes to commit
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "11.5.1",
|
||||
"version": "11.7.1",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -53,7 +53,7 @@
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^25.14.1",
|
||||
"@push.rocks/smartproxy": "^25.16.0",
|
||||
"@push.rocks/smartradius": "^1.1.1",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -78,8 +78,8 @@ importers:
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^25.14.1
|
||||
version: 25.14.1
|
||||
specifier: ^25.16.0
|
||||
version: 25.16.0
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -1256,8 +1256,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.3':
|
||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||
|
||||
'@push.rocks/smartproxy@25.14.1':
|
||||
resolution: {integrity: sha512-QXJ1M7Or81lmCusAKkmIB8M9jJwl1/AKItnmTkn8IQ9zsPd6r+0uhP1j5tCO/LwRQRpzOADnpSrpVcrtMKK9kQ==}
|
||||
'@push.rocks/smartproxy@25.16.0':
|
||||
resolution: {integrity: sha512-X3XtOjbYUHukKeZDBWO2g69k3otYI8JsA8RQdQySTuAKPnqj4QjhFJRfkgc+jGgWaWk3j5Mq5RMGsoVOmu4VGA==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.5':
|
||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||
@@ -6539,7 +6539,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.3': {}
|
||||
|
||||
'@push.rocks/smartproxy@25.14.1':
|
||||
'@push.rocks/smartproxy@25.16.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartlog': 3.2.1
|
||||
|
||||
125
readme.md
125
readme.md
@@ -18,6 +18,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- [Architecture](#architecture)
|
||||
- [Configuration Reference](#configuration-reference)
|
||||
- [HTTP/HTTPS & TCP/SNI Routing](#httphttps--tcpsni-routing)
|
||||
- [HTTP/3 (QUIC) Support](#http3-quic-support)
|
||||
- [Email System](#email-system)
|
||||
- [DNS Server](#dns-server)
|
||||
- [RADIUS Server](#radius-server)
|
||||
@@ -37,6 +38,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
|
||||
### 🌐 Universal Traffic Router
|
||||
- **HTTP/HTTPS routing** with domain matching, path-based forwarding, and automatic TLS
|
||||
- **HTTP/3 (QUIC) enabled by default** — qualifying HTTPS routes automatically get QUIC/H3 support with zero configuration
|
||||
- **TCP/SNI proxy** for any protocol with TLS termination or passthrough
|
||||
- **DNS server** (Rust-powered via [SmartDNS](https://code.foss.global/push.rocks/smartdns)) with authoritative zones, dynamic record management, and DNS-over-HTTPS
|
||||
- **Multi-protocol support** on the same infrastructure via [SmartProxy](https://code.foss.global/push.rocks/smartproxy)
|
||||
@@ -425,6 +427,27 @@ interface IDcRouterOptions {
|
||||
};
|
||||
};
|
||||
|
||||
// ── HTTP/3 (QUIC) ────────────────────────────────────────────
|
||||
/** HTTP/3 config — enabled by default on qualifying HTTPS routes */
|
||||
http3?: {
|
||||
enabled?: boolean; // default: true
|
||||
quicSettings?: {
|
||||
maxIdleTimeout?: number; // default: 30000ms
|
||||
maxConcurrentBidiStreams?: number; // default: 100
|
||||
maxConcurrentUniStreams?: number; // default: 100
|
||||
initialCongestionWindow?: number;
|
||||
};
|
||||
altSvc?: {
|
||||
port?: number; // default: listening port
|
||||
maxAge?: number; // default: 86400s
|
||||
};
|
||||
udpSettings?: {
|
||||
sessionTimeout?: number; // default: 60000ms
|
||||
maxSessionsPerIP?: number; // default: 1000
|
||||
maxDatagramSize?: number; // default: 65535
|
||||
};
|
||||
};
|
||||
|
||||
// ── OpsServer ────────────────────────────────────────────────
|
||||
/** Port for the OpsServer web dashboard (default: 3000) */
|
||||
opsServerPort?: number;
|
||||
@@ -516,6 +539,102 @@ DcRouter uses [SmartProxy](https://code.foss.global/push.rocks/smartproxy) for a
|
||||
}
|
||||
```
|
||||
|
||||
## HTTP/3 (QUIC) Support
|
||||
|
||||
DcRouter ships with **HTTP/3 enabled by default** 🚀. All qualifying HTTPS routes on port 443 are automatically augmented with QUIC/H3 configuration — no extra setup needed. Under the hood, SmartProxy's native HTTP/3 support (via `IRouteQuic`) handles QUIC transport, Alt-Svc advertisement, and HTTP/3 negotiation.
|
||||
|
||||
### How It Works
|
||||
|
||||
When DcRouter assembles routes in `setupSmartProxy()`, it automatically augments qualifying routes with:
|
||||
- `match.transport: 'all'` — listen on both TCP (HTTP/1.1 + HTTP/2) and UDP (QUIC/HTTP/3) on the same port
|
||||
- `action.udp.quic` — QUIC configuration with `enableHttp3: true` and `altSvcMaxAge: 86400`
|
||||
|
||||
Browsers that support HTTP/3 will discover it via the `Alt-Svc` header on initial TCP responses, then upgrade to QUIC for subsequent requests.
|
||||
|
||||
### What Gets Augmented
|
||||
|
||||
A route qualifies for HTTP/3 augmentation when **all** of these are true:
|
||||
- Port includes **443** (single number, array, or range)
|
||||
- Action type is **`forward`** (not `socket-handler`)
|
||||
- **TLS is enabled** (passthrough, terminate, or terminate-and-reencrypt)
|
||||
- Route is **not** an email route (ports 25/587/465)
|
||||
- Route doesn't already have `transport: 'all'` or existing `udp.quic` config
|
||||
|
||||
### Zero-Config (Default Behavior)
|
||||
|
||||
```typescript
|
||||
// HTTP/3 is ON by default — this route automatically gets QUIC/H3:
|
||||
const router = new DcRouter({
|
||||
smartProxyConfig: {
|
||||
routes: [{
|
||||
name: 'web-app',
|
||||
match: { domains: ['example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: '192.168.1.10', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' }
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Per-Route Opt-Out
|
||||
|
||||
Disable HTTP/3 on a specific route using `action.options.http3`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
name: 'legacy-app',
|
||||
match: { domains: ['legacy.example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: '192.168.1.50', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
options: { http3: false } // ← This route stays TCP-only
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Opt-Out
|
||||
|
||||
Disable HTTP/3 across all routes:
|
||||
|
||||
```typescript
|
||||
const router = new DcRouter({
|
||||
http3: { enabled: false },
|
||||
smartProxyConfig: { routes: [/* ... */] }
|
||||
});
|
||||
```
|
||||
|
||||
### Custom QUIC Settings
|
||||
|
||||
Fine-tune QUIC parameters globally:
|
||||
|
||||
```typescript
|
||||
const router = new DcRouter({
|
||||
http3: {
|
||||
quicSettings: {
|
||||
maxIdleTimeout: 60000, // 60s idle timeout
|
||||
maxConcurrentBidiStreams: 200, // More parallel streams
|
||||
maxConcurrentUniStreams: 50,
|
||||
},
|
||||
altSvc: {
|
||||
maxAge: 3600, // 1 hour Alt-Svc cache
|
||||
},
|
||||
udpSettings: {
|
||||
sessionTimeout: 120000, // 2 min UDP session timeout
|
||||
maxSessionsPerIP: 500,
|
||||
}
|
||||
},
|
||||
smartProxyConfig: { routes: [/* ... */] }
|
||||
});
|
||||
```
|
||||
|
||||
### Programmatic Routes
|
||||
|
||||
Routes added at runtime via the Route Management API also get HTTP/3 augmentation automatically — the `RouteConfigManager` applies the same augmentation logic when merging programmatic routes.
|
||||
|
||||
## Email System
|
||||
|
||||
The email system is powered by [`@push.rocks/smartmta`](https://code.foss.global/push.rocks/smartmta), a TypeScript + Rust hybrid MTA. DcRouter configures and orchestrates smartmta's **UnifiedEmailServer**, which handles SMTP sessions, route matching, delivery queuing, DKIM signing, and all email processing.
|
||||
@@ -1221,7 +1340,7 @@ const router = new DcRouter(options: IDcRouterOptions);
|
||||
|
||||
### Re-exported Types
|
||||
|
||||
DcRouter re-exports key types from smartmta for convenience:
|
||||
DcRouter re-exports key types for convenience:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
@@ -1231,6 +1350,7 @@ import {
|
||||
type IUnifiedEmailServerOptions,
|
||||
type IEmailRoute,
|
||||
type IEmailDomainConfig,
|
||||
type IHttp3Config,
|
||||
} from '@serve.zone/dcrouter';
|
||||
```
|
||||
|
||||
@@ -1277,9 +1397,10 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
||||
| `test.dns-server-config.ts` | DNS record parsing, grouping, extraction | 5 |
|
||||
| `test.dns-socket-handler.ts` | DNS socket handler and route generation | 6 |
|
||||
| `test.errors.ts` | Error classes, handler, retry utilities | 5 |
|
||||
| `test.http3-augmentation.ts` | HTTP/3 route augmentation, qualification, opt-in/out, QUIC settings | 20 |
|
||||
| `test.ipreputationchecker.ts` | IP reputation, DNSBL, caching, risk classification | 10 |
|
||||
| `test.jwt-auth.ts` | JWT login, verification, logout, invalid credentials | 8 |
|
||||
| `test.opsserver-api.ts` | Health, statistics, configuration, log APIs | 6 |
|
||||
| `test.opsserver-api.ts` | Health, statistics, configuration, log APIs | 8 |
|
||||
| `test.protected-endpoint.ts` | Admin auth, identity verification, public endpoints | 8 |
|
||||
| `test.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 8 |
|
||||
|
||||
|
||||
304
test/test.http3-augmentation.ts
Normal file
304
test/test.http3-augmentation.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
routeQualifiesForHttp3,
|
||||
augmentRouteWithHttp3,
|
||||
augmentRoutesWithHttp3,
|
||||
type IHttp3Config,
|
||||
} from '../ts/http3/index.js';
|
||||
import type * as plugins from '../ts/plugins.js';
|
||||
|
||||
// Helper to create a basic HTTPS forward route on port 443
|
||||
function makeRoute(
|
||||
overrides: Partial<plugins.smartproxy.IRouteConfig> = {},
|
||||
): plugins.smartproxy.IRouteConfig {
|
||||
return {
|
||||
match: { ports: 443, ...overrides.match },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
...overrides.action,
|
||||
},
|
||||
name: overrides.name ?? 'test-https-route',
|
||||
...Object.fromEntries(
|
||||
Object.entries(overrides).filter(([k]) => !['match', 'action', 'name'].includes(k)),
|
||||
),
|
||||
} as plugins.smartproxy.IRouteConfig;
|
||||
}
|
||||
|
||||
const defaultConfig: IHttp3Config = { enabled: true };
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Qualification tests
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tap.test('should augment qualifying HTTPS route on port 443', async () => {
|
||||
const route = makeRoute();
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp).toBeTruthy();
|
||||
expect(result.action.udp!.quic).toBeTruthy();
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(86400);
|
||||
});
|
||||
|
||||
tap.test('should NOT augment route on non-443 port', async () => {
|
||||
const route = makeRoute({ match: { ports: 8080 } });
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
expect(result.action.udp).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should NOT augment socket-handler type route', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'socket-handler' as any,
|
||||
socketHandler: (() => {}) as any,
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should NOT augment route without TLS', async () => {
|
||||
const route: plugins.smartproxy.IRouteConfig = {
|
||||
match: { ports: 443 },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
},
|
||||
name: 'no-tls-route',
|
||||
};
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should NOT augment email routes', async () => {
|
||||
const emailNames = ['smtp-route', 'submission-route', 'smtps-route', 'email-port-2525-route'];
|
||||
for (const name of emailNames) {
|
||||
const route = makeRoute({ name });
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should respect per-route opt-out (options.http3 = false)', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
options: { http3: false },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
expect(result.action.udp).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should respect per-route opt-in when global is disabled', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
options: { http3: true },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, { enabled: false });
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should NOT double-augment routes with transport: all', async () => {
|
||||
const route = makeRoute({
|
||||
match: { ports: 443, transport: 'all' as any },
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
// Should be the exact same object (no augmentation)
|
||||
expect(result).toEqual(route);
|
||||
});
|
||||
|
||||
tap.test('should NOT double-augment routes with existing udp.quic', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
udp: { quic: { enableHttp3: true } },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result).toEqual(route);
|
||||
});
|
||||
|
||||
tap.test('should augment route with port range including 443', async () => {
|
||||
const route = makeRoute({
|
||||
match: { ports: [{ from: 400, to: 500 }] },
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should augment route with port array including 443', async () => {
|
||||
const route = makeRoute({
|
||||
match: { ports: [80, 443] },
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should NOT augment route with port range NOT including 443', async () => {
|
||||
const route = makeRoute({
|
||||
match: { ports: [{ from: 8000, to: 9000 }] },
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should augment TLS passthrough routes', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'passthrough' },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should augment terminate-and-reencrypt routes', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Configuration tests
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tap.test('should apply default QUIC settings when none provided', async () => {
|
||||
const route = makeRoute();
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(86400);
|
||||
// Undefined means SmartProxy will use its own defaults
|
||||
expect(result.action.udp!.quic!.maxIdleTimeout).toBeUndefined();
|
||||
expect(result.action.udp!.quic!.altSvcPort).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should apply custom QUIC settings', async () => {
|
||||
const route = makeRoute();
|
||||
const config: IHttp3Config = {
|
||||
enabled: true,
|
||||
quicSettings: {
|
||||
maxIdleTimeout: 60000,
|
||||
maxConcurrentBidiStreams: 200,
|
||||
maxConcurrentUniStreams: 50,
|
||||
initialCongestionWindow: 65536,
|
||||
},
|
||||
altSvc: {
|
||||
port: 8443,
|
||||
maxAge: 3600,
|
||||
},
|
||||
udpSettings: {
|
||||
sessionTimeout: 120000,
|
||||
maxSessionsPerIP: 500,
|
||||
maxDatagramSize: 32768,
|
||||
},
|
||||
};
|
||||
const result = augmentRouteWithHttp3(route, config);
|
||||
|
||||
expect(result.action.udp!.quic!.maxIdleTimeout).toEqual(60000);
|
||||
expect(result.action.udp!.quic!.maxConcurrentBidiStreams).toEqual(200);
|
||||
expect(result.action.udp!.quic!.maxConcurrentUniStreams).toEqual(50);
|
||||
expect(result.action.udp!.quic!.initialCongestionWindow).toEqual(65536);
|
||||
expect(result.action.udp!.quic!.altSvcPort).toEqual(8443);
|
||||
expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(3600);
|
||||
expect(result.action.udp!.sessionTimeout).toEqual(120000);
|
||||
expect(result.action.udp!.maxSessionsPerIP).toEqual(500);
|
||||
expect(result.action.udp!.maxDatagramSize).toEqual(32768);
|
||||
});
|
||||
|
||||
tap.test('should not mutate the original route', async () => {
|
||||
const route = makeRoute();
|
||||
const originalTransport = route.match.transport;
|
||||
const originalUdp = route.action.udp;
|
||||
|
||||
augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(route.match.transport).toEqual(originalTransport);
|
||||
expect(route.action.udp).toEqual(originalUdp);
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Batch augmentation
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tap.test('should augment multiple routes in a batch', async () => {
|
||||
const routes = [
|
||||
makeRoute({ name: 'web-app' }),
|
||||
makeRoute({ name: 'smtp-route', match: { ports: 25 } }),
|
||||
makeRoute({ name: 'api-gateway' }),
|
||||
makeRoute({
|
||||
name: 'dns-query',
|
||||
action: { type: 'socket-handler' as any, socketHandler: (() => {}) as any },
|
||||
}),
|
||||
];
|
||||
|
||||
const results = augmentRoutesWithHttp3(routes, defaultConfig);
|
||||
|
||||
// web-app and api-gateway should be augmented
|
||||
expect(results[0].match.transport).toEqual('all');
|
||||
expect(results[2].match.transport).toEqual('all');
|
||||
|
||||
// smtp and dns should NOT be augmented
|
||||
expect(results[1].match.transport).toBeUndefined();
|
||||
expect(results[3].match.transport).toBeUndefined();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Default enabled behavior
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tap.test('should treat undefined enabled as true (default on)', async () => {
|
||||
const route = makeRoute();
|
||||
const result = augmentRouteWithHttp3(route, {}); // no enabled field at all
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should disable when enabled is explicitly false', async () => {
|
||||
const route = makeRoute();
|
||||
const result = augmentRouteWithHttp3(route, { enabled: false });
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
expect(result.action.udp).toBeUndefined();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '11.5.1',
|
||||
version: '11.7.1',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
||||
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||
import { RouteConfigManager, ApiTokenManager } from './config/index.js';
|
||||
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
|
||||
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||
@@ -163,6 +164,14 @@ export interface IDcRouterOptions {
|
||||
* Remote Ingress configuration for edge tunnel nodes
|
||||
* Enables edge nodes to accept incoming connections and tunnel them to this DcRouter
|
||||
*/
|
||||
/**
|
||||
* HTTP/3 (QUIC) configuration for HTTPS routes.
|
||||
* Enabled by default — qualifying HTTPS routes on port 443 are automatically
|
||||
* augmented with QUIC/H3 fields. Set { enabled: false } to disable globally.
|
||||
* Individual routes can opt out via action.options.http3 = false.
|
||||
*/
|
||||
http3?: IHttp3Config;
|
||||
|
||||
/** Port for the OpsServer web UI (default: 3000) */
|
||||
opsServerPort?: number;
|
||||
|
||||
@@ -297,6 +306,7 @@ export class DcRouter {
|
||||
this.storageManager,
|
||||
() => this.getConstructorRoutes(),
|
||||
() => this.smartProxy,
|
||||
() => this.options.http3,
|
||||
);
|
||||
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
||||
await this.apiTokenManager.initialize();
|
||||
@@ -469,6 +479,13 @@ export class DcRouter {
|
||||
challengeHandlers.push(dns01Handler);
|
||||
}
|
||||
|
||||
// HTTP/3 augmentation (enabled by default unless explicitly disabled)
|
||||
if (this.options.http3?.enabled !== false) {
|
||||
const http3Config: IHttp3Config = { enabled: true, ...this.options.http3 };
|
||||
routes = augmentRoutesWithHttp3(routes, http3Config);
|
||||
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
||||
}
|
||||
|
||||
// Cache constructor routes for RouteConfigManager
|
||||
this.constructorRoutes = [...routes];
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
IMergedRoute,
|
||||
IRouteWarning,
|
||||
} from '../../ts_interfaces/data/route-management.js';
|
||||
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
|
||||
|
||||
const ROUTES_PREFIX = '/config-api/routes/';
|
||||
const OVERRIDES_PREFIX = '/config-api/overrides/';
|
||||
@@ -20,6 +21,7 @@ export class RouteConfigManager {
|
||||
private storageManager: StorageManager,
|
||||
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
||||
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
||||
private getHttp3Config?: () => IHttp3Config | undefined,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -258,10 +260,15 @@ export class RouteConfigManager {
|
||||
enabledRoutes.push(route);
|
||||
}
|
||||
|
||||
// Add enabled programmatic routes
|
||||
// Add enabled programmatic routes (with HTTP/3 augmentation if enabled)
|
||||
const http3Config = this.getHttp3Config?.();
|
||||
for (const stored of this.storedRoutes.values()) {
|
||||
if (stored.enabled) {
|
||||
enabledRoutes.push(stored.route);
|
||||
if (http3Config && http3Config.enabled !== false) {
|
||||
enabledRoutes.push(augmentRouteWithHttp3(stored.route, { enabled: true, ...http3Config }));
|
||||
} else {
|
||||
enabledRoutes.push(stored.route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
153
ts/http3/http3-route-augmentation.ts
Normal file
153
ts/http3/http3-route-augmentation.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Configuration for HTTP/3 (QUIC) route augmentation.
|
||||
* HTTP/3 is enabled by default on all qualifying HTTPS routes.
|
||||
*/
|
||||
export interface IHttp3Config {
|
||||
/** Enable HTTP/3 augmentation on qualifying routes (default: true) */
|
||||
enabled?: boolean;
|
||||
/** QUIC-specific settings applied to all augmented routes */
|
||||
quicSettings?: {
|
||||
/** QUIC connection idle timeout in ms (default: 30000) */
|
||||
maxIdleTimeout?: number;
|
||||
/** Max concurrent bidirectional streams per connection (default: 100) */
|
||||
maxConcurrentBidiStreams?: number;
|
||||
/** Max concurrent unidirectional streams per connection (default: 100) */
|
||||
maxConcurrentUniStreams?: number;
|
||||
/** Initial congestion window size in bytes */
|
||||
initialCongestionWindow?: number;
|
||||
};
|
||||
/** Alt-Svc header settings */
|
||||
altSvc?: {
|
||||
/** Port advertised in Alt-Svc header (default: same as listening port) */
|
||||
port?: number;
|
||||
/** Max age for Alt-Svc advertisement in seconds (default: 86400) */
|
||||
maxAge?: number;
|
||||
};
|
||||
/** UDP session settings */
|
||||
udpSettings?: {
|
||||
/** Idle timeout for UDP sessions in ms (default: 60000) */
|
||||
sessionTimeout?: number;
|
||||
/** Max concurrent UDP sessions per source IP (default: 1000) */
|
||||
maxSessionsPerIP?: number;
|
||||
/** Max accepted datagram size in bytes (default: 65535) */
|
||||
maxDatagramSize?: number;
|
||||
};
|
||||
}
|
||||
|
||||
type TPortRange = plugins.smartproxy.IRouteConfig['match']['ports'];
|
||||
|
||||
/**
|
||||
* Check whether a TPortRange includes port 443.
|
||||
*/
|
||||
function portRangeIncludes443(ports: TPortRange): boolean {
|
||||
if (typeof ports === 'number') return ports === 443;
|
||||
if (Array.isArray(ports)) {
|
||||
return ports.some((p) => {
|
||||
if (typeof p === 'number') return p === 443;
|
||||
return p.from <= 443 && p.to >= 443;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route name indicates an email route that should not get HTTP/3.
|
||||
*/
|
||||
function isEmailRoute(route: plugins.smartproxy.IRouteConfig): boolean {
|
||||
const name = route.name?.toLowerCase() || '';
|
||||
return (
|
||||
name.startsWith('smtp-') ||
|
||||
name.startsWith('submission-') ||
|
||||
name.startsWith('smtps-') ||
|
||||
name.startsWith('email-')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a route qualifies for HTTP/3 augmentation.
|
||||
*/
|
||||
export function routeQualifiesForHttp3(
|
||||
route: plugins.smartproxy.IRouteConfig,
|
||||
globalConfig: IHttp3Config,
|
||||
): boolean {
|
||||
// Check global enable + per-route override
|
||||
const globalEnabled = globalConfig.enabled !== false; // default true
|
||||
const perRouteOverride = route.action.options?.http3;
|
||||
|
||||
// If per-route explicitly set, use that; otherwise use global
|
||||
const shouldAugment =
|
||||
perRouteOverride !== undefined ? perRouteOverride : globalEnabled;
|
||||
if (!shouldAugment) return false;
|
||||
|
||||
// Must be forward type
|
||||
if (route.action.type !== 'forward') return false;
|
||||
|
||||
// Must include port 443
|
||||
if (!portRangeIncludes443(route.match.ports)) return false;
|
||||
|
||||
// Must have TLS
|
||||
if (!route.action.tls) return false;
|
||||
|
||||
// Skip email routes
|
||||
if (isEmailRoute(route)) return false;
|
||||
|
||||
// Skip if already configured with transport 'all' or 'udp'
|
||||
if (route.match.transport === 'all' || route.match.transport === 'udp') return false;
|
||||
|
||||
// Skip if already has QUIC config
|
||||
if (route.action.udp?.quic) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment a single route with HTTP/3 fields.
|
||||
* Returns a new route object (does not mutate the original).
|
||||
*/
|
||||
export function augmentRouteWithHttp3(
|
||||
route: plugins.smartproxy.IRouteConfig,
|
||||
config: IHttp3Config,
|
||||
): plugins.smartproxy.IRouteConfig {
|
||||
if (!routeQualifiesForHttp3(route, config)) {
|
||||
return route;
|
||||
}
|
||||
|
||||
return {
|
||||
...route,
|
||||
match: {
|
||||
...route.match,
|
||||
transport: 'all' as const,
|
||||
},
|
||||
action: {
|
||||
...route.action,
|
||||
udp: {
|
||||
...(route.action.udp || {}),
|
||||
sessionTimeout: config.udpSettings?.sessionTimeout,
|
||||
maxSessionsPerIP: config.udpSettings?.maxSessionsPerIP,
|
||||
maxDatagramSize: config.udpSettings?.maxDatagramSize,
|
||||
quic: {
|
||||
enableHttp3: true,
|
||||
maxIdleTimeout: config.quicSettings?.maxIdleTimeout,
|
||||
maxConcurrentBidiStreams: config.quicSettings?.maxConcurrentBidiStreams,
|
||||
maxConcurrentUniStreams: config.quicSettings?.maxConcurrentUniStreams,
|
||||
altSvcPort: config.altSvc?.port,
|
||||
altSvcMaxAge: config.altSvc?.maxAge ?? 86400,
|
||||
initialCongestionWindow: config.quicSettings?.initialCongestionWindow,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment all qualifying routes in an array.
|
||||
* Returns a new array (does not mutate originals).
|
||||
*/
|
||||
export function augmentRoutesWithHttp3(
|
||||
routes: plugins.smartproxy.IRouteConfig[],
|
||||
config: IHttp3Config,
|
||||
): plugins.smartproxy.IRouteConfig[] {
|
||||
return routes.map((route) => augmentRouteWithHttp3(route, config));
|
||||
}
|
||||
1
ts/http3/index.ts
Normal file
1
ts/http3/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './http3-route-augmentation.js';
|
||||
@@ -14,6 +14,9 @@ export * from './radius/index.js';
|
||||
// Remote Ingress module
|
||||
export * from './remoteingress/index.js';
|
||||
|
||||
// HTTP/3 module
|
||||
export type { IHttp3Config } from './http3/index.js';
|
||||
|
||||
export const runCli = async () => {
|
||||
let options: import('./classes.dcrouter.js').IDcRouterOptions = {};
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ ts/
|
||||
│ └── documents/ # Cached document models
|
||||
├── config/ # Configuration utilities
|
||||
├── errors/ # Error classes and retry logic
|
||||
├── http3/ # HTTP/3 (QUIC) route augmentation
|
||||
│ ├── index.ts # Barrel export
|
||||
│ └── http3-route-augmentation.ts # Pure utility: augmentRoutesWithHttp3(), IHttp3Config
|
||||
├── monitoring/ # MetricsManager (SmartMetrics integration)
|
||||
├── opsserver/ # OpsServer dashboard + API handlers
|
||||
│ ├── classes.opsserver.ts # HTTP server + TypedRouter setup
|
||||
@@ -99,6 +102,9 @@ export { RadiusServer, IRadiusServerConfig } from './radius/index.js';
|
||||
|
||||
// Remote Ingress
|
||||
export { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||
|
||||
// HTTP/3
|
||||
export type { IHttp3Config } from './http3/index.js';
|
||||
```
|
||||
|
||||
## Key Classes
|
||||
@@ -115,6 +121,7 @@ The central orchestrator. Accepts `IDcRouterOptions` and manages the lifecycle o
|
||||
| `radiusConfig` | RadiusServer (auth + accounting) | `@push.rocks/smartradius` |
|
||||
| `remoteIngressConfig` | RemoteIngressManager + TunnelManager | `@serve.zone/remoteingress` |
|
||||
| `tls` + `dnsChallenge` | SmartAcme (ACME cert provisioning) | `@push.rocks/smartacme` |
|
||||
| `http3` | HTTP/3 route augmentation (enabled by default) | built-in |
|
||||
| `cacheConfig` | CacheDb (embedded MongoDB) | `@push.rocks/smartdata` |
|
||||
| *(always)* | OpsServer (dashboard + API) | `@api.global/typedserver` |
|
||||
| *(always)* | MetricsManager | `@push.rocks/smartmetrics` |
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '11.5.1',
|
||||
version: '11.7.1',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user