Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6e1951aa2 | |||
| 76fd563e21 | |||
| ee831ea057 | |||
| a65c2ec096 | |||
| 65822278d5 | |||
| aa3955fc67 | |||
| d4605062bb | |||
| cd3f08d55f | |||
| 6d447f0086 | |||
| c7de3873d8 | |||
| 6d4e30e8a9 | |||
| 0e308b692b | |||
| 9f74b6e063 | |||
| 1d0f47f256 | |||
| 4e9301ae2a | |||
| 7e2142ce53 | |||
| 67190605a6 | |||
| 9479a07ddf | |||
| fbed56092f | |||
| 547b82b35b | |||
| 3dc63fa02e | |||
| e0154f5b70 | |||
| b268409897 | |||
| f3a9fd12c5 | |||
| ef741d84fb | |||
| b0ea97b922 | |||
| d1560811f5 | |||
| 5e872c4e6a | |||
| 3620e4549a | |||
| b32865e790 | |||
| ebe71a2a94 | |||
| 877a2ad0ee | |||
| 7be1aaedb3 | |||
| 05eb8e9723 | |||
| d95d89ea6f | |||
| 5d1b988579 |
@@ -6,7 +6,7 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:szci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:szci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-dbase:npmci
|
image: code.foss.global/host.today/ht-docker-node:dbase_dind
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -82,15 +82,13 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @git.zone/tsdocker
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
run: |
|
run: |
|
||||||
npmci docker login
|
tsdocker login
|
||||||
npmci docker build
|
tsdocker build
|
||||||
npmci docker test
|
tsdocker push
|
||||||
# npmci docker push gitea.lossless.digital
|
|
||||||
npmci docker push dockerregistry.lossless.digital
|
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
needs: test
|
needs: test
|
||||||
|
|||||||
40
Dockerfile
40
Dockerfile
@@ -1,44 +1,24 @@
|
|||||||
# gitzone dockerfile_service
|
# gitzone dockerfile_service
|
||||||
## STAGE 1 // BUILD
|
## STAGE 1 // BUILD
|
||||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node1
|
FROM code.foss.global/host.today/ht-docker-node:lts AS build
|
||||||
COPY ./ /app
|
COPY ./ /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG NPMCI_TOKEN_NPM2
|
|
||||||
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
|
|
||||||
RUN npmci npm prepare
|
|
||||||
RUN pnpm config set store-dir .pnpm-store
|
RUN pnpm config set store-dir .pnpm-store
|
||||||
RUN rm -rf node_modules && pnpm install
|
RUN rm -rf node_modules && pnpm install
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
RUN rm -rf .pnpm-store node_modules && pnpm install --prod
|
||||||
|
|
||||||
|
## STAGE 2 // PRODUCTION
|
||||||
|
FROM code.foss.global/host.today/ht-docker-node:alpine-node AS production
|
||||||
|
|
||||||
|
# gcompat + libstdc++ for glibc-linked Rust binaries (smartproxy, smartmta, remoteingress)
|
||||||
|
RUN apk add --no-cache gcompat libstdc++
|
||||||
|
|
||||||
# gitzone dockerfile_service
|
|
||||||
## STAGE 2 // install production
|
|
||||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node2
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=node1 /app /app
|
COPY --from=build /app /app
|
||||||
RUN rm -rf .pnpm-store
|
|
||||||
ARG NPMCI_TOKEN_NPM2
|
|
||||||
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
|
|
||||||
RUN npmci npm prepare
|
|
||||||
RUN pnpm config set store-dir .pnpm-store
|
|
||||||
RUN rm -rf node_modules/ && pnpm install --prod
|
|
||||||
|
|
||||||
|
ENV DCROUTER_MODE=OCI_CONTAINER
|
||||||
|
|
||||||
## STAGE 3 // rebuild dependencies for alpine
|
|
||||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:alpinenpmci as node3
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=node2 /app /app
|
|
||||||
ARG NPMCI_TOKEN_NPM2
|
|
||||||
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
|
|
||||||
RUN npmci npm prepare
|
|
||||||
RUN pnpm config set store-dir .pnpm-store
|
|
||||||
RUN pnpm rebuild -r
|
|
||||||
|
|
||||||
## STAGE 4 // the final production image with all dependencies in place
|
|
||||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:alpine as node4
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=node3 /app /app
|
|
||||||
|
|
||||||
### Healthchecks
|
|
||||||
RUN pnpm install -g @servezone/healthy
|
RUN pnpm install -g @servezone/healthy
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ]
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ]
|
||||||
|
|
||||||
|
|||||||
106
changelog.md
106
changelog.md
@@ -1,5 +1,111 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-19 - 11.8.1 - fix(dcrouter)
|
||||||
|
use constructor routes for remote ingress setup and bump smartproxy dependency
|
||||||
|
|
||||||
|
- Switch remote ingress initialization to use constructorRoutes instead of smartProxyConfig routes so derived edge ports are based on the active route set.
|
||||||
|
- Update @push.rocks/smartproxy from ^25.16.2 to ^25.16.3.
|
||||||
|
|
||||||
|
## 2026-03-19 - 11.8.0 - feat(remoteingress)
|
||||||
|
add UDP listen port derivation and edge configuration support
|
||||||
|
|
||||||
|
- derive UDP ports from remote ingress routes using transport 'udp' or 'all'
|
||||||
|
- expose effective UDP listen ports in allowed edge payloads and remote ingress interfaces
|
||||||
|
- update @push.rocks/smartproxy to ^25.16.2
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-03-19 - 11.5.0 - feat(opsserver)
|
||||||
|
add configurable OpsServer port and update related tests and documentation
|
||||||
|
|
||||||
|
- introduces an optional `opsServerPort` configuration that overrides the default OpsServer port 3000
|
||||||
|
- updates OpsServer startup logic to use the configured port
|
||||||
|
- adjusts integration tests to run against dedicated OpsServer ports to avoid conflicts
|
||||||
|
- documents the new OpsServer port option in the README and TypeScript docs
|
||||||
|
- includes dependency updates and a remote ingress port range type refinement
|
||||||
|
|
||||||
|
## 2026-03-19 - 11.4.0 - feat(docs)
|
||||||
|
document OCI container deployment and enable verbose docker build scripts
|
||||||
|
|
||||||
|
- adds a new README section covering Docker/OCI container deployment, environment variables, and image build/push commands
|
||||||
|
- updates docker build and release npm scripts to pass the --verbose flag for more detailed output
|
||||||
|
|
||||||
|
## 2026-03-18 - 11.3.0 - feat(docker)
|
||||||
|
add OCI container startup configuration and migrate Docker release pipeline to tsdocker
|
||||||
|
|
||||||
|
- adds OCI container mode startup that reads DcRouter options from environment variables and an optional JSON config file
|
||||||
|
- simplifies the Docker image to a two-stage build with production dependencies only and Alpine runtime compatibility packages
|
||||||
|
- updates Gitea workflows and npm scripts to use tsdocker for image build and release
|
||||||
|
|
||||||
|
## 2026-03-18 - 11.2.56 - fix(deps)
|
||||||
|
bump @serve.zone/remoteingress to ^4.9.0
|
||||||
|
|
||||||
|
- Updates @serve.zone/remoteingress from ^4.8.18 to ^4.9.0 in package.json
|
||||||
|
|
||||||
|
## 2026-03-17 - 11.2.55 - fix(deps)
|
||||||
|
bump @serve.zone/catalog to ^2.7.0 and @serve.zone/remoteingress to ^4.8.18
|
||||||
|
|
||||||
|
- updates @serve.zone/catalog from ^2.6.2 to ^2.7.0
|
||||||
|
- updates @serve.zone/remoteingress from ^4.8.16 to ^4.8.18
|
||||||
|
|
||||||
|
## 2026-03-17 - 11.2.54 - fix(deps)
|
||||||
|
bump @serve.zone/remoteingress to ^4.8.16
|
||||||
|
|
||||||
|
- Updates @serve.zone/remoteingress from ^4.8.14 to ^4.8.16 in package.json.
|
||||||
|
|
||||||
|
## 2026-03-17 - 11.2.53 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy and @serve.zone/remoteingress patch versions
|
||||||
|
|
||||||
|
- update @push.rocks/smartproxy from ^25.11.23 to ^25.11.24
|
||||||
|
- update @serve.zone/remoteingress from ^4.8.13 to ^4.8.14
|
||||||
|
|
||||||
|
## 2026-03-17 - 11.2.52 - fix(deps)
|
||||||
|
bump @serve.zone/remoteingress to ^4.8.13
|
||||||
|
|
||||||
|
- Updates the @serve.zone/remoteingress dependency from ^4.8.12 to ^4.8.13.
|
||||||
|
|
||||||
|
## 2026-03-17 - 11.2.51 - fix(deps)
|
||||||
|
bump @serve.zone/remoteingress to ^4.8.12
|
||||||
|
|
||||||
|
- Updates @serve.zone/remoteingress from ^4.8.11 to ^4.8.12 in package.json
|
||||||
|
|
||||||
|
## 2026-03-17 - 11.2.50 - fix(deps)
|
||||||
|
bump @serve.zone/remoteingress to ^4.8.11
|
||||||
|
|
||||||
|
- updates @serve.zone/remoteingress from ^4.8.10 to ^4.8.11
|
||||||
|
|
||||||
|
## 2026-03-17 - 11.2.49 - fix(deps)
|
||||||
|
bump @serve.zone/remoteingress to ^4.8.10
|
||||||
|
|
||||||
|
- Updates @serve.zone/remoteingress from ^4.8.9 to ^4.8.10 in package.json
|
||||||
|
|
||||||
|
## 2026-03-17 - 11.2.48 - fix(deps)
|
||||||
|
bump @serve.zone/remoteingress to ^4.8.9
|
||||||
|
|
||||||
|
- Updates @serve.zone/remoteingress from ^4.8.7 to ^4.8.9 in package.json
|
||||||
|
|
||||||
## 2026-03-17 - 11.2.47 - fix(deps)
|
## 2026-03-17 - 11.2.47 - fix(deps)
|
||||||
bump @push.rocks/smartproxy to ^25.11.23
|
bump @push.rocks/smartproxy to ^25.11.23
|
||||||
|
|
||||||
|
|||||||
@@ -72,9 +72,14 @@
|
|||||||
"dockerRegistryRepoMap": {
|
"dockerRegistryRepoMap": {
|
||||||
"registry.gitlab.com": "code.foss.global/serve.zone/dcrouter"
|
"registry.gitlab.com": "code.foss.global/serve.zone/dcrouter"
|
||||||
},
|
},
|
||||||
"dockerBuildargEnvMap": {
|
|
||||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
|
||||||
},
|
|
||||||
"npmRegistryUrl": "verdaccio.lossless.digital"
|
"npmRegistryUrl": "verdaccio.lossless.digital"
|
||||||
|
},
|
||||||
|
"@git.zone/tsdocker": {
|
||||||
|
"registries": ["code.foss.global"],
|
||||||
|
"registryRepoMap": {
|
||||||
|
"code.foss.global": "serve.zone/dcrouter",
|
||||||
|
"dockerregistry.lossless.digital": "serve.zone/dcrouter"
|
||||||
|
},
|
||||||
|
"platforms": ["linux/amd64", "linux/arm64"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "11.2.47",
|
"version": "11.8.1",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
"start": "(node --max_old_space_size=250 ./cli.js)",
|
"start": "(node --max_old_space_size=250 ./cli.js)",
|
||||||
"startTs": "(node cli.ts.js)",
|
"startTs": "(node cli.ts.js)",
|
||||||
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
||||||
|
"build:docker": "tsdocker build --verbose",
|
||||||
|
"release:docker": "tsdocker push --verbose",
|
||||||
"bundle": "(tsbundle)",
|
"bundle": "(tsbundle)",
|
||||||
"watch": "tswatch"
|
"watch": "tswatch"
|
||||||
},
|
},
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
"@git.zone/tsbuild": "^4.3.0",
|
"@git.zone/tsbuild": "^4.3.0",
|
||||||
"@git.zone/tsbundle": "^2.9.1",
|
"@git.zone/tsbundle": "^2.9.1",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.3.2",
|
"@git.zone/tstest": "^3.5.0",
|
||||||
"@git.zone/tswatch": "^3.3.0",
|
"@git.zone/tswatch": "^3.3.0",
|
||||||
"@types/node": "^25.5.0"
|
"@types/node": "^25.5.0"
|
||||||
},
|
},
|
||||||
@@ -38,7 +40,7 @@
|
|||||||
"@push.rocks/lik": "^6.3.1",
|
"@push.rocks/lik": "^6.3.1",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^9.1.3",
|
"@push.rocks/smartacme": "^9.3.0",
|
||||||
"@push.rocks/smartdata": "^7.1.0",
|
"@push.rocks/smartdata": "^7.1.0",
|
||||||
"@push.rocks/smartdns": "^7.9.0",
|
"@push.rocks/smartdns": "^7.9.0",
|
||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
@@ -51,15 +53,15 @@
|
|||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^25.11.23",
|
"@push.rocks/smartproxy": "^25.16.3",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstate": "^2.2.0",
|
"@push.rocks/smartstate": "^2.2.0",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@serve.zone/catalog": "^2.6.2",
|
"@serve.zone/catalog": "^2.9.0",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
"@serve.zone/remoteingress": "^4.8.7",
|
"@serve.zone/remoteingress": "^4.13.0",
|
||||||
"@tsclass/tsclass": "^9.4.0",
|
"@tsclass/tsclass": "^9.4.0",
|
||||||
"lru-cache": "^11.2.7",
|
"lru-cache": "^11.2.7",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
|
|||||||
1811
pnpm-lock.yaml
generated
1811
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
177
readme.md
177
readme.md
@@ -18,6 +18,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- [Architecture](#architecture)
|
- [Architecture](#architecture)
|
||||||
- [Configuration Reference](#configuration-reference)
|
- [Configuration Reference](#configuration-reference)
|
||||||
- [HTTP/HTTPS & TCP/SNI Routing](#httphttps--tcpsni-routing)
|
- [HTTP/HTTPS & TCP/SNI Routing](#httphttps--tcpsni-routing)
|
||||||
|
- [HTTP/3 (QUIC) Support](#http3-quic-support)
|
||||||
- [Email System](#email-system)
|
- [Email System](#email-system)
|
||||||
- [DNS Server](#dns-server)
|
- [DNS Server](#dns-server)
|
||||||
- [RADIUS Server](#radius-server)
|
- [RADIUS Server](#radius-server)
|
||||||
@@ -30,12 +31,14 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- [API Reference](#api-reference)
|
- [API Reference](#api-reference)
|
||||||
- [Sub-Modules](#sub-modules)
|
- [Sub-Modules](#sub-modules)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
|
- [Docker / OCI Container Deployment](#docker--oci-container-deployment)
|
||||||
- [License and Legal Information](#license-and-legal-information)
|
- [License and Legal Information](#license-and-legal-information)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🌐 Universal Traffic Router
|
### 🌐 Universal Traffic Router
|
||||||
- **HTTP/HTTPS routing** with domain matching, path-based forwarding, and automatic TLS
|
- **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
|
- **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
|
- **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)
|
- **Multi-protocol support** on the same infrastructure via [SmartProxy](https://code.foss.global/push.rocks/smartproxy)
|
||||||
@@ -343,7 +346,7 @@ graph TB
|
|||||||
|
|
||||||
DcRouter acts purely as an **orchestrator** — it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
|
DcRouter acts purely as an **orchestrator** — it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
|
||||||
|
|
||||||
1. **On `start()`**: DcRouter initializes OpsServer (port 3000), then spins up SmartProxy, smartmta, SmartDNS, SmartRadius, and RemoteIngress based on which configs are provided.
|
1. **On `start()`**: DcRouter initializes OpsServer (default port 3000, configurable via `opsServerPort`), then spins up SmartProxy, smartmta, SmartDNS, SmartRadius, and RemoteIngress based on which configs are provided.
|
||||||
2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery. RemoteIngress runs a Rust data plane for edge tunnel networking. SmartAcme v9 handles all certificate operations with built-in concurrency control and rate limiting.
|
2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery. RemoteIngress runs a Rust data plane for edge tunnel networking. SmartAcme v9 handles all certificate operations with built-in concurrency control and rate limiting.
|
||||||
3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
|
3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
|
||||||
|
|
||||||
@@ -424,6 +427,31 @@ 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;
|
||||||
|
|
||||||
// ── TLS & Certificates ────────────────────────────────────────
|
// ── TLS & Certificates ────────────────────────────────────────
|
||||||
tls?: {
|
tls?: {
|
||||||
contactEmail: string;
|
contactEmail: string;
|
||||||
@@ -511,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
|
## 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.
|
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.
|
||||||
@@ -1015,7 +1139,7 @@ action: {
|
|||||||
|
|
||||||
## OpsServer Dashboard
|
## OpsServer Dashboard
|
||||||
|
|
||||||
The OpsServer provides a web-based management interface served on port 3000. It's built with modern web components using [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog).
|
The OpsServer provides a web-based management interface served on port 3000 by default (configurable via `opsServerPort`). It's built with modern web components using [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog).
|
||||||
|
|
||||||
### Dashboard Views
|
### Dashboard Views
|
||||||
|
|
||||||
@@ -1216,7 +1340,7 @@ const router = new DcRouter(options: IDcRouterOptions);
|
|||||||
|
|
||||||
### Re-exported Types
|
### Re-exported Types
|
||||||
|
|
||||||
DcRouter re-exports key types from smartmta for convenience:
|
DcRouter re-exports key types for convenience:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {
|
import {
|
||||||
@@ -1226,6 +1350,7 @@ import {
|
|||||||
type IUnifiedEmailServerOptions,
|
type IUnifiedEmailServerOptions,
|
||||||
type IEmailRoute,
|
type IEmailRoute,
|
||||||
type IEmailDomainConfig,
|
type IEmailDomainConfig,
|
||||||
|
type IHttp3Config,
|
||||||
} from '@serve.zone/dcrouter';
|
} from '@serve.zone/dcrouter';
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1272,12 +1397,56 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
|||||||
| `test.dns-server-config.ts` | DNS record parsing, grouping, extraction | 5 |
|
| `test.dns-server-config.ts` | DNS record parsing, grouping, extraction | 5 |
|
||||||
| `test.dns-socket-handler.ts` | DNS socket handler and route generation | 6 |
|
| `test.dns-socket-handler.ts` | DNS socket handler and route generation | 6 |
|
||||||
| `test.errors.ts` | Error classes, handler, retry utilities | 5 |
|
| `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.ipreputationchecker.ts` | IP reputation, DNSBL, caching, risk classification | 10 |
|
||||||
| `test.jwt-auth.ts` | JWT login, verification, logout, invalid credentials | 8 |
|
| `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.protected-endpoint.ts` | Admin auth, identity verification, public endpoints | 8 |
|
||||||
| `test.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 8 |
|
| `test.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 8 |
|
||||||
|
|
||||||
|
## Docker / OCI Container Deployment
|
||||||
|
|
||||||
|
DcRouter ships with a `Dockerfile` and supports environment-variable-driven configuration for OCI container deployments. When `DCROUTER_MODE=OCI_CONTAINER` is set, DcRouter automatically reads configuration from environment variables (and optionally from a JSON config file).
|
||||||
|
|
||||||
|
### Running with Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-e DCROUTER_MODE=OCI_CONTAINER \
|
||||||
|
-e DCROUTER_TLS_EMAIL=admin@example.com \
|
||||||
|
-e DCROUTER_PUBLIC_IP=203.0.113.1 \
|
||||||
|
-e DCROUTER_DNS_NS_DOMAINS=ns1.example.com,ns2.example.com \
|
||||||
|
-e DCROUTER_DNS_SCOPES=example.com \
|
||||||
|
-p 80:80 -p 443:443 -p 25:25 -p 53:53/udp -p 3000:3000 \
|
||||||
|
code.foss.global/serve.zone/dcrouter:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `DCROUTER_MODE` | Set to `OCI_CONTAINER` to enable container mode | `OCI_CONTAINER` |
|
||||||
|
| `DCROUTER_CONFIG_PATH` | Path to a JSON config file (loaded as base, env vars override) | `/config/dcrouter.json` |
|
||||||
|
| `DCROUTER_BASE_DIR` | Override base data directory | `/data/dcrouter` |
|
||||||
|
| `DCROUTER_TLS_EMAIL` | ACME contact email | `admin@example.com` |
|
||||||
|
| `DCROUTER_TLS_DOMAIN` | Primary TLS domain | `example.com` |
|
||||||
|
| `DCROUTER_PUBLIC_IP` | Public IP for DNS records | `203.0.113.1` |
|
||||||
|
| `DCROUTER_PROXY_IPS` | Comma-separated ingress proxy IPs | `198.51.100.1,198.51.100.2` |
|
||||||
|
| `DCROUTER_DNS_NS_DOMAINS` | Comma-separated nameserver domains | `ns1.example.com,ns2.example.com` |
|
||||||
|
| `DCROUTER_DNS_SCOPES` | Comma-separated authoritative domains | `example.com,other.com` |
|
||||||
|
| `DCROUTER_EMAIL_HOSTNAME` | SMTP server hostname | `mail.example.com` |
|
||||||
|
| `DCROUTER_EMAIL_PORTS` | Comma-separated email ports | `25,587,465` |
|
||||||
|
| `DCROUTER_CACHE_ENABLED` | Enable/disable cache database | `true` |
|
||||||
|
|
||||||
|
### Building the Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run build:docker # Build the container image
|
||||||
|
pnpm run release:docker # Push to registry
|
||||||
|
```
|
||||||
|
|
||||||
|
The Docker build supports multi-platform (`linux/amd64`, `linux/arm64`) via [tsdocker](https://code.foss.global/git.zone/tsdocker).
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ tap.test('DcRouter class - Email config with domains and routes', async () => {
|
|||||||
tls: {
|
tls: {
|
||||||
contactEmail: 'test@example.com'
|
contactEmail: 'test@example.com'
|
||||||
},
|
},
|
||||||
|
opsServerPort: 3104,
|
||||||
cacheConfig: {
|
cacheConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ tap.test('should NOT instantiate DNS server when dnsNsDomains is not set', async
|
|||||||
smartProxyConfig: {
|
smartProxyConfig: {
|
||||||
routes: []
|
routes: []
|
||||||
},
|
},
|
||||||
|
opsServerPort: 3100,
|
||||||
cacheConfig: { enabled: false }
|
cacheConfig: { enabled: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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();
|
||||||
@@ -9,6 +9,7 @@ let identity: interfaces.data.IIdentity;
|
|||||||
tap.test('should start DCRouter with OpsServer', async () => {
|
tap.test('should start DCRouter with OpsServer', async () => {
|
||||||
testDcRouter = new DcRouter({
|
testDcRouter = new DcRouter({
|
||||||
// Minimal config for testing
|
// Minimal config for testing
|
||||||
|
opsServerPort: 3102,
|
||||||
cacheConfig: { enabled: false },
|
cacheConfig: { enabled: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ tap.test('should start DCRouter with OpsServer', async () => {
|
|||||||
|
|
||||||
tap.test('should login with admin credentials and receive JWT', async () => {
|
tap.test('should login with admin credentials and receive JWT', async () => {
|
||||||
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3102/typedrequest',
|
||||||
'adminLoginWithUsernameAndPassword'
|
'adminLoginWithUsernameAndPassword'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ tap.test('should login with admin credentials and receive JWT', async () => {
|
|||||||
|
|
||||||
tap.test('should verify valid JWT identity', async () => {
|
tap.test('should verify valid JWT identity', async () => {
|
||||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3102/typedrequest',
|
||||||
'verifyIdentity'
|
'verifyIdentity'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ tap.test('should verify valid JWT identity', async () => {
|
|||||||
|
|
||||||
tap.test('should reject invalid JWT', async () => {
|
tap.test('should reject invalid JWT', async () => {
|
||||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3102/typedrequest',
|
||||||
'verifyIdentity'
|
'verifyIdentity'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ tap.test('should reject invalid JWT', async () => {
|
|||||||
|
|
||||||
tap.test('should verify JWT matches identity data', async () => {
|
tap.test('should verify JWT matches identity data', async () => {
|
||||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3102/typedrequest',
|
||||||
'verifyIdentity'
|
'verifyIdentity'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ tap.test('should verify JWT matches identity data', async () => {
|
|||||||
|
|
||||||
tap.test('should handle logout', async () => {
|
tap.test('should handle logout', async () => {
|
||||||
const logoutRequest = new TypedRequest<interfaces.requests.IReq_AdminLogout>(
|
const logoutRequest = new TypedRequest<interfaces.requests.IReq_AdminLogout>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3102/typedrequest',
|
||||||
'adminLogout'
|
'adminLogout'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ tap.test('should handle logout', async () => {
|
|||||||
|
|
||||||
tap.test('should reject wrong credentials', async () => {
|
tap.test('should reject wrong credentials', async () => {
|
||||||
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3102/typedrequest',
|
||||||
'adminLoginWithUsernameAndPassword'
|
'adminLoginWithUsernameAndPassword'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ let adminIdentity: interfaces.data.IIdentity;
|
|||||||
tap.test('should start DCRouter with OpsServer', async () => {
|
tap.test('should start DCRouter with OpsServer', async () => {
|
||||||
testDcRouter = new DcRouter({
|
testDcRouter = new DcRouter({
|
||||||
// Minimal config for testing
|
// Minimal config for testing
|
||||||
|
opsServerPort: 3101,
|
||||||
cacheConfig: { enabled: false },
|
cacheConfig: { enabled: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ tap.test('should start DCRouter with OpsServer', async () => {
|
|||||||
|
|
||||||
tap.test('should login as admin', async () => {
|
tap.test('should login as admin', async () => {
|
||||||
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3101/typedrequest',
|
||||||
'adminLoginWithUsernameAndPassword'
|
'adminLoginWithUsernameAndPassword'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ tap.test('should login as admin', async () => {
|
|||||||
|
|
||||||
tap.test('should respond to health status request', async () => {
|
tap.test('should respond to health status request', async () => {
|
||||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3101/typedrequest',
|
||||||
'getHealthStatus'
|
'getHealthStatus'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ tap.test('should respond to health status request', async () => {
|
|||||||
|
|
||||||
tap.test('should respond to server statistics request', async () => {
|
tap.test('should respond to server statistics request', async () => {
|
||||||
const statsRequest = new TypedRequest<interfaces.requests.IReq_GetServerStatistics>(
|
const statsRequest = new TypedRequest<interfaces.requests.IReq_GetServerStatistics>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3101/typedrequest',
|
||||||
'getServerStatistics'
|
'getServerStatistics'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ tap.test('should respond to server statistics request', async () => {
|
|||||||
|
|
||||||
tap.test('should respond to configuration request', async () => {
|
tap.test('should respond to configuration request', async () => {
|
||||||
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3101/typedrequest',
|
||||||
'getConfiguration'
|
'getConfiguration'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ tap.test('should respond to configuration request', async () => {
|
|||||||
|
|
||||||
tap.test('should handle log retrieval request', async () => {
|
tap.test('should handle log retrieval request', async () => {
|
||||||
const logsRequest = new TypedRequest<interfaces.requests.IReq_GetRecentLogs>(
|
const logsRequest = new TypedRequest<interfaces.requests.IReq_GetRecentLogs>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3101/typedrequest',
|
||||||
'getRecentLogs'
|
'getRecentLogs'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ tap.test('should handle log retrieval request', async () => {
|
|||||||
|
|
||||||
tap.test('should reject unauthenticated requests', async () => {
|
tap.test('should reject unauthenticated requests', async () => {
|
||||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3101/typedrequest',
|
||||||
'getHealthStatus'
|
'getHealthStatus'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ let adminIdentity: interfaces.data.IIdentity;
|
|||||||
tap.test('should start DCRouter with OpsServer', async () => {
|
tap.test('should start DCRouter with OpsServer', async () => {
|
||||||
testDcRouter = new DcRouter({
|
testDcRouter = new DcRouter({
|
||||||
// Minimal config for testing
|
// Minimal config for testing
|
||||||
|
opsServerPort: 3103,
|
||||||
cacheConfig: { enabled: false },
|
cacheConfig: { enabled: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ tap.test('should start DCRouter with OpsServer', async () => {
|
|||||||
|
|
||||||
tap.test('should login as admin', async () => {
|
tap.test('should login as admin', async () => {
|
||||||
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3103/typedrequest',
|
||||||
'adminLoginWithUsernameAndPassword'
|
'adminLoginWithUsernameAndPassword'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ tap.test('should login as admin', async () => {
|
|||||||
|
|
||||||
tap.test('should allow admin to verify identity', async () => {
|
tap.test('should allow admin to verify identity', async () => {
|
||||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3103/typedrequest',
|
||||||
'verifyIdentity'
|
'verifyIdentity'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ tap.test('should allow admin to verify identity', async () => {
|
|||||||
|
|
||||||
tap.test('should reject verify identity without identity', async () => {
|
tap.test('should reject verify identity without identity', async () => {
|
||||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3103/typedrequest',
|
||||||
'verifyIdentity'
|
'verifyIdentity'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ tap.test('should reject verify identity without identity', async () => {
|
|||||||
|
|
||||||
tap.test('should reject verify identity with invalid JWT', async () => {
|
tap.test('should reject verify identity with invalid JWT', async () => {
|
||||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3103/typedrequest',
|
||||||
'verifyIdentity'
|
'verifyIdentity'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ tap.test('should reject verify identity with invalid JWT', async () => {
|
|||||||
|
|
||||||
tap.test('should reject protected endpoints without auth', async () => {
|
tap.test('should reject protected endpoints without auth', async () => {
|
||||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3103/typedrequest',
|
||||||
'getHealthStatus'
|
'getHealthStatus'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ tap.test('should reject protected endpoints without auth', async () => {
|
|||||||
|
|
||||||
tap.test('should allow authenticated access to protected endpoints', async () => {
|
tap.test('should allow authenticated access to protected endpoints', async () => {
|
||||||
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3103/typedrequest',
|
||||||
'getConfiguration'
|
'getConfiguration'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.2.47',
|
version: '11.8.1',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
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 { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||||
import { RouteConfigManager, ApiTokenManager } from './config/index.js';
|
import { RouteConfigManager, ApiTokenManager } from './config/index.js';
|
||||||
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
|
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
|
||||||
|
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
||||||
|
|
||||||
export interface IDcRouterOptions {
|
export interface IDcRouterOptions {
|
||||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||||
@@ -163,6 +164,17 @@ export interface IDcRouterOptions {
|
|||||||
* Remote Ingress configuration for edge tunnel nodes
|
* Remote Ingress configuration for edge tunnel nodes
|
||||||
* Enables edge nodes to accept incoming connections and tunnel them to this DcRouter
|
* 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;
|
||||||
|
|
||||||
remoteIngressConfig?: {
|
remoteIngressConfig?: {
|
||||||
/** Enable remote ingress hub (default: false) */
|
/** Enable remote ingress hub (default: false) */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -294,6 +306,7 @@ export class DcRouter {
|
|||||||
this.storageManager,
|
this.storageManager,
|
||||||
() => this.getConstructorRoutes(),
|
() => this.getConstructorRoutes(),
|
||||||
() => this.smartProxy,
|
() => this.smartProxy,
|
||||||
|
() => this.options.http3,
|
||||||
);
|
);
|
||||||
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
||||||
await this.apiTokenManager.initialize();
|
await this.apiTokenManager.initialize();
|
||||||
@@ -466,6 +479,13 @@ export class DcRouter {
|
|||||||
challengeHandlers.push(dns01Handler);
|
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
|
// Cache constructor routes for RouteConfigManager
|
||||||
this.constructorRoutes = [...routes];
|
this.constructorRoutes = [...routes];
|
||||||
|
|
||||||
@@ -1733,7 +1753,7 @@ export class DcRouter {
|
|||||||
await this.remoteIngressManager.initialize();
|
await this.remoteIngressManager.initialize();
|
||||||
|
|
||||||
// Pass current routes so the manager can derive edge ports from remoteIngress-tagged routes
|
// Pass current routes so the manager can derive edge ports from remoteIngress-tagged routes
|
||||||
const currentRoutes = this.options.smartProxyConfig?.routes || [];
|
const currentRoutes = this.constructorRoutes;
|
||||||
this.remoteIngressManager.setRoutes(currentRoutes as any[]);
|
this.remoteIngressManager.setRoutes(currentRoutes as any[]);
|
||||||
|
|
||||||
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
|
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
IMergedRoute,
|
IMergedRoute,
|
||||||
IRouteWarning,
|
IRouteWarning,
|
||||||
} from '../../ts_interfaces/data/route-management.js';
|
} from '../../ts_interfaces/data/route-management.js';
|
||||||
|
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
|
||||||
|
|
||||||
const ROUTES_PREFIX = '/config-api/routes/';
|
const ROUTES_PREFIX = '/config-api/routes/';
|
||||||
const OVERRIDES_PREFIX = '/config-api/overrides/';
|
const OVERRIDES_PREFIX = '/config-api/overrides/';
|
||||||
@@ -20,6 +21,7 @@ export class RouteConfigManager {
|
|||||||
private storageManager: StorageManager,
|
private storageManager: StorageManager,
|
||||||
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
||||||
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
||||||
|
private getHttp3Config?: () => IHttp3Config | undefined,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,10 +260,15 @@ export class RouteConfigManager {
|
|||||||
enabledRoutes.push(route);
|
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()) {
|
for (const stored of this.storedRoutes.values()) {
|
||||||
if (stored.enabled) {
|
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';
|
||||||
26
ts/index.ts
26
ts/index.ts
@@ -5,6 +5,7 @@ export { UnifiedEmailServer } from '@push.rocks/smartmta';
|
|||||||
export type { IUnifiedEmailServerOptions, IEmailRoute, IEmailDomainConfig } from '@push.rocks/smartmta';
|
export type { IUnifiedEmailServerOptions, IEmailRoute, IEmailDomainConfig } from '@push.rocks/smartmta';
|
||||||
|
|
||||||
// DcRouter
|
// DcRouter
|
||||||
|
import { DcRouter } from './classes.dcrouter.js';
|
||||||
export * from './classes.dcrouter.js';
|
export * from './classes.dcrouter.js';
|
||||||
|
|
||||||
// RADIUS module
|
// RADIUS module
|
||||||
@@ -13,4 +14,27 @@ export * from './radius/index.js';
|
|||||||
// Remote Ingress module
|
// Remote Ingress module
|
||||||
export * from './remoteingress/index.js';
|
export * from './remoteingress/index.js';
|
||||||
|
|
||||||
export const runCli = async () => {};
|
// HTTP/3 module
|
||||||
|
export type { IHttp3Config } from './http3/index.js';
|
||||||
|
|
||||||
|
export const runCli = async () => {
|
||||||
|
let options: import('./classes.dcrouter.js').IDcRouterOptions = {};
|
||||||
|
|
||||||
|
if (process.env.DCROUTER_MODE === 'OCI_CONTAINER') {
|
||||||
|
const { getOciContainerConfig } = await import('../ts_oci_container/index.js');
|
||||||
|
options = getOciContainerConfig();
|
||||||
|
console.log('[DCRouter] Starting in OCI Container mode...');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dcRouter = new DcRouter(options);
|
||||||
|
await dcRouter.start();
|
||||||
|
console.log('[DCRouter] Running. Send SIGTERM or SIGINT to stop.');
|
||||||
|
|
||||||
|
const shutdown = async () => {
|
||||||
|
console.log('[DCRouter] Shutting down...');
|
||||||
|
await dcRouter.stop();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
};
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class OpsServer {
|
|||||||
// Set up handlers
|
// Set up handlers
|
||||||
await this.setupHandlers();
|
await this.setupHandlers();
|
||||||
|
|
||||||
await this.server.start(3000);
|
await this.server.start(this.dcRouterRef.options.opsServerPort ?? 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
14
ts/readme.md
14
ts/readme.md
@@ -37,7 +37,7 @@ const router = new DcRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await router.start();
|
await router.start();
|
||||||
// OpsServer dashboard at http://localhost:3000
|
// OpsServer dashboard at http://localhost:3000 (configurable via opsServerPort)
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
await router.stop();
|
await router.stop();
|
||||||
@@ -60,6 +60,9 @@ ts/
|
|||||||
│ └── documents/ # Cached document models
|
│ └── documents/ # Cached document models
|
||||||
├── config/ # Configuration utilities
|
├── config/ # Configuration utilities
|
||||||
├── errors/ # Error classes and retry logic
|
├── 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)
|
├── monitoring/ # MetricsManager (SmartMetrics integration)
|
||||||
├── opsserver/ # OpsServer dashboard + API handlers
|
├── opsserver/ # OpsServer dashboard + API handlers
|
||||||
│ ├── classes.opsserver.ts # HTTP server + TypedRouter setup
|
│ ├── classes.opsserver.ts # HTTP server + TypedRouter setup
|
||||||
@@ -71,7 +74,10 @@ ts/
|
|||||||
│ ├── email.handler.ts # Email operations
|
│ ├── email.handler.ts # Email operations
|
||||||
│ ├── certificate.handler.ts # Certificate management
|
│ ├── certificate.handler.ts # Certificate management
|
||||||
│ ├── radius.handler.ts # RADIUS management
|
│ ├── radius.handler.ts # RADIUS management
|
||||||
│ └── remoteingress.handler.ts # Remote ingress edge + token management
|
│ ├── remoteingress.handler.ts # Remote ingress edge + token management
|
||||||
|
│ ├── route-management.handler.ts # Programmatic route CRUD
|
||||||
|
│ ├── api-token.handler.ts # API token management
|
||||||
|
│ └── security.handler.ts # Security metrics + connections
|
||||||
├── radius/ # RADIUS server integration
|
├── radius/ # RADIUS server integration
|
||||||
├── remoteingress/ # Remote ingress hub integration
|
├── remoteingress/ # Remote ingress hub integration
|
||||||
│ ├── classes.remoteingress-manager.ts # Edge CRUD + port derivation
|
│ ├── classes.remoteingress-manager.ts # Edge CRUD + port derivation
|
||||||
@@ -96,6 +102,9 @@ export { RadiusServer, IRadiusServerConfig } from './radius/index.js';
|
|||||||
|
|
||||||
// Remote Ingress
|
// Remote Ingress
|
||||||
export { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
export { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||||
|
|
||||||
|
// HTTP/3
|
||||||
|
export type { IHttp3Config } from './http3/index.js';
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Classes
|
## Key Classes
|
||||||
@@ -112,6 +121,7 @@ The central orchestrator. Accepts `IDcRouterOptions` and manages the lifecycle o
|
|||||||
| `radiusConfig` | RadiusServer (auth + accounting) | `@push.rocks/smartradius` |
|
| `radiusConfig` | RadiusServer (auth + accounting) | `@push.rocks/smartradius` |
|
||||||
| `remoteIngressConfig` | RemoteIngressManager + TunnelManager | `@serve.zone/remoteingress` |
|
| `remoteIngressConfig` | RemoteIngressManager + TunnelManager | `@serve.zone/remoteingress` |
|
||||||
| `tls` + `dnsChallenge` | SmartAcme (ACME cert provisioning) | `@push.rocks/smartacme` |
|
| `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` |
|
| `cacheConfig` | CacheDb (embedded MongoDB) | `@push.rocks/smartdata` |
|
||||||
| *(always)* | OpsServer (dashboard + API) | `@api.global/typedserver` |
|
| *(always)* | OpsServer (dashboard + API) | `@api.global/typedserver` |
|
||||||
| *(always)* | MetricsManager | `@push.rocks/smartmetrics` |
|
| *(always)* | MetricsManager | `@push.rocks/smartmetrics` |
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const STORAGE_PREFIX = '/remote-ingress/';
|
|||||||
/**
|
/**
|
||||||
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
|
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
|
||||||
*/
|
*/
|
||||||
function extractPorts(portRange: number | number[] | Array<{ from: number; to: number }>): number[] {
|
function extractPorts(portRange: number | Array<number | { from: number; to: number }>): number[] {
|
||||||
const ports = new Set<number>();
|
const ports = new Set<number>();
|
||||||
if (typeof portRange === 'number') {
|
if (typeof portRange === 'number') {
|
||||||
ports.add(portRange);
|
ports.add(portRange);
|
||||||
@@ -94,6 +94,38 @@ export class RemoteIngressManager {
|
|||||||
return [...ports].sort((a, b) => a - b);
|
return [...ports].sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive UDP listen ports for an edge from routes with transport 'udp' or 'all'.
|
||||||
|
* These ports need UDP listeners on the edge (e.g. for QUIC/HTTP3).
|
||||||
|
*/
|
||||||
|
public deriveUdpPortsForEdge(edgeId: string, edgeTags?: string[]): number[] {
|
||||||
|
const ports = new Set<number>();
|
||||||
|
|
||||||
|
for (const route of this.routes) {
|
||||||
|
if (!route.remoteIngress?.enabled) continue;
|
||||||
|
|
||||||
|
// Apply edge filter if present
|
||||||
|
const filter = route.remoteIngress.edgeFilter;
|
||||||
|
if (filter && filter.length > 0) {
|
||||||
|
const idMatch = filter.includes(edgeId);
|
||||||
|
const tagMatch = edgeTags?.some((tag) => filter.includes(tag)) ?? false;
|
||||||
|
if (!idMatch && !tagMatch) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include ports from routes that listen on UDP
|
||||||
|
const transport = route.match?.transport;
|
||||||
|
if (transport === 'udp' || transport === 'all') {
|
||||||
|
if (route.match?.ports) {
|
||||||
|
for (const p of extractPorts(route.match.ports)) {
|
||||||
|
ports.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...ports].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the effective listen ports for an edge.
|
* Get the effective listen ports for an edge.
|
||||||
* Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true.
|
* Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true.
|
||||||
@@ -106,6 +138,18 @@ export class RemoteIngressManager {
|
|||||||
return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
|
return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the effective UDP listen ports for an edge.
|
||||||
|
* Manual UDP ports are always included. Auto-derived UDP ports are added when autoDerivePorts is true.
|
||||||
|
*/
|
||||||
|
public getEffectiveListenPortsUdp(edge: IRemoteIngress): number[] {
|
||||||
|
const manualPorts = edge.listenPortsUdp || [];
|
||||||
|
const shouldDerive = edge.autoDerivePorts !== false;
|
||||||
|
if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b);
|
||||||
|
const derivedPorts = this.deriveUdpPortsForEdge(edge.id, edge.tags);
|
||||||
|
return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get manual and derived port breakdown for an edge (used in API responses).
|
* Get manual and derived port breakdown for an edge (used in API responses).
|
||||||
* Derived ports exclude any ports already present in the manual list.
|
* Derived ports exclude any ports already present in the manual list.
|
||||||
@@ -241,15 +285,18 @@ export class RemoteIngressManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of allowed edges (enabled only) for the Rust hub.
|
* Get the list of allowed edges (enabled only) for the Rust hub.
|
||||||
|
* Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
|
||||||
*/
|
*/
|
||||||
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[] }> {
|
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> {
|
||||||
const result: Array<{ id: string; secret: string; listenPorts: number[] }> = [];
|
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> = [];
|
||||||
for (const edge of this.edges.values()) {
|
for (const edge of this.edges.values()) {
|
||||||
if (edge.enabled) {
|
if (edge.enabled) {
|
||||||
|
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
||||||
result.push({
|
result.push({
|
||||||
id: edge.id,
|
id: edge.id,
|
||||||
secret: edge.secret,
|
secret: edge.secret,
|
||||||
listenPorts: this.getEffectiveListenPorts(edge),
|
listenPorts: this.getEffectiveListenPorts(edge),
|
||||||
|
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export interface IRemoteIngress {
|
|||||||
name: string;
|
name: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
listenPorts: number[];
|
listenPorts: number[];
|
||||||
|
/** UDP listen ports (e.g. for QUIC/HTTP3). Derived from routes with transport 'udp' or 'all'. */
|
||||||
|
listenPortsUdp?: number[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
|
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
|
||||||
autoDerivePorts: boolean;
|
autoDerivePorts: boolean;
|
||||||
@@ -20,6 +22,8 @@ export interface IRemoteIngress {
|
|||||||
manualPorts?: number[];
|
manualPorts?: number[];
|
||||||
/** Ports auto-derived from route configs — only present in API responses. */
|
/** Ports auto-derived from route configs — only present in API responses. */
|
||||||
derivedPorts?: number[];
|
derivedPorts?: number[];
|
||||||
|
/** Effective UDP ports (union of manual + derived) — only present in API responses. */
|
||||||
|
effectiveListenPortsUdp?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
100
ts_oci_container/index.ts
Normal file
100
ts_oci_container/index.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import type { IDcRouterOptions } from '../ts/classes.dcrouter.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a comma-separated env var into a string array.
|
||||||
|
* Returns undefined if the env var is not set or empty.
|
||||||
|
*/
|
||||||
|
function parseCommaSeparated(envVar: string | undefined): string[] | undefined {
|
||||||
|
if (!envVar || envVar.trim() === '') return undefined;
|
||||||
|
return envVar.split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a comma-separated env var into a number array.
|
||||||
|
* Returns undefined if the env var is not set or empty.
|
||||||
|
*/
|
||||||
|
function parseCommaSeparatedNumbers(envVar: string | undefined): number[] | undefined {
|
||||||
|
const parts = parseCommaSeparated(envVar);
|
||||||
|
if (!parts) return undefined;
|
||||||
|
return parts.map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds IDcRouterOptions from environment variables for OCI container mode.
|
||||||
|
*
|
||||||
|
* If DCROUTER_CONFIG_PATH is set and the file exists, it is loaded as a JSON base config.
|
||||||
|
* Individual env vars are then applied as overrides on top.
|
||||||
|
*/
|
||||||
|
export function getOciContainerConfig(): IDcRouterOptions {
|
||||||
|
let options: IDcRouterOptions = {};
|
||||||
|
|
||||||
|
// Load JSON config file if specified
|
||||||
|
const configPath = process.env.DCROUTER_CONFIG_PATH;
|
||||||
|
if (configPath && plugins.fs.existsSync(configPath)) {
|
||||||
|
const raw = plugins.fs.readFileSync(configPath, 'utf8');
|
||||||
|
options = JSON.parse(raw);
|
||||||
|
console.log(`[OCI Container] Loaded config from ${configPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply env var overrides
|
||||||
|
if (process.env.DCROUTER_BASE_DIR) {
|
||||||
|
options.baseDir = process.env.DCROUTER_BASE_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS config
|
||||||
|
const tlsEmail = process.env.DCROUTER_TLS_EMAIL;
|
||||||
|
const tlsDomain = process.env.DCROUTER_TLS_DOMAIN;
|
||||||
|
if (tlsEmail || tlsDomain) {
|
||||||
|
options.tls = {
|
||||||
|
...options.tls,
|
||||||
|
contactEmail: tlsEmail || options.tls?.contactEmail || '',
|
||||||
|
...(tlsDomain ? { domain: tlsDomain } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network config
|
||||||
|
if (process.env.DCROUTER_PUBLIC_IP) {
|
||||||
|
options.publicIp = process.env.DCROUTER_PUBLIC_IP;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyIps = parseCommaSeparated(process.env.DCROUTER_PROXY_IPS);
|
||||||
|
if (proxyIps) {
|
||||||
|
options.proxyIps = proxyIps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS config
|
||||||
|
const nsDomains = parseCommaSeparated(process.env.DCROUTER_DNS_NS_DOMAINS);
|
||||||
|
if (nsDomains) {
|
||||||
|
options.dnsNsDomains = nsDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnsScopes = parseCommaSeparated(process.env.DCROUTER_DNS_SCOPES);
|
||||||
|
if (dnsScopes) {
|
||||||
|
options.dnsScopes = dnsScopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email config
|
||||||
|
const emailHostname = process.env.DCROUTER_EMAIL_HOSTNAME;
|
||||||
|
const emailPorts = parseCommaSeparatedNumbers(process.env.DCROUTER_EMAIL_PORTS);
|
||||||
|
if (emailHostname || emailPorts) {
|
||||||
|
options.emailConfig = {
|
||||||
|
...options.emailConfig,
|
||||||
|
...(emailHostname ? { hostname: emailHostname } : {}),
|
||||||
|
...(emailPorts ? { ports: emailPorts } : {}),
|
||||||
|
domains: options.emailConfig?.domains || [],
|
||||||
|
routes: options.emailConfig?.routes || [],
|
||||||
|
} as IDcRouterOptions['emailConfig'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache config
|
||||||
|
const cacheEnabled = process.env.DCROUTER_CACHE_ENABLED;
|
||||||
|
if (cacheEnabled !== undefined) {
|
||||||
|
options.cacheConfig = {
|
||||||
|
...options.cacheConfig,
|
||||||
|
enabled: cacheEnabled === 'true',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
7
ts_oci_container/plugins.ts
Normal file
7
ts_oci_container/plugins.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export {
|
||||||
|
fs,
|
||||||
|
path,
|
||||||
|
};
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.2.47',
|
version: '11.8.1',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user