Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f7a26fb38 | |||
| a089b681c4 | |||
| 3e71301bf5 | |||
| 58cc8c0753 | |||
| e279814803 | |||
| 6bee2eb172 | |||
| db8ea99e88 | |||
| 98ccf82af0 | |||
| 0f99525612 | |||
| 8e707d9c4d | |||
| 418c825b01 | |||
| 75f29af27f | |||
| 4467fe629a | |||
| 1912feffe5 | |||
| 9077b3dad6 | |||
| d09ac51c5b | |||
| 9d7975721d | |||
| 667d62b456 | |||
| 90b1ca8de3 | |||
| 17d824d718 | |||
| 06a8636aee | |||
| 4bf08c1fc3 | |||
| 7e721c54d0 |
@@ -0,0 +1,140 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: code.foss.global/host.today/ht-docker-node:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Configure pnpm registry
|
||||
run: pnpm config set registry https://verdaccio.lossless.digital/
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
|
||||
echo "Building version: $VERSION"
|
||||
|
||||
- name: Verify package.json version matches tag
|
||||
run: |
|
||||
PACKAGE_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")
|
||||
TAG_VERSION="${{ steps.version.outputs.version_number }}"
|
||||
echo "package.json version: $PACKAGE_VERSION"
|
||||
echo "Tag version: $TAG_VERSION"
|
||||
if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "ERROR: Version mismatch!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test package
|
||||
run: pnpm test
|
||||
|
||||
- name: Build binary artifacts
|
||||
run: pnpm run build:binary
|
||||
|
||||
- name: Generate SHA256 checksums
|
||||
run: |
|
||||
cd dist/binaries
|
||||
sha256sum * > SHA256SUMS.txt
|
||||
cat SHA256SUMS.txt
|
||||
cd ../..
|
||||
|
||||
- name: Pack npm artifact
|
||||
run: |
|
||||
mkdir -p dist/package
|
||||
pnpm pack --pack-destination dist/package
|
||||
ls -lh dist/package
|
||||
|
||||
- name: Extract changelog for this version
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
if [ -f changelog.md ]; then
|
||||
awk "/## $VERSION/,/## /" changelog.md | sed '$d' > /tmp/release_notes.md || true
|
||||
fi
|
||||
if [ ! -s /tmp/release_notes.md ]; then
|
||||
cat > /tmp/release_notes.md << EOF
|
||||
## DcRouter $VERSION
|
||||
|
||||
NodeNext package build plus self-extracting Linux binaries.
|
||||
|
||||
### Artifacts
|
||||
|
||||
- npm package tarball
|
||||
- dcrouter-linux-x64
|
||||
- dcrouter-linux-arm64
|
||||
- SHA256SUMS.txt
|
||||
EOF
|
||||
fi
|
||||
|
||||
- name: Delete existing release if it exists
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
EXISTING_RELEASE_ID=$(curl -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/dcrouter/releases/tags/$VERSION" \
|
||||
| jq -r '.id // empty')
|
||||
if [ -n "$EXISTING_RELEASE_ID" ]; then
|
||||
curl -X DELETE -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/dcrouter/releases/$EXISTING_RELEASE_ID"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
- name: Create Gitea Release
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
RELEASE_ID=$(curl -X POST -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/dcrouter/releases" \
|
||||
-d "{
|
||||
\"tag_name\": \"$VERSION\",
|
||||
\"name\": \"DcRouter $VERSION\",
|
||||
\"body\": $(jq -Rs . /tmp/release_notes.md),
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}" | jq -r '.id')
|
||||
for artifact in dist/package/* dist/binaries/*; do
|
||||
[ -f "$artifact" ] || continue
|
||||
filename=$(basename "$artifact")
|
||||
curl -X POST -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$artifact" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/dcrouter/releases/$RELEASE_ID/assets?name=$filename"
|
||||
done
|
||||
|
||||
- name: Release Summary
|
||||
run: |
|
||||
echo "Release ${{ steps.version.outputs.version }} complete"
|
||||
ls -lh dist/package
|
||||
ls -lh dist/binaries
|
||||
+23
-1
@@ -29,6 +29,28 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"@git.zone/tsdeno": {
|
||||
"compileTargets": [
|
||||
{
|
||||
"name": "dcrouter-linux-x64",
|
||||
"entryPoint": "binary/dcrouter.ts",
|
||||
"outDir": "dist/binaries",
|
||||
"target": "x86_64-unknown-linux-gnu",
|
||||
"permissions": ["--allow-all"],
|
||||
"noCheck": true,
|
||||
"selfExtracting": true
|
||||
},
|
||||
{
|
||||
"name": "dcrouter-linux-arm64",
|
||||
"entryPoint": "binary/dcrouter.ts",
|
||||
"outDir": "dist/binaries",
|
||||
"target": "aarch64-unknown-linux-gnu",
|
||||
"permissions": ["--allow-all"],
|
||||
"noCheck": true,
|
||||
"selfExtracting": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"@git.zone/cli": {
|
||||
"schemaVersion": 2,
|
||||
"projectType": "service",
|
||||
@@ -96,4 +118,4 @@
|
||||
]
|
||||
},
|
||||
"@ship.zone/szci": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
process.env.CLI_CALL = 'true';
|
||||
|
||||
const cliTool = await import('../dist_ts/index.js');
|
||||
await cliTool.runCli();
|
||||
@@ -3,6 +3,102 @@
|
||||
## Pending
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 2026-05-30 - 13.40.0
|
||||
|
||||
### Features
|
||||
|
||||
- use active connection snapshots for proxy metrics and RADIUS network secrets (monitoring-opsserver-radius)
|
||||
- Add cached SmartProxy active connection snapshots for connection info and network statistics.
|
||||
- Report ops security active connections from per-connection snapshots with protocol, state, and byte counters.
|
||||
- Configure RADIUS clients through smartradius network secrets, including CIDR ranges, and forward additional RADIUS attributes.
|
||||
- Bump smartproxy to ^27.12.1 and smartradius to ^1.3.0.
|
||||
|
||||
## 2026-05-30 - 13.39.0
|
||||
|
||||
### Features
|
||||
|
||||
- add remote ingress performance overrides and update RADIUS integration (remoteingress,radius)
|
||||
- Persist and propagate optional remote ingress performance overrides through remote ingress create/update APIs, database documents, and hub allowed-edge sync.
|
||||
- Add web UI controls and status display for per-edge maximum connection overrides.
|
||||
- Extend remote ingress performance interfaces with stream payload, timeout, and server-first port settings.
|
||||
- Update RADIUS server integration for smartradius 1.2 request/response handling and client secret resolution, including CIDR matching.
|
||||
|
||||
## 2026-05-30 - 13.38.4
|
||||
|
||||
### Fixes
|
||||
|
||||
- bump @serve.zone/remoteingress to ^4.22.1 (deps)
|
||||
- Updated @serve.zone/remoteingress in package.json and deno.json.
|
||||
|
||||
## 2026-05-30 - 13.38.3
|
||||
|
||||
### Fixes
|
||||
|
||||
- update @serve.zone/remoteingress to ^4.22.0 (deps)
|
||||
- Updated @serve.zone/remoteingress from ^4.21.1 to ^4.22.0 in package.json and deno.json.
|
||||
|
||||
## 2026-05-30 - 13.38.2
|
||||
|
||||
### Fixes
|
||||
|
||||
- bump @serve.zone/remoteingress to ^4.21.1 (deps)
|
||||
- Updated @serve.zone/remoteingress in package.json and deno.json from ^4.21.0 to ^4.21.1.
|
||||
|
||||
## 2026-05-30 - 13.38.1
|
||||
|
||||
### Fixes
|
||||
|
||||
- bump @serve.zone/remoteingress to ^4.21.0 (deps)
|
||||
- Updates @serve.zone/remoteingress from ^4.18.0 to ^4.21.0.
|
||||
- update @serve.zone/remoteingress to ^4.21.0 (deps)
|
||||
- Updates the Deno import mapping for @serve.zone/remoteingress from ^4.18.0 to ^4.21.0.
|
||||
|
||||
## 2026-05-29 - 13.38.0
|
||||
|
||||
### Features
|
||||
|
||||
- support explicit DNS bind interface configuration (dns)
|
||||
- Add a dnsBindInterface option to override the embedded DNS UDP bind address.
|
||||
- Read DCROUTER_DNS_BIND_INTERFACE from OCI container configuration and document it in CLI help.
|
||||
- Add test coverage for explicit DNS bind interface handling in OCI config.
|
||||
|
||||
## 2026-05-29 - 13.37.2
|
||||
|
||||
### Fixes
|
||||
|
||||
- exclude assets from compiled and published artifacts (packaging)
|
||||
- Removed assets from the Deno compile include list.
|
||||
- Removed assets from the npm package files list.
|
||||
|
||||
## 2026-05-29 - 13.37.1
|
||||
|
||||
### Fixes
|
||||
|
||||
- configure pnpm registry for release workflow (release)
|
||||
- Sets the pnpm registry before dependency installation so release builds resolve packages from the configured registry.
|
||||
|
||||
## 2026-05-29 - 13.37.0
|
||||
|
||||
### Features
|
||||
|
||||
- add CLI binary distribution (distribution)
|
||||
- Add dcrouter bin entry, Deno compile targets, binary entrypoint, and tag-driven release workflow for Linux artifacts.
|
||||
- Add --version and --help handling to the CLI for safe package and binary smoke tests.
|
||||
- Keep the Deno binary import map aligned with the current SmartDNS and SmartProxy runtime dependencies.
|
||||
- add one-line installer and Docker distribution docs (distribution)
|
||||
- Add an install.sh flow that installs Linux x64 and arm64 release binaries by default with a NodeNext source-build fallback.
|
||||
- Document installer modes, binary artifact names, and the published multi-arch Docker image.
|
||||
|
||||
## 2026-05-29 - 13.36.3
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"version": "13.40.0",
|
||||
"exports": "./binary/dcrouter.ts",
|
||||
"compile": {
|
||||
"include": [
|
||||
"dist_serve"
|
||||
]
|
||||
},
|
||||
"imports": {
|
||||
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.3.1",
|
||||
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
|
||||
"@api.global/typedserver": "npm:@api.global/typedserver@^8.4.6",
|
||||
"@api.global/typedsocket": "npm:@api.global/typedsocket@^4.1.3",
|
||||
"@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@^7.1.0",
|
||||
"@idp.global/sdk/server": "npm:@idp.global/sdk@^1.3.1/server",
|
||||
"@push.rocks/lik": "npm:@push.rocks/lik@^6.4.1",
|
||||
"@push.rocks/projectinfo": "npm:@push.rocks/projectinfo@^5.1.0",
|
||||
"@push.rocks/qenv": "npm:@push.rocks/qenv@^6.1.4",
|
||||
"@push.rocks/smartacme": "npm:@push.rocks/smartacme@^9.5.0",
|
||||
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.1.7",
|
||||
"@push.rocks/smartdb": "npm:@push.rocks/smartdb@^2.10.1",
|
||||
"@push.rocks/smartdns": "npm:@push.rocks/smartdns@^7.9.3",
|
||||
"@push.rocks/smartfs": "npm:@push.rocks/smartfs@^1.5.1",
|
||||
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
|
||||
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.2",
|
||||
"@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.2.2",
|
||||
"@push.rocks/smartmetrics": "npm:@push.rocks/smartmetrics@^3.0.3",
|
||||
"@push.rocks/smartmigration": "npm:@push.rocks/smartmigration@1.4.1",
|
||||
"@push.rocks/smartmta": "npm:@push.rocks/smartmta@^5.3.3",
|
||||
"@push.rocks/smartnetwork": "npm:@push.rocks/smartnetwork@^4.7.2",
|
||||
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^6.0.0",
|
||||
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.2.4",
|
||||
"@push.rocks/smartproxy": "npm:@push.rocks/smartproxy@^27.11.1",
|
||||
"@push.rocks/smartradius": "npm:@push.rocks/smartradius@^1.1.2",
|
||||
"@push.rocks/smartrequest": "npm:@push.rocks/smartrequest@^5.0.3",
|
||||
"@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.10",
|
||||
"@push.rocks/smartstate": "npm:@push.rocks/smartstate@^2.3.1",
|
||||
"@push.rocks/smartunique": "npm:@push.rocks/smartunique@^3.0.9",
|
||||
"@push.rocks/smartvpn": "npm:@push.rocks/smartvpn@1.20.0",
|
||||
"@push.rocks/taskbuffer": "npm:@push.rocks/taskbuffer@^8.0.2",
|
||||
"@serve.zone/interfaces": "npm:@serve.zone/interfaces@^5.8.0",
|
||||
"@serve.zone/remoteingress": "npm:@serve.zone/remoteingress@^4.22.1",
|
||||
"@tsclass/tsclass": "npm:@tsclass/tsclass@^9.5.1",
|
||||
"lru-cache": "npm:lru-cache@^11.4.0",
|
||||
"qrcode": "npm:qrcode@^1.5.4",
|
||||
"uuid": "npm:uuid@^14.0.0"
|
||||
}
|
||||
}
|
||||
Executable
+359
@@ -0,0 +1,359 @@
|
||||
#!/bin/bash
|
||||
|
||||
# DcRouter Installer Script
|
||||
# Installs the self-extracting Linux binary by default, or builds the NodeNext
|
||||
# source package when --source is specified.
|
||||
#
|
||||
# Usage:
|
||||
# Binary install:
|
||||
# curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash
|
||||
#
|
||||
# Source install:
|
||||
# curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash -s -- --source
|
||||
#
|
||||
# Options:
|
||||
# -h, --help Show this help message
|
||||
# --version VERSION Install a specific tag/version (e.g. vX.Y.Z)
|
||||
# --install-dir DIR Installation directory (default: /opt/dcrouter)
|
||||
# --binary Install release binary (default)
|
||||
# --source Clone the tag and build the NodeNext package locally
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SHOW_HELP=0
|
||||
SPECIFIED_VERSION=""
|
||||
INSTALL_DIR="/opt/dcrouter"
|
||||
INSTALL_MODE="binary"
|
||||
GITEA_BASE_URL="https://code.foss.global"
|
||||
GITEA_REPO="serve.zone/dcrouter"
|
||||
SERVICE_NAME="dcrouter"
|
||||
BIN_DIR="/usr/local/bin"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
SHOW_HELP=1
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Error: --version requires a value"
|
||||
exit 1
|
||||
fi
|
||||
SPECIFIED_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--install-dir)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Error: --install-dir requires a value"
|
||||
exit 1
|
||||
fi
|
||||
INSTALL_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--binary)
|
||||
INSTALL_MODE="binary"
|
||||
shift
|
||||
;;
|
||||
--source)
|
||||
INSTALL_MODE="source"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use -h or --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $SHOW_HELP -eq 1 ]]; then
|
||||
echo "DcRouter Installer Script"
|
||||
echo "Installs DcRouter as a self-extracting binary or NodeNext source build."
|
||||
echo ""
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " --version VERSION Install a specific tag/version (e.g. vX.Y.Z)"
|
||||
echo " --install-dir DIR Installation directory (default: /opt/dcrouter)"
|
||||
echo " --binary Install release binary (default)"
|
||||
echo " --source Clone the tag and build the NodeNext package locally"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash -s -- --source"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash -s -- --version vX.Y.Z"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
echo "Please run as root (sudo bash install.sh or pipe to sudo bash)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$INSTALL_DIR" in
|
||||
""|"/")
|
||||
echo "Error: unsafe install directory: $INSTALL_DIR"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Error: required command not found: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_pnpm() {
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
if command -v corepack >/dev/null 2>&1; then
|
||||
corepack enable
|
||||
fi
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo "Error: pnpm is required for --source installs. Install Node.js with corepack/pnpm first."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
make_executable_if_present() {
|
||||
if [[ -f "$1" ]]; then
|
||||
chmod 0755 "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
get_latest_version() {
|
||||
echo "Fetching latest release version from Gitea..." >&2
|
||||
|
||||
local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest"
|
||||
local response
|
||||
if ! response=$(curl -fsSL "$api_url" 2>/dev/null); then
|
||||
echo "Error: Failed to fetch latest release information from Gitea API" >&2
|
||||
echo "URL: $api_url" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local version
|
||||
version=$(printf '%s' "$response" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
if [[ -z "$version" ]]; then
|
||||
echo "Error: Could not determine latest version from API response" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
detect_binary_name() {
|
||||
local os
|
||||
local arch
|
||||
os=$(uname -s)
|
||||
arch=$(uname -m)
|
||||
|
||||
if [[ "$os" != "Linux" ]]; then
|
||||
echo "Error: binary installer currently supports Linux only. Use --source for this platform." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$arch" in
|
||||
x86_64|amd64)
|
||||
echo "dcrouter-linux-x64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "dcrouter-linux-arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Error: unsupported architecture for binary install: $arch. Use --source." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "================================================"
|
||||
echo " DcRouter Installation Script"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
require_command curl
|
||||
require_command sed
|
||||
|
||||
if [[ -n "$SPECIFIED_VERSION" ]]; then
|
||||
VERSION="$SPECIFIED_VERSION"
|
||||
echo "Installing specified version: $VERSION"
|
||||
else
|
||||
VERSION=$(get_latest_version)
|
||||
echo "Installing latest version: $VERSION"
|
||||
fi
|
||||
echo "Install mode: $INSTALL_MODE"
|
||||
echo ""
|
||||
|
||||
SOURCE_REF="$VERSION"
|
||||
REPO_URL="${GITEA_BASE_URL}/${GITEA_REPO}.git"
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
SOURCE_DIR="$TEMP_DIR/source"
|
||||
BACKUP_DIR=""
|
||||
SERVICE_WAS_RUNNING=0
|
||||
SERVICE_STOPPED=0
|
||||
SYSTEMD_AVAILABLE=0
|
||||
|
||||
cleanup_temp() {
|
||||
rm -rf "$TEMP_DIR"
|
||||
}
|
||||
trap cleanup_temp EXIT
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
SYSTEMD_AVAILABLE=1
|
||||
if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
|
||||
SERVICE_WAS_RUNNING=1
|
||||
fi
|
||||
fi
|
||||
|
||||
restore_previous_installation() {
|
||||
if [[ -n "$BACKUP_DIR" && -d "$BACKUP_DIR" ]]; then
|
||||
echo "Restoring previous installation from $BACKUP_DIR..."
|
||||
rm -rf "$INSTALL_DIR" || true
|
||||
mv "$BACKUP_DIR" "$INSTALL_DIR" || true
|
||||
if [[ -f "$INSTALL_DIR/dcrouter" ]]; then
|
||||
mkdir -p "$BIN_DIR" || true
|
||||
ln -sf "$INSTALL_DIR/dcrouter" "$BIN_DIR/dcrouter" || true
|
||||
elif [[ -f "$INSTALL_DIR/cli.js" ]]; then
|
||||
mkdir -p "$BIN_DIR" || true
|
||||
ln -sf "$INSTALL_DIR/cli.js" "$BIN_DIR/dcrouter" || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
restart_previous_service_on_error() {
|
||||
if [[ $SERVICE_STOPPED -eq 1 && $SYSTEMD_AVAILABLE -eq 1 ]]; then
|
||||
echo "Installation failed after stopping DcRouter; restarting previous service..."
|
||||
systemctl start "$SERVICE_NAME" || true
|
||||
fi
|
||||
}
|
||||
|
||||
handle_install_error() {
|
||||
trap - ERR
|
||||
restore_previous_installation
|
||||
restart_previous_service_on_error
|
||||
}
|
||||
trap handle_install_error ERR
|
||||
|
||||
stop_service_if_running() {
|
||||
if [[ $SERVICE_WAS_RUNNING -eq 1 && $SYSTEMD_AVAILABLE -eq 1 ]] && systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
|
||||
echo "Stopping DcRouter service..."
|
||||
systemctl stop "$SERVICE_NAME"
|
||||
SERVICE_STOPPED=1
|
||||
fi
|
||||
}
|
||||
|
||||
move_previous_installation() {
|
||||
mkdir -p "$(dirname "$INSTALL_DIR")"
|
||||
if [[ -d "$INSTALL_DIR" ]]; then
|
||||
BACKUP_DIR="${INSTALL_DIR}.previous.$$"
|
||||
echo "Moving previous installation to $BACKUP_DIR"
|
||||
mv "$INSTALL_DIR" "$BACKUP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
install_source_build() {
|
||||
require_command git
|
||||
require_command node
|
||||
ensure_pnpm
|
||||
|
||||
echo "Cloning DcRouter source from $REPO_URL ($SOURCE_REF)..."
|
||||
git clone --depth 1 --branch "$SOURCE_REF" "$REPO_URL" "$SOURCE_DIR"
|
||||
|
||||
echo "Installing dependencies..."
|
||||
pnpm --dir "$SOURCE_DIR" install --frozen-lockfile
|
||||
|
||||
echo "Building DcRouter..."
|
||||
pnpm --dir "$SOURCE_DIR" run build
|
||||
|
||||
echo "Validating built CLI..."
|
||||
node "$SOURCE_DIR/cli.js" --version >/dev/null
|
||||
|
||||
stop_service_if_running
|
||||
move_previous_installation
|
||||
|
||||
echo "Installing source build to $INSTALL_DIR"
|
||||
mv "$SOURCE_DIR" "$INSTALL_DIR"
|
||||
make_executable_if_present "$INSTALL_DIR/cli.js"
|
||||
make_executable_if_present "$INSTALL_DIR/cli.ts.js"
|
||||
make_executable_if_present "$INSTALL_DIR/cli.child.js"
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
ln -sf "$INSTALL_DIR/cli.js" "$BIN_DIR/dcrouter"
|
||||
}
|
||||
|
||||
install_release_binary() {
|
||||
local binary_name
|
||||
local download_url
|
||||
local temp_file
|
||||
|
||||
binary_name=$(detect_binary_name)
|
||||
download_url="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${binary_name}"
|
||||
temp_file="$TEMP_DIR/$binary_name"
|
||||
|
||||
echo "Downloading DcRouter binary: $download_url"
|
||||
curl -fSL "$download_url" -o "$temp_file"
|
||||
chmod 0755 "$temp_file"
|
||||
|
||||
echo "Validating downloaded binary..."
|
||||
"$temp_file" --version >/dev/null
|
||||
|
||||
stop_service_if_running
|
||||
move_previous_installation
|
||||
|
||||
echo "Installing binary to $INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
install -m 0755 "$temp_file" "$INSTALL_DIR/dcrouter"
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
ln -sf "$INSTALL_DIR/dcrouter" "$BIN_DIR/dcrouter"
|
||||
}
|
||||
|
||||
if [[ "$INSTALL_MODE" == "source" ]]; then
|
||||
install_source_build
|
||||
else
|
||||
install_release_binary
|
||||
fi
|
||||
|
||||
echo "Symlink created: $BIN_DIR/dcrouter"
|
||||
|
||||
if ! "$BIN_DIR/dcrouter" --version >/dev/null; then
|
||||
echo "Error: Installed DcRouter CLI failed validation"
|
||||
restore_previous_installation
|
||||
restart_previous_service_on_error
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$BACKUP_DIR" && -d "$BACKUP_DIR" ]]; then
|
||||
rm -rf "$BACKUP_DIR"
|
||||
fi
|
||||
|
||||
if [[ $SERVICE_WAS_RUNNING -eq 1 && $SYSTEMD_AVAILABLE -eq 1 ]]; then
|
||||
echo "Restarting DcRouter service..."
|
||||
systemctl restart "$SERVICE_NAME"
|
||||
SERVICE_STOPPED=0
|
||||
echo "Service restarted successfully."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
trap - ERR
|
||||
|
||||
echo "================================================"
|
||||
echo " DcRouter Installation Complete!"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Installation details:"
|
||||
echo " Install directory: $INSTALL_DIR"
|
||||
echo " Symlink location: $BIN_DIR/dcrouter"
|
||||
echo " Version: $VERSION"
|
||||
echo " Mode: $INSTALL_MODE"
|
||||
echo ""
|
||||
echo "Get started:"
|
||||
echo ""
|
||||
echo " dcrouter --version"
|
||||
echo " dcrouter --help"
|
||||
echo ""
|
||||
+17
-15
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "13.36.3",
|
||||
"version": "13.40.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"dcrouter": "./cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js",
|
||||
"./interfaces": "./dist_ts_interfaces/index.js",
|
||||
@@ -15,7 +18,8 @@
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||
"start": "(node ./cli.js)",
|
||||
"startTs": "(node cli.ts.js)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && pnpm run bundle)",
|
||||
"build:binary": "(pnpm run build && tsdeno compile)",
|
||||
"build:docker": "tsdocker build --verbose",
|
||||
"release:docker": "tsdocker push --verbose",
|
||||
"bundle": "(tsbundle)",
|
||||
@@ -25,6 +29,7 @@
|
||||
"@git.zone/tsbuild": "^4.4.2",
|
||||
"@git.zone/tsbundle": "^2.10.4",
|
||||
"@git.zone/tsdocker": "^2.4.0",
|
||||
"@git.zone/tsdeno": "^1.4.0",
|
||||
"@git.zone/tsrun": "^2.0.4",
|
||||
"@git.zone/tstest": "^3.6.6",
|
||||
"@git.zone/tswatch": "^3.3.5",
|
||||
@@ -36,7 +41,7 @@
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@api.global/typedsocket": "^4.1.3",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.81.0",
|
||||
"@design.estate/dees-catalog": "^3.83.0",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@idp.global/sdk": "^1.3.1",
|
||||
"@push.rocks/lik": "^6.4.1",
|
||||
@@ -56,8 +61,8 @@
|
||||
"@push.rocks/smartnetwork": "^4.7.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.4",
|
||||
"@push.rocks/smartproxy": "^27.11.1",
|
||||
"@push.rocks/smartradius": "^1.1.2",
|
||||
"@push.rocks/smartproxy": "^27.12.1",
|
||||
"@push.rocks/smartradius": "^1.3.0",
|
||||
"@push.rocks/smartrequest": "^5.0.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.3.1",
|
||||
@@ -66,7 +71,7 @@
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.12.4",
|
||||
"@serve.zone/interfaces": "^5.8.0",
|
||||
"@serve.zone/remoteingress": "^4.18.0",
|
||||
"@serve.zone/remoteingress": "^4.22.1",
|
||||
"@tsclass/tsclass": "^9.5.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"lru-cache": "^11.4.0",
|
||||
@@ -99,25 +104,22 @@
|
||||
"VLAN assignment",
|
||||
"MAC authentication"
|
||||
],
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"mongodb-memory-server",
|
||||
"puppeteer"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"binary/**/*",
|
||||
"ts_web/**/*",
|
||||
"ts_apiclient/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"dist_ts_apiclient/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"cli.ts.js",
|
||||
"cli.child.js",
|
||||
"cli.child.ts",
|
||||
"deno.json",
|
||||
"tsconfig.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
]
|
||||
|
||||
Generated
+43
-22
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^3.81.0
|
||||
version: 3.81.0(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.83.0
|
||||
version: 3.83.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
@@ -84,11 +84,11 @@ importers:
|
||||
specifier: ^4.2.4
|
||||
version: 4.2.4
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^27.11.1
|
||||
version: 27.11.1
|
||||
specifier: ^27.12.1
|
||||
version: 27.12.1
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
'@push.rocks/smartrequest':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
@@ -114,8 +114,8 @@ importers:
|
||||
specifier: ^5.8.0
|
||||
version: 5.8.0
|
||||
'@serve.zone/remoteingress':
|
||||
specifier: ^4.18.0
|
||||
version: 4.18.0
|
||||
specifier: ^4.22.1
|
||||
version: 4.22.1
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.1
|
||||
@@ -138,6 +138,9 @@ importers:
|
||||
'@git.zone/tsbundle':
|
||||
specifier: ^2.10.4
|
||||
version: 2.10.4
|
||||
'@git.zone/tsdeno':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
'@git.zone/tsdocker':
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
@@ -362,8 +365,8 @@ packages:
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@3.81.0':
|
||||
resolution: {integrity: sha512-N7ocwSKVdjDQWmVV2XWiyg3dotGEuxP4/jhyB6duH8zJ3k63wmGm8+FeoP+LzRc8/U0Bl8w7UZrewlkIEMstUA==}
|
||||
'@design.estate/dees-catalog@3.83.0':
|
||||
resolution: {integrity: sha512-Ia4fwZ5ndziJkSE000nCro83rD8Rujki7ASHBQhL6ZDflZRJRlfuc13azVnQC2sazKlo/bWSgiiLcpc3V2IYrw==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -726,6 +729,10 @@ packages:
|
||||
resolution: {integrity: sha512-/xWOGrnuMaJ/Xo/EasaF9N3N9w1J9LDywZaRTa0UTtzbEtfJP7F2NJ9l4tWCwS+vTKpnqApX7ZueRh1h5MrwPQ==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tsdeno@1.4.0':
|
||||
resolution: {integrity: sha512-84kFa/uKPTlzeLxtHoFxefk6O9khsWWQ2PCWNbCNYIUqWHUvN9COpGq0GXWtsoxLWPhTTIeHsOX4+O55uT2MPw==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tsdocker@2.4.0':
|
||||
resolution: {integrity: sha512-GFE93RxFm8HDrSm5Ulggy4se7heb4GaNQgaWV6Mds6lhkm6GouO91xZYlmXVH9glzBoFJNG63pFXYHW6nrqf5A==}
|
||||
hasBin: true
|
||||
@@ -1422,14 +1429,14 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.4':
|
||||
resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==}
|
||||
|
||||
'@push.rocks/smartproxy@27.11.1':
|
||||
resolution: {integrity: sha512-29THhFUTr9NtU1/UBqqOgcbsHcUMHj7Dhh2XfXp6NP/rfDGUFiFFmCNcAdC3OJ0n6BgwTBOtOzo+4rJbrGJRpw==}
|
||||
'@push.rocks/smartproxy@27.12.1':
|
||||
resolution: {integrity: sha512-B1QNyGzwFea8fE2vvXO0iDzYrTfe3HcEnhPhNi6hVnmdSPe1yhNYUu5tm1CKLeCoXu/EVkAUkEFv/+d7gKa9EA==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.6':
|
||||
resolution: {integrity: sha512-G+8cyDERvbXQcb9Sd8lnYdWYz8b3Mv2LfFf1ULmucDqQhcRHvxrWX/dKsvBZrwKPR4Wg+795Dyd+E1iOOh3tHw==}
|
||||
|
||||
'@push.rocks/smartradius@1.1.2':
|
||||
resolution: {integrity: sha512-p4fHhMgXZRuyRuMQjFQLVnXBG1Fz2latJ7BGAsfInOuVUaitBr/Wni9mZULAuIIddeWwUx9QvIGlv3tgmFn/ow==}
|
||||
'@push.rocks/smartradius@1.3.0':
|
||||
resolution: {integrity: sha512-97BQhVT5gdDTNfb8LZiqaPddTMlx5Eqpsj7jTBQ2kj4tYpK0YWRiKkpBxxEXTjsIsq7iTxHeNTwc8kMZj+yU3g==}
|
||||
|
||||
'@push.rocks/smartrequest@2.1.0':
|
||||
resolution: {integrity: sha512-3eHLTRInHA+u+W98TqJwgTES7rRimBAsJC4JxVNQC3UUezmblAhM5/TIQsEBQTsbjAY8SeQKy6NHzW6iTiaD8w==}
|
||||
@@ -1712,8 +1719,9 @@ packages:
|
||||
'@serve.zone/interfaces@5.8.0':
|
||||
resolution: {integrity: sha512-0ekSKUL/b44wmmzuCRANzrjaJRAHtkqiL8cPiMASEs7UJBDqbJCrgtrlJK84pz5dxBz3jTcdznNd5qjB8c6H0A==}
|
||||
|
||||
'@serve.zone/remoteingress@4.18.0':
|
||||
resolution: {integrity: sha512-/cW9wb/e57u9+715RzV5d8HCezWtR88LcpistTNSl7GACi5ai+C2tPy7ZQprnnrNhqjfgzWiAH4bKZafwONntg==}
|
||||
'@serve.zone/remoteingress@4.22.1':
|
||||
resolution: {integrity: sha512-SkpP9VeC30A6HyonlLLE8rZVNWAPjw5NeY3pU+CWRJ0Si+hJX3FkyI4IFbOOBE+PE4JbxdIjwMNzvpxuXqZeUQ==}
|
||||
hasBin: true
|
||||
|
||||
'@smithy/chunked-blob-reader-native@4.2.3':
|
||||
resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==}
|
||||
@@ -4376,7 +4384,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.3(@push.rocks/smartserve@2.0.4)
|
||||
'@cloudflare/workers-types': 4.20260507.1
|
||||
'@design.estate/dees-catalog': 3.81.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.83.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.4.1
|
||||
'@push.rocks/smartdelay': 3.1.0
|
||||
@@ -4910,7 +4918,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@3.81.0(@tiptap/pm@2.27.2)':
|
||||
'@design.estate/dees-catalog@3.83.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.6
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
@@ -5243,6 +5251,19 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@git.zone/tsdeno@1.4.0':
|
||||
dependencies:
|
||||
'@push.rocks/early': 4.0.4
|
||||
'@push.rocks/smartcli': 4.0.21
|
||||
'@push.rocks/smartconfig': 6.1.1
|
||||
'@push.rocks/smartfs': 1.5.1
|
||||
'@push.rocks/smartshell': 3.3.8
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- react
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@git.zone/tsdocker@2.4.0':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.4.1
|
||||
@@ -6675,7 +6696,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.4': {}
|
||||
|
||||
'@push.rocks/smartproxy@27.11.1':
|
||||
'@push.rocks/smartproxy@27.12.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartlog': 3.2.2
|
||||
@@ -6699,7 +6720,7 @@ snapshots:
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
'@push.rocks/smartradius@1.1.2':
|
||||
'@push.rocks/smartradius@1.3.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.1.0
|
||||
'@push.rocks/smartpromise': 4.2.4
|
||||
@@ -7047,7 +7068,7 @@ snapshots:
|
||||
|
||||
'@serve.zone/catalog@2.12.4(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-catalog': 3.81.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.83.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-domtools': 2.5.6
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@design.estate/dees-wcctools': 3.9.0
|
||||
@@ -7064,7 +7085,7 @@ snapshots:
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
'@tsclass/tsclass': 9.5.1
|
||||
|
||||
'@serve.zone/remoteingress@4.18.0':
|
||||
'@serve.zone/remoteingress@4.22.1':
|
||||
dependencies:
|
||||
'@push.rocks/qenv': 6.1.4
|
||||
'@push.rocks/smartnftables': 1.2.0
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
mongodb-memory-server: true
|
||||
puppeteer: true
|
||||
@@ -34,6 +34,20 @@ Highlights:
|
||||
|
||||
## Install
|
||||
|
||||
Install the CLI/runtime on a Linux gateway host with the released self-extracting binary:
|
||||
|
||||
```bash
|
||||
curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash
|
||||
```
|
||||
|
||||
The installer downloads `dcrouter-linux-x64` or `dcrouter-linux-arm64` from the latest Gitea release, installs it under `/opt/dcrouter`, and links `/usr/local/bin/dcrouter`. Use `--version vX.Y.Z` to pin a release, `--install-dir /path` to change the target directory, or `--source` to clone the tag and build the NodeNext package locally.
|
||||
|
||||
```bash
|
||||
curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash -s -- --source
|
||||
```
|
||||
|
||||
Use the package as a TypeScript library:
|
||||
|
||||
```bash
|
||||
pnpm add @serve.zone/dcrouter
|
||||
```
|
||||
@@ -260,6 +274,21 @@ Supported environment overrides include:
|
||||
| `DCROUTER_CACHE_ENABLED` | Enables or disables DB-backed persistence. |
|
||||
| `DCROUTER_MAX_CONNECTIONS`, `DCROUTER_MAX_CONNECTIONS_PER_IP`, `DCROUTER_CONNECTION_RATE_LIMIT` | SmartProxy capacity and rate-limit overrides. |
|
||||
|
||||
## Docker Image
|
||||
|
||||
Release builds publish a multi-arch OCI image at `code.foss.global/serve.zone/dcrouter:latest` for `linux/amd64` and `linux/arm64`. The image sets `DCROUTER_MODE=OCI_CONTAINER` and starts `node ./cli.js`.
|
||||
|
||||
```bash
|
||||
docker run --rm --name dcrouter \
|
||||
--network host \
|
||||
-v dcrouter-data:/data \
|
||||
-e DCROUTER_BASE_DIR=/data \
|
||||
-e DCROUTER_TLS_EMAIL=ops@example.com \
|
||||
code.foss.global/serve.zone/dcrouter:latest
|
||||
```
|
||||
|
||||
Host networking is the simplest container mode for a gateway that owns HTTP/S, SMTP, DNS, RADIUS, remote ingress, and dynamic proxy ports. For narrower deployments, publish only the ports you enable in `IDcRouterOptions` or via the `DCROUTER_*` environment overrides.
|
||||
|
||||
## Published Modules
|
||||
|
||||
This repository intentionally publishes multiple module boundaries from one codebase.
|
||||
|
||||
@@ -14,6 +14,38 @@ const emptyProtocolDistribution = {
|
||||
otherTotal: 0,
|
||||
};
|
||||
|
||||
function createActiveConnectionSnapshots(entries: Array<{
|
||||
count: number;
|
||||
sourceIp?: string;
|
||||
routeId?: string;
|
||||
domain?: string;
|
||||
localPort?: number;
|
||||
}>) {
|
||||
const snapshots: any[] = [];
|
||||
let index = 0;
|
||||
for (const entry of entries) {
|
||||
for (let i = 0; i < entry.count; i++) {
|
||||
snapshots.push({
|
||||
id: `test-connection-${index++}`,
|
||||
sourceIp: entry.sourceIp || '192.0.2.10',
|
||||
sourcePort: 40000 + index,
|
||||
localPort: entry.localPort || 443,
|
||||
domain: entry.domain,
|
||||
routeId: entry.routeId,
|
||||
targetHost: '127.0.0.1',
|
||||
targetPort: 8443,
|
||||
protocol: 'https',
|
||||
state: 'active',
|
||||
startedAtMs: Date.now(),
|
||||
ageMs: 0,
|
||||
bytesIn: 0,
|
||||
bytesOut: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
function createProxyMetrics(args: {
|
||||
connectionsByRoute: Map<string, number>;
|
||||
throughputByRoute: Map<string, { in: number; out: number }>;
|
||||
@@ -90,6 +122,10 @@ tap.test('MetricsManager joins domain activity to id-keyed route metrics', async
|
||||
|
||||
const smartProxy = {
|
||||
getMetrics: () => proxyMetrics,
|
||||
getActiveConnectionSnapshots: () => createActiveConnectionSnapshots([
|
||||
{ count: 3, routeId: 'route-id-only', domain: 'alpha.example.com' },
|
||||
{ count: 1, routeId: 'route-id-only', domain: 'beta.example.com' },
|
||||
]),
|
||||
routeManager: {
|
||||
getRoutes: () => [
|
||||
{
|
||||
@@ -150,6 +186,9 @@ tap.test('MetricsManager prefers live domain request rates for current activity'
|
||||
|
||||
const smartProxy = {
|
||||
getMetrics: () => proxyMetrics,
|
||||
getActiveConnectionSnapshots: () => createActiveConnectionSnapshots([
|
||||
{ count: 10, routeId: 'route-id-only', domain: 'beta.example.com' },
|
||||
]),
|
||||
routeManager: {
|
||||
getRoutes: () => [
|
||||
{
|
||||
@@ -231,6 +270,7 @@ tap.test('MetricsManager does not duplicate backend active counts onto protocol
|
||||
|
||||
const smartProxy = {
|
||||
getMetrics: () => proxyMetrics,
|
||||
getActiveConnectionSnapshots: () => [],
|
||||
routeManager: {
|
||||
getRoutes: () => [],
|
||||
},
|
||||
@@ -265,6 +305,10 @@ tap.test('MetricsManager queues IP intelligence without awaiting enrichment', as
|
||||
const manager = new MetricsManager({
|
||||
smartProxy: {
|
||||
getMetrics: () => proxyMetrics,
|
||||
getActiveConnectionSnapshots: () => createActiveConnectionSnapshots([
|
||||
{ count: 4, sourceIp: '8.8.8.8' },
|
||||
{ count: 2, sourceIp: '1.1.1.1' },
|
||||
]),
|
||||
routeManager: { getRoutes: () => [] },
|
||||
},
|
||||
securityPolicyManager: {
|
||||
@@ -300,6 +344,11 @@ tap.test('MetricsManager aggregates top ASNs from IP intelligence', async () =>
|
||||
const manager = new MetricsManager({
|
||||
smartProxy: {
|
||||
getMetrics: () => proxyMetrics,
|
||||
getActiveConnectionSnapshots: () => createActiveConnectionSnapshots([
|
||||
{ count: 4, sourceIp: '8.8.8.8' },
|
||||
{ count: 3, sourceIp: '8.8.4.4' },
|
||||
{ count: 5, sourceIp: '1.1.1.1' },
|
||||
]),
|
||||
routeManager: { getRoutes: () => [] },
|
||||
},
|
||||
securityPolicyManager: {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { getOciContainerConfig } from '../ts_oci_container/index.js';
|
||||
|
||||
tap.test('OCI config should accept explicit DNS bind interface', async () => {
|
||||
const previousValue = process.env.DCROUTER_DNS_BIND_INTERFACE;
|
||||
process.env.DCROUTER_DNS_BIND_INTERFACE = '192.168.190.3';
|
||||
|
||||
try {
|
||||
const config = getOciContainerConfig();
|
||||
expect(config.dnsBindInterface).toEqual('192.168.190.3');
|
||||
} finally {
|
||||
if (previousValue === undefined) {
|
||||
delete process.env.DCROUTER_DNS_BIND_INTERFACE;
|
||||
} else {
|
||||
process.env.DCROUTER_DNS_BIND_INTERFACE = previousValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.36.3',
|
||||
version: '13.40.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
+18
-10
@@ -93,6 +93,9 @@ export interface IDcRouterOptions {
|
||||
* Email domains with `internal-dns` mode must be included here
|
||||
*/
|
||||
dnsScopes?: string[];
|
||||
|
||||
/** Explicit UDP bind address for the embedded DNS server. Defaults to auto-detection. */
|
||||
dnsBindInterface?: string;
|
||||
|
||||
/**
|
||||
* IPs of proxies that forward traffic to your server (optional)
|
||||
@@ -1875,16 +1878,21 @@ export class DcRouter {
|
||||
logger.log('info', `Setting up DNS server with primary nameserver: ${primaryNameserver}`);
|
||||
|
||||
// Get VM IP address for UDP binding
|
||||
const networkInterfaces = plugins.os.networkInterfaces();
|
||||
let vmIpAddress = '0.0.0.0'; // Default to all interfaces
|
||||
|
||||
// Try to find the VM's internal IP address
|
||||
for (const [_name, interfaces] of Object.entries(networkInterfaces)) {
|
||||
if (interfaces) {
|
||||
for (const iface of interfaces) {
|
||||
if (!iface.internal && iface.family === 'IPv4') {
|
||||
vmIpAddress = iface.address;
|
||||
break;
|
||||
const networkInterfaces = plugins.os.networkInterfaces() as Record<
|
||||
string,
|
||||
Array<{ internal: boolean; family: string; address: string }> | undefined
|
||||
>;
|
||||
let vmIpAddress = this.options.dnsBindInterface || '0.0.0.0'; // Default to all interfaces
|
||||
|
||||
// Try to find the VM's internal IP address when no explicit bind address is configured.
|
||||
if (!this.options.dnsBindInterface) {
|
||||
interfaceLoop: for (const [_name, interfaces] of Object.entries(networkInterfaces)) {
|
||||
if (interfaces) {
|
||||
for (const iface of interfaces) {
|
||||
if (!iface.internal && iface.family === 'IPv4') {
|
||||
vmIpAddress = iface.address;
|
||||
break interfaceLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||
import type { IRemoteIngressPerformanceConfig } from '../../../ts_interfaces/data/remoteingress.js';
|
||||
|
||||
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||
|
||||
@@ -27,6 +28,9 @@ export class RemoteIngressEdgeDoc extends plugins.smartdata.SmartDataDbDoc<Remot
|
||||
@plugins.smartdata.svDb()
|
||||
public autoDerivePorts!: boolean;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public performance?: IRemoteIngressPerformanceConfig;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public tags!: string[];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type * as plugins from '../plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Configuration for HTTP/3 (QUIC) route augmentation.
|
||||
@@ -36,22 +36,6 @@ export interface IHttp3Config {
|
||||
};
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -85,7 +69,7 @@ export function routeQualifiesForHttp3(
|
||||
if (route.action.type !== 'forward') return false;
|
||||
|
||||
// Must include port 443
|
||||
if (!portRangeIncludes443(route.match.ports)) return false;
|
||||
if (!plugins.smartproxy.portRangeIncludes(route.match.ports, 443)) return false;
|
||||
|
||||
// Must have TLS
|
||||
if (!route.action.tls) return false;
|
||||
|
||||
+24
@@ -1,3 +1,4 @@
|
||||
import { commitinfo } from './00_commitinfo_data.js';
|
||||
export * from './00_commitinfo_data.js';
|
||||
|
||||
// Re-export smartmta (excluding commitinfo to avoid naming conflict)
|
||||
@@ -18,6 +19,29 @@ export * from './remoteingress/index.js';
|
||||
export type { IHttp3Config } from './http3/index.js';
|
||||
|
||||
export const runCli = async () => {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--version') || args.includes('version')) {
|
||||
console.log(commitinfo.version);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
|
||||
console.log(`dcrouter ${commitinfo.version}
|
||||
|
||||
Usage:
|
||||
dcrouter
|
||||
dcrouter --version
|
||||
dcrouter --help
|
||||
|
||||
Environment:
|
||||
DCROUTER_MODE=OCI_CONTAINER Start with OCI container configuration
|
||||
DCROUTER_DNS_BIND_INTERFACE Override the embedded DNS UDP bind address
|
||||
DATA_DIR=<path> Override the writable dcrouter data directory
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
let options: import('./classes.dcrouter.js').IDcRouterOptions = {};
|
||||
|
||||
if (process.env.DCROUTER_MODE === 'OCI_CONTAINER') {
|
||||
|
||||
@@ -143,8 +143,9 @@ export class MetricsManager {
|
||||
public async getServerStats() {
|
||||
return this.metricsCache.get('serverStats', async () => {
|
||||
const smartMetricsData = await this.smartMetrics.getMetrics();
|
||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||
const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
|
||||
const smartProxy = this.dcRouter.smartProxy;
|
||||
const proxyMetrics = smartProxy ? smartProxy.getMetrics() : null;
|
||||
const proxyStats = smartProxy ? await smartProxy.getStatistics() : null;
|
||||
const { heapUsed, heapTotal, external, rss } = process.memoryUsage();
|
||||
|
||||
return {
|
||||
@@ -291,27 +292,44 @@ export class MetricsManager {
|
||||
});
|
||||
}
|
||||
|
||||
public async getActiveConnectionSnapshots(
|
||||
options: plugins.smartproxy.IActiveConnectionSnapshotOptions = {},
|
||||
): Promise<plugins.smartproxy.IActiveConnectionSnapshot[]> {
|
||||
const cacheKey = `activeConnectionSnapshots:${options.limit ?? 1000}:${options.routeId ?? ''}`;
|
||||
return await this.metricsCache.get<plugins.smartproxy.IActiveConnectionSnapshot[]>(cacheKey, async () => {
|
||||
if (!this.dcRouter.smartProxy) {
|
||||
return [];
|
||||
}
|
||||
return this.dcRouter.smartProxy.getActiveConnectionSnapshots(options);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Get connection info from SmartProxy
|
||||
public async getConnectionInfo() {
|
||||
return this.metricsCache.get('connectionInfo', () => {
|
||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||
|
||||
if (!proxyMetrics) {
|
||||
return [] as Array<{ type: string; count: number; source: string; lastActivity: Date }>;
|
||||
return this.metricsCache.get('connectionInfo', async () => {
|
||||
const snapshots = await this.getActiveConnectionSnapshots({ limit: 10000 });
|
||||
const connectionsByRoute = new Map<string, { count: number; lastActivity: Date }>();
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
const source = snapshot.routeId || snapshot.domain || `${snapshot.protocol || 'connection'}:${snapshot.localPort}`;
|
||||
const existing = connectionsByRoute.get(source) || { count: 0, lastActivity: new Date(snapshot.startedAtMs) };
|
||||
existing.count++;
|
||||
if (snapshot.startedAtMs > existing.lastActivity.getTime()) {
|
||||
existing.lastActivity = new Date(snapshot.startedAtMs);
|
||||
}
|
||||
connectionsByRoute.set(source, existing);
|
||||
}
|
||||
|
||||
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||
const connectionInfo: Array<{ type: string; count: number; source: string; lastActivity: Date }> = [];
|
||||
|
||||
for (const [routeName, count] of connectionsByRoute) {
|
||||
for (const [source, info] of connectionsByRoute) {
|
||||
connectionInfo.push({
|
||||
type: 'https',
|
||||
count,
|
||||
source: routeName,
|
||||
lastActivity: new Date(),
|
||||
count: info.count,
|
||||
source,
|
||||
lastActivity: info.lastActivity,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return connectionInfo;
|
||||
});
|
||||
}
|
||||
@@ -547,7 +565,8 @@ export class MetricsManager {
|
||||
public async getNetworkStats() {
|
||||
// Use shorter cache TTL for network stats to ensure real-time updates
|
||||
return this.metricsCache.get('networkStats', async () => {
|
||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||
const smartProxy = this.dcRouter.smartProxy;
|
||||
const proxyMetrics = smartProxy ? smartProxy.getMetrics() : null;
|
||||
|
||||
if (!proxyMetrics) {
|
||||
return {
|
||||
@@ -568,8 +587,22 @@ export class MetricsManager {
|
||||
};
|
||||
}
|
||||
|
||||
// Get metrics using the new API
|
||||
const connectionsByIP = proxyMetrics.connections.byIP();
|
||||
const activeConnectionSnapshots = await this.getActiveConnectionSnapshots({ limit: 10000 });
|
||||
|
||||
const connectionsByIP = new Map<string, number>();
|
||||
const connectionsByRoute = new Map<string, number>();
|
||||
const activeConnectionsByDomain = new Map<string, number>();
|
||||
|
||||
for (const snapshot of activeConnectionSnapshots) {
|
||||
connectionsByIP.set(snapshot.sourceIp, (connectionsByIP.get(snapshot.sourceIp) || 0) + 1);
|
||||
if (snapshot.routeId) {
|
||||
connectionsByRoute.set(snapshot.routeId, (connectionsByRoute.get(snapshot.routeId) || 0) + 1);
|
||||
}
|
||||
if (snapshot.domain) {
|
||||
activeConnectionsByDomain.set(snapshot.domain, (activeConnectionsByDomain.get(snapshot.domain) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const instantThroughput = proxyMetrics.throughput.instant();
|
||||
|
||||
// Get throughput rate
|
||||
@@ -578,8 +611,11 @@ export class MetricsManager {
|
||||
bytesOutPerSecond: instantThroughput.out
|
||||
};
|
||||
|
||||
// Get top IPs by connection count
|
||||
const topIPs = proxyMetrics.connections.topIPs(10);
|
||||
// Get top IPs by active connection count
|
||||
const topIPs = Array.from(connectionsByIP.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
.map(([ip, count]) => ({ ip, count }));
|
||||
|
||||
// Get total data transferred
|
||||
const totalDataTransferred = {
|
||||
@@ -738,7 +774,6 @@ export class MetricsManager {
|
||||
const topASNs = await this.buildTopASNs(observedIps, allIPData);
|
||||
|
||||
// Build domain activity using per-IP domain request counts from Rust engine
|
||||
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
||||
|
||||
// Aggregate per-IP domain request counts into per-domain totals
|
||||
@@ -773,6 +808,9 @@ export class MetricsManager {
|
||||
for (const entry of protocolCache) {
|
||||
if (entry.domain) allKnownDomains.add(entry.domain);
|
||||
}
|
||||
for (const snapshot of activeConnectionSnapshots) {
|
||||
if (snapshot.domain) allKnownDomains.add(snapshot.domain);
|
||||
}
|
||||
|
||||
// Build reverse map: concrete domain → canonical route key(s)
|
||||
const domainToRoutes = new Map<string, string[]>();
|
||||
@@ -844,7 +882,7 @@ export class MetricsManager {
|
||||
}
|
||||
|
||||
domainAgg.set(domain, {
|
||||
activeConnections: Math.round(totalConns),
|
||||
activeConnections: activeConnectionsByDomain.get(domain) ?? Math.round(totalConns),
|
||||
bytesInPerSec: totalIn,
|
||||
bytesOutPerSec: totalOut,
|
||||
routeCount: routeKeys.length,
|
||||
|
||||
@@ -67,6 +67,7 @@ export class RemoteIngressHandler {
|
||||
dataArg.listenPorts || [],
|
||||
dataArg.tags,
|
||||
dataArg.autoDerivePorts ?? true,
|
||||
dataArg.performance,
|
||||
);
|
||||
|
||||
// Sync allowed edges with the hub
|
||||
@@ -129,6 +130,7 @@ export class RemoteIngressHandler {
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
enabled: dataArg.enabled,
|
||||
performance: dataArg.performance,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
import { MetricsManager } from '../../monitoring/index.js';
|
||||
import { requireOpsAuth } from '../helpers/auth.js';
|
||||
|
||||
export class SecurityHandler {
|
||||
@@ -46,18 +45,7 @@ export class SecurityHandler {
|
||||
'getActiveConnections',
|
||||
async (dataArg, toolsArg) => {
|
||||
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
|
||||
const connections = await this.getActiveConnections(dataArg.protocol, dataArg.state);
|
||||
const connectionInfos: interfaces.data.IConnectionInfo[] = connections.map(conn => ({
|
||||
id: conn.id,
|
||||
remoteAddress: conn.source.ip,
|
||||
localAddress: conn.destination.ip,
|
||||
startTime: conn.startTime,
|
||||
protocol: conn.type === 'http' ? 'https' : conn.type as any,
|
||||
state: conn.status === 'active' ? 'connected' : conn.status as any,
|
||||
bytesReceived: (conn as any)._throughputIn || 0,
|
||||
bytesSent: (conn as any)._throughputOut || 0,
|
||||
connectionCount: conn.bytesTransferred || 1,
|
||||
}));
|
||||
const connectionInfos = await this.getActiveConnections(dataArg.protocol, dataArg.state);
|
||||
const totalConnections = connectionInfos.reduce((sum, conn) => sum + (conn.connectionCount || 1), 0);
|
||||
|
||||
const summary = {
|
||||
@@ -362,106 +350,66 @@ export class SecurityHandler {
|
||||
private async getActiveConnections(
|
||||
protocol?: 'http' | 'https' | 'smtp' | 'smtps',
|
||||
state?: string
|
||||
): Promise<Array<{
|
||||
id: string;
|
||||
type: 'http' | 'smtp' | 'dns';
|
||||
source: {
|
||||
ip: string;
|
||||
port: number;
|
||||
country?: string;
|
||||
};
|
||||
destination: {
|
||||
ip: string;
|
||||
port: number;
|
||||
service?: string;
|
||||
};
|
||||
startTime: number;
|
||||
bytesTransferred: number;
|
||||
status: 'active' | 'idle' | 'closing';
|
||||
}>> {
|
||||
const connections: Array<{
|
||||
id: string;
|
||||
type: 'http' | 'smtp' | 'dns';
|
||||
source: {
|
||||
ip: string;
|
||||
port: number;
|
||||
country?: string;
|
||||
};
|
||||
destination: {
|
||||
ip: string;
|
||||
port: number;
|
||||
service?: string;
|
||||
};
|
||||
startTime: number;
|
||||
bytesTransferred: number;
|
||||
status: 'active' | 'idle' | 'closing';
|
||||
}> = [];
|
||||
|
||||
// Get connection info and network stats from MetricsManager if available
|
||||
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
|
||||
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||
|
||||
// One aggregate row per IP with real throughput data
|
||||
if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) {
|
||||
let connIndex = 0;
|
||||
const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server';
|
||||
): Promise<interfaces.data.IConnectionInfo[]> {
|
||||
const metricsManager = this.opsServerRef.dcRouterRef.metricsManager;
|
||||
if (!metricsManager) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const [ip, count] of networkStats.connectionsByIP) {
|
||||
const tp = networkStats.throughputByIP?.get(ip);
|
||||
connections.push({
|
||||
id: `ip-${connIndex++}`,
|
||||
type: 'http',
|
||||
source: {
|
||||
ip: ip,
|
||||
port: 0,
|
||||
},
|
||||
destination: {
|
||||
ip: publicIp,
|
||||
port: 443,
|
||||
service: 'proxy',
|
||||
},
|
||||
startTime: 0,
|
||||
bytesTransferred: count, // Store connection count here
|
||||
status: 'active',
|
||||
// Attach real throughput for the handler mapping
|
||||
...(tp ? { _throughputIn: tp.in, _throughputOut: tp.out } : {}),
|
||||
} as any);
|
||||
}
|
||||
} else if (connectionInfo.length > 0) {
|
||||
// Fallback to route-based connection info if no IP data available
|
||||
connectionInfo.forEach((info, index) => {
|
||||
connections.push({
|
||||
id: `conn-${index}`,
|
||||
type: 'http',
|
||||
source: {
|
||||
ip: 'unknown',
|
||||
port: 0,
|
||||
},
|
||||
destination: {
|
||||
ip: this.opsServerRef.dcRouterRef.options.publicIp || 'server',
|
||||
port: 443,
|
||||
service: info.source,
|
||||
},
|
||||
startTime: info.lastActivity.getTime(),
|
||||
bytesTransferred: 0,
|
||||
status: 'active',
|
||||
});
|
||||
});
|
||||
const snapshots = await metricsManager.getActiveConnectionSnapshots({ limit: 10000 });
|
||||
const connections = snapshots.map((snapshot): interfaces.data.IConnectionInfo => ({
|
||||
id: String(snapshot.id),
|
||||
remoteAddress: snapshot.sourcePort === null
|
||||
? snapshot.sourceIp
|
||||
: `${snapshot.sourceIp}:${snapshot.sourcePort}`,
|
||||
localAddress: snapshot.targetHost
|
||||
? `${snapshot.targetHost}:${snapshot.targetPort ?? snapshot.localPort}`
|
||||
: `${this.opsServerRef.dcRouterRef.options.publicIp || 'server'}:${snapshot.localPort}`,
|
||||
startTime: snapshot.startedAtMs,
|
||||
protocol: this.mapSnapshotProtocol(snapshot),
|
||||
state: this.mapSnapshotState(snapshot.state),
|
||||
bytesReceived: snapshot.bytesIn,
|
||||
bytesSent: snapshot.bytesOut,
|
||||
}));
|
||||
|
||||
return connections.filter((connection) => {
|
||||
if (protocol && connection.protocol !== protocol) {
|
||||
return false;
|
||||
}
|
||||
if (state && connection.state !== state) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private mapSnapshotProtocol(
|
||||
snapshot: plugins.smartproxy.IActiveConnectionSnapshot,
|
||||
): interfaces.data.IConnectionInfo['protocol'] {
|
||||
if (snapshot.localPort === 465) {
|
||||
return 'smtps';
|
||||
}
|
||||
|
||||
// Filter by protocol if specified
|
||||
if (protocol) {
|
||||
return connections.filter(conn => {
|
||||
if (protocol === 'https' || protocol === 'http') {
|
||||
return conn.type === 'http';
|
||||
}
|
||||
return conn.type === protocol.replace('s', ''); // smtp/smtps -> smtp
|
||||
});
|
||||
if ([25, 587, 2525].includes(snapshot.localPort)) {
|
||||
return 'smtp';
|
||||
}
|
||||
|
||||
return connections;
|
||||
|
||||
switch (snapshot.protocol) {
|
||||
case 'http':
|
||||
return 'http';
|
||||
case 'https':
|
||||
case 'tls':
|
||||
case 'tls-passthrough':
|
||||
case 'tls-reencrypt':
|
||||
case 'tls-socket-handler':
|
||||
case 'quic':
|
||||
return 'https';
|
||||
default:
|
||||
return snapshot.localPort === 80 ? 'http' : 'https';
|
||||
}
|
||||
}
|
||||
|
||||
private mapSnapshotState(state: string): interfaces.data.IConnectionInfo['state'] {
|
||||
return state === 'closing' ? 'closing' : 'connected';
|
||||
}
|
||||
|
||||
private async getRateLimitStatus(
|
||||
|
||||
+9
-9
@@ -1,13 +1,13 @@
|
||||
// node native
|
||||
import * as dns from 'dns';
|
||||
import * as fs from 'fs';
|
||||
import * as crypto from 'crypto';
|
||||
import * as http from 'http';
|
||||
import * as net from 'net';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as tls from 'tls';
|
||||
import * as util from 'util';
|
||||
import * as dns from 'node:dns';
|
||||
import * as fs from 'node:fs';
|
||||
import * as crypto from 'node:crypto';
|
||||
import * as http from 'node:http';
|
||||
import * as net from 'node:net';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import * as tls from 'node:tls';
|
||||
import * as util from 'node:util';
|
||||
|
||||
export {
|
||||
dns,
|
||||
|
||||
@@ -91,7 +91,6 @@ export class RadiusServer {
|
||||
private vlanManager: VlanManager;
|
||||
private accountingManager: AccountingManager;
|
||||
private config: IRadiusServerConfig;
|
||||
private clientSecrets: Map<string, string> = new Map();
|
||||
private running: boolean = false;
|
||||
|
||||
// Statistics
|
||||
@@ -138,24 +137,18 @@ export class RadiusServer {
|
||||
await this.vlanManager.importMappings(this.config.vlanAssignment.mappings);
|
||||
}
|
||||
|
||||
// Build client secrets map
|
||||
this.buildClientSecretsMap();
|
||||
const cidrSecrets = this.buildClientSecretsMap();
|
||||
|
||||
// Create the RADIUS server
|
||||
this.radiusServer = new plugins.smartradius.RadiusServer({
|
||||
authPort: this.config.authPort,
|
||||
acctPort: this.config.acctPort,
|
||||
bindAddress: this.config.bindAddress,
|
||||
defaultSecret: this.getDefaultSecret(),
|
||||
cidrSecrets,
|
||||
authenticationHandler: this.handleAuthentication.bind(this),
|
||||
accountingHandler: this.handleAccounting.bind(this),
|
||||
});
|
||||
|
||||
// Configure per-client secrets
|
||||
for (const [ip, secret] of this.clientSecrets) {
|
||||
this.radiusServer.setClientSecret(ip, secret);
|
||||
}
|
||||
|
||||
// Start the server
|
||||
await this.radiusServer.start();
|
||||
|
||||
@@ -189,19 +182,22 @@ export class RadiusServer {
|
||||
/**
|
||||
* Handle authentication request
|
||||
*/
|
||||
private async handleAuthentication(request: any): Promise<any> {
|
||||
private async handleAuthentication(
|
||||
request: plugins.smartradius.IAuthenticationRequest,
|
||||
): Promise<plugins.smartradius.IAuthenticationResponse> {
|
||||
this.stats.authRequests++;
|
||||
|
||||
const authData: IAuthRequestData = {
|
||||
username: request.attributes?.UserName || '',
|
||||
password: request.attributes?.UserPassword,
|
||||
nasIpAddress: request.attributes?.NasIpAddress || request.source?.address || '',
|
||||
nasPort: request.attributes?.NasPort,
|
||||
nasPortType: request.attributes?.NasPortType,
|
||||
nasIdentifier: request.attributes?.NasIdentifier,
|
||||
calledStationId: request.attributes?.CalledStationId,
|
||||
callingStationId: request.attributes?.CallingStationId,
|
||||
serviceType: request.attributes?.ServiceType,
|
||||
username: request.username || '',
|
||||
password: request.password,
|
||||
nasIpAddress: request.nasIpAddress || request.clientAddress || '',
|
||||
nasPort: request.nasPort,
|
||||
nasPortType: request.nasPortType !== undefined ? String(request.nasPortType) : undefined,
|
||||
nasIdentifier: request.nasIdentifier,
|
||||
calledStationId: request.calledStationId,
|
||||
callingStationId: request.callingStationId,
|
||||
serviceType: request.serviceType !== undefined ? String(request.serviceType) : undefined,
|
||||
framedMtu: request.framedMtu,
|
||||
};
|
||||
|
||||
logger.log('debug', `RADIUS Auth Request: user=${authData.username}, NAS=${authData.nasIpAddress}`);
|
||||
@@ -215,15 +211,15 @@ export class RadiusServer {
|
||||
logger.log('info', `RADIUS Auth Accept: user=${authData.username}, VLAN=${result.vlanId}`);
|
||||
|
||||
// Build response with VLAN attributes
|
||||
const response: any = {
|
||||
const response: plugins.smartradius.IAuthenticationResponse = {
|
||||
code: plugins.smartradius.ERadiusCode.AccessAccept,
|
||||
replyMessage: result.replyMessage,
|
||||
};
|
||||
|
||||
// Add VLAN attributes if assigned
|
||||
if (result.vlanId !== undefined) {
|
||||
response.tunnelType = 13; // VLAN
|
||||
response.tunnelMediumType = 6; // IEEE 802
|
||||
response.tunnelType = plugins.smartradius.ETunnelType.Vlan;
|
||||
response.tunnelMediumType = plugins.smartradius.ETunnelMediumType.Ieee802;
|
||||
response.tunnelPrivateGroupId = String(result.vlanId);
|
||||
}
|
||||
|
||||
@@ -257,34 +253,37 @@ export class RadiusServer {
|
||||
/**
|
||||
* Handle accounting request
|
||||
*/
|
||||
private async handleAccounting(request: any): Promise<any> {
|
||||
private async handleAccounting(
|
||||
request: plugins.smartradius.IAccountingRequest,
|
||||
): Promise<plugins.smartradius.IAccountingResponse> {
|
||||
this.stats.accountingRequests++;
|
||||
|
||||
if (!this.config.accounting?.enabled) {
|
||||
// Still respond even if not tracking
|
||||
return { code: plugins.smartradius.ERadiusCode.AccountingResponse };
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const statusType = request.attributes?.AcctStatusType;
|
||||
const sessionId = request.attributes?.AcctSessionId || '';
|
||||
const statusType = request.statusType;
|
||||
const sessionId = request.sessionId || '';
|
||||
|
||||
const accountingData = {
|
||||
sessionId,
|
||||
username: request.attributes?.UserName || '',
|
||||
macAddress: request.attributes?.CallingStationId,
|
||||
nasIpAddress: request.attributes?.NasIpAddress || request.source?.address || '',
|
||||
nasPort: request.attributes?.NasPort,
|
||||
nasPortType: request.attributes?.NasPortType,
|
||||
nasIdentifier: request.attributes?.NasIdentifier,
|
||||
calledStationId: request.attributes?.CalledStationId,
|
||||
callingStationId: request.attributes?.CallingStationId,
|
||||
inputOctets: request.attributes?.AcctInputOctets,
|
||||
outputOctets: request.attributes?.AcctOutputOctets,
|
||||
inputPackets: request.attributes?.AcctInputPackets,
|
||||
outputPackets: request.attributes?.AcctOutputPackets,
|
||||
sessionTime: request.attributes?.AcctSessionTime,
|
||||
terminateCause: request.attributes?.AcctTerminateCause,
|
||||
serviceType: request.attributes?.ServiceType,
|
||||
username: request.username || '',
|
||||
macAddress: request.callingStationId,
|
||||
nasIpAddress: request.nasIpAddress || request.clientAddress || '',
|
||||
nasPort: request.nasPort,
|
||||
nasPortType: request.nasPortType !== undefined ? String(request.nasPortType) : undefined,
|
||||
nasIdentifier: request.nasIdentifier,
|
||||
calledStationId: request.calledStationId,
|
||||
callingStationId: request.callingStationId,
|
||||
inputOctets: request.inputOctets,
|
||||
outputOctets: request.outputOctets,
|
||||
inputPackets: request.inputPackets,
|
||||
outputPackets: request.outputPackets,
|
||||
sessionTime: request.sessionTime,
|
||||
terminateCause: request.terminateCause !== undefined ? String(request.terminateCause) : undefined,
|
||||
framedIpAddress: request.framedIpAddress,
|
||||
serviceType: request.serviceType !== undefined ? String(request.serviceType) : undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -311,7 +310,7 @@ export class RadiusServer {
|
||||
logger.log('error', `RADIUS accounting error: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
return { code: plugins.smartradius.ERadiusCode.AccountingResponse };
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -391,37 +390,18 @@ export class RadiusServer {
|
||||
/**
|
||||
* Build client secrets map from configuration
|
||||
*/
|
||||
private buildClientSecretsMap(): void {
|
||||
this.clientSecrets.clear();
|
||||
private buildClientSecretsMap(): Record<string, string> {
|
||||
const cidrSecrets: Record<string, string> = {};
|
||||
|
||||
for (const client of this.config.clients) {
|
||||
if (!client.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle CIDR ranges
|
||||
if (client.ipRange.includes('/')) {
|
||||
// For CIDR ranges, we'll use the network address as key
|
||||
// In practice, smartradius may handle this differently
|
||||
const [network] = client.ipRange.split('/');
|
||||
this.clientSecrets.set(network, client.secret);
|
||||
} else {
|
||||
this.clientSecrets.set(client.ipRange, client.secret);
|
||||
}
|
||||
cidrSecrets[client.ipRange] = client.secret;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default secret for unknown clients
|
||||
*/
|
||||
private getDefaultSecret(): string {
|
||||
// Use first enabled client's secret as default, or a random one
|
||||
for (const client of this.config.clients) {
|
||||
if (client.enabled) {
|
||||
return client.secret;
|
||||
}
|
||||
}
|
||||
return plugins.crypto.randomBytes(16).toString('hex');
|
||||
return cidrSecrets;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,21 +410,19 @@ export class RadiusServer {
|
||||
async addClient(client: IRadiusClient): Promise<void> {
|
||||
// Check if client already exists
|
||||
const existingIndex = this.config.clients.findIndex(c => c.name === client.name);
|
||||
const previousClient = existingIndex >= 0 ? this.config.clients[existingIndex] : undefined;
|
||||
if (existingIndex >= 0) {
|
||||
this.config.clients[existingIndex] = client;
|
||||
} else {
|
||||
this.config.clients.push(client);
|
||||
}
|
||||
|
||||
// Update client secrets if running
|
||||
if (this.running && this.radiusServer && client.enabled) {
|
||||
if (client.ipRange.includes('/')) {
|
||||
const [network] = client.ipRange.split('/');
|
||||
this.radiusServer.setClientSecret(network, client.secret);
|
||||
this.clientSecrets.set(network, client.secret);
|
||||
} else {
|
||||
this.radiusServer.setClientSecret(client.ipRange, client.secret);
|
||||
this.clientSecrets.set(client.ipRange, client.secret);
|
||||
if (this.running && this.radiusServer) {
|
||||
if (previousClient) {
|
||||
this.radiusServer.removeNetworkSecret(previousClient.ipRange);
|
||||
}
|
||||
if (client.enabled) {
|
||||
this.radiusServer.setNetworkSecret(client.ipRange, client.secret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,12 +438,8 @@ export class RadiusServer {
|
||||
const client = this.config.clients[index];
|
||||
this.config.clients.splice(index, 1);
|
||||
|
||||
// Remove from secrets map
|
||||
if (client.ipRange.includes('/')) {
|
||||
const [network] = client.ipRange.split('/');
|
||||
this.clientSecrets.delete(network);
|
||||
} else {
|
||||
this.clientSecrets.delete(client.ipRange);
|
||||
if (this.radiusServer) {
|
||||
this.radiusServer.removeNetworkSecret(client.ipRange);
|
||||
}
|
||||
|
||||
logger.log('info', `RADIUS client removed: ${name}`);
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
||||
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
||||
import { RemoteIngressEdgeDoc } from '../db/index.js';
|
||||
|
||||
interface IRemoteIngressFirewallConfig {
|
||||
blockedIps?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
|
||||
*/
|
||||
function extractPorts(portRange: number | Array<number | { from: number; to: number }>): number[] {
|
||||
const ports = new Set<number>();
|
||||
if (typeof portRange === 'number') {
|
||||
ports.add(portRange);
|
||||
} else if (Array.isArray(portRange)) {
|
||||
for (const entry of portRange) {
|
||||
if (typeof entry === 'number') {
|
||||
ports.add(entry);
|
||||
} else if (typeof entry === 'object' && 'from' in entry && 'to' in entry) {
|
||||
for (let p = entry.from; p <= entry.to; p++) {
|
||||
ports.add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] {
|
||||
const ports = new Set<number>(plugins.smartproxy.expandPortRange(portRange) as number[]);
|
||||
return [...ports].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
@@ -59,6 +43,7 @@ export class RemoteIngressManager {
|
||||
listenPortsUdp: doc.listenPortsUdp,
|
||||
enabled: doc.enabled,
|
||||
autoDerivePorts: doc.autoDerivePorts,
|
||||
performance: doc.performance,
|
||||
tags: doc.tags,
|
||||
createdAt: doc.createdAt,
|
||||
updatedAt: doc.updatedAt,
|
||||
@@ -189,6 +174,7 @@ export class RemoteIngressManager {
|
||||
listenPorts: number[] = [],
|
||||
tags?: string[],
|
||||
autoDerivePorts: boolean = true,
|
||||
performance?: IRemoteIngressPerformanceConfig,
|
||||
): Promise<IRemoteIngress> {
|
||||
const id = plugins.uuid.v4();
|
||||
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
||||
@@ -201,6 +187,7 @@ export class RemoteIngressManager {
|
||||
listenPorts,
|
||||
enabled: true,
|
||||
autoDerivePorts,
|
||||
performance,
|
||||
tags: tags || [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
@@ -237,6 +224,7 @@ export class RemoteIngressManager {
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
enabled?: boolean;
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
},
|
||||
): Promise<IRemoteIngress | null> {
|
||||
@@ -249,6 +237,7 @@ export class RemoteIngressManager {
|
||||
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
||||
if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts;
|
||||
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
||||
if (updates.performance !== undefined) edge.performance = updates.performance;
|
||||
if (updates.tags !== undefined) edge.tags = updates.tags;
|
||||
edge.updatedAt = Date.now();
|
||||
|
||||
@@ -317,17 +306,19 @@ export class RemoteIngressManager {
|
||||
* 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[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> {
|
||||
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> = [];
|
||||
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig }> {
|
||||
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig }> = [];
|
||||
for (const edge of this.edges.values()) {
|
||||
if (edge.enabled) {
|
||||
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
||||
const performance = edge.performance && Object.keys(edge.performance).length > 0 ? edge.performance : undefined;
|
||||
result.push({
|
||||
id: edge.id,
|
||||
secret: edge.secret,
|
||||
listenPorts: this.getEffectiveListenPorts(edge),
|
||||
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
||||
...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}),
|
||||
...(performance ? { performance } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface IRemoteIngress {
|
||||
enabled: boolean;
|
||||
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
|
||||
autoDerivePorts: boolean;
|
||||
/** Optional per-edge performance overrides. */
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
@@ -55,6 +57,10 @@ export interface IRemoteIngressPerformanceConfig {
|
||||
maxStreamWindowBytes?: number;
|
||||
sustainedStreamWindowBytes?: number;
|
||||
quicDatagramReceiveBufferBytes?: number;
|
||||
streamFramePayloadBytes?: number;
|
||||
firstDataConnectTimeoutMs?: number;
|
||||
clientWriteTimeoutMs?: number;
|
||||
serverFirstPorts?: number[];
|
||||
}
|
||||
|
||||
export interface IRemoteIngressPerformanceEffective {
|
||||
@@ -65,6 +71,10 @@ export interface IRemoteIngressPerformanceEffective {
|
||||
maxStreamWindowBytes: number;
|
||||
sustainedStreamWindowBytes: number;
|
||||
quicDatagramReceiveBufferBytes: number;
|
||||
streamFramePayloadBytes: number;
|
||||
firstDataConnectTimeoutMs: number;
|
||||
clientWriteTimeoutMs: number;
|
||||
serverFirstPorts: number[];
|
||||
}
|
||||
|
||||
export interface IRemoteIngressFlowControlStatus {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as authInterfaces from '../data/auth.js';
|
||||
import type { IRemoteIngress, IRemoteIngressStatus } from '../data/remoteingress.js';
|
||||
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../data/remoteingress.js';
|
||||
|
||||
// ============================================================================
|
||||
// Remote Ingress Edge Management
|
||||
@@ -20,6 +20,7 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
|
||||
name: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
};
|
||||
response: {
|
||||
@@ -63,6 +64,7 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
enabled?: boolean;
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
};
|
||||
response: {
|
||||
|
||||
@@ -74,6 +74,10 @@ export function getOciContainerConfig(): IDcRouterOptions {
|
||||
options.dnsScopes = dnsScopes;
|
||||
}
|
||||
|
||||
if (process.env.DCROUTER_DNS_BIND_INTERFACE) {
|
||||
options.dnsBindInterface = process.env.DCROUTER_DNS_BIND_INTERFACE;
|
||||
}
|
||||
|
||||
// Email config
|
||||
const emailHostname = process.env.DCROUTER_EMAIL_HOSTNAME;
|
||||
const emailPorts = parseCommaSeparatedNumbers(process.env.DCROUTER_EMAIL_PORTS);
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.36.3',
|
||||
version: '13.40.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -1120,6 +1120,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
@@ -1135,6 +1136,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
performance: dataArg.performance,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
@@ -1187,6 +1189,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name?: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
@@ -1203,6 +1206,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
performance: dataArg.performance,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
publicIp: this.getEdgePublicIp(edge.id),
|
||||
ports: this.getPortsHtml(edge),
|
||||
tunnels: this.getEdgeTunnelCount(edge.id),
|
||||
maxConnections: this.getMaxConnectionsHtml(edge),
|
||||
window: this.getWindowHtml(edge.id),
|
||||
queues: this.getQueuesHtml(edge.id),
|
||||
traffic: this.getTrafficHtml(edge.id),
|
||||
@@ -261,6 +262,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports'} .description=${'Comma-separated port numbers, optional'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'maxStreamsPerEdge'} .label=${'Max Connections'} .description=${'Optional maximum concurrent client connections for this edge. Leave empty to use the hub default.'}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags'} .description=${'Comma-separated, optional'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
@@ -284,12 +286,20 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||
: undefined;
|
||||
const autoDerivePorts = formData.autoDerivePorts !== false;
|
||||
let performance: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
|
||||
try {
|
||||
performance = this.collectPerformanceOverride(formData);
|
||||
} catch (err: unknown) {
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
|
||||
return;
|
||||
}
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.createRemoteIngressAction,
|
||||
{ name, listenPorts, autoDerivePorts, tags },
|
||||
{ name, listenPorts, autoDerivePorts, performance, tags },
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
@@ -338,6 +348,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .value=${edge.name}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports'} .description=${'Comma-separated port numbers'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'maxStreamsPerEdge'} .label=${'Max Connections'} .description=${'Optional maximum concurrent client connections for this edge. Leave empty to use the hub default.'} .value=${edge.performance?.maxStreamsPerEdge?.toString() || ''}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags'} .description=${'Comma-separated'} .value=${(edge.tags || []).join(', ')}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
@@ -359,6 +370,14 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||
: [];
|
||||
const autoDerivePorts = formData.autoDerivePorts !== false;
|
||||
let performance: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
|
||||
try {
|
||||
performance = this.collectPerformanceOverride(formData, edge.performance);
|
||||
} catch (err: unknown) {
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
|
||||
return;
|
||||
}
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
@@ -369,6 +388,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
name: formData.name || edge.name,
|
||||
listenPorts,
|
||||
autoDerivePorts,
|
||||
performance,
|
||||
tags,
|
||||
},
|
||||
);
|
||||
@@ -475,6 +495,19 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
return status?.activeTunnels || 0;
|
||||
}
|
||||
|
||||
private getMaxConnectionsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult | string {
|
||||
const status = this.getEdgeStatus(edge.id);
|
||||
const override = edge.performance?.maxStreamsPerEdge;
|
||||
const effective = status?.performance?.maxStreamsPerEdge;
|
||||
if (!override && !effective) return '-';
|
||||
return html`
|
||||
<div class="metricStack">
|
||||
<span>${override || effective}</span>
|
||||
<span class="metricMuted">${override ? 'edge override' : 'hub default'}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getTransportHtml(edgeId: string): TemplateResult | string {
|
||||
const status = this.getEdgeStatus(edgeId);
|
||||
if (!status?.connected) return '-';
|
||||
@@ -535,4 +568,27 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
}
|
||||
return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private collectPerformanceOverride(
|
||||
formData: Record<string, any>,
|
||||
base?: interfaces.data.IRemoteIngressPerformanceConfig,
|
||||
): interfaces.data.IRemoteIngressPerformanceConfig | undefined {
|
||||
const next: interfaces.data.IRemoteIngressPerformanceConfig = { ...(base || {}) };
|
||||
const maxStreamsText = `${formData.maxStreamsPerEdge || ''}`.trim();
|
||||
if (maxStreamsText) {
|
||||
const maxStreamsPerEdge = Number.parseInt(maxStreamsText, 10);
|
||||
if (!Number.isInteger(maxStreamsPerEdge) || maxStreamsPerEdge < 1) {
|
||||
throw new Error('Max Connections must be a positive integer');
|
||||
}
|
||||
next.maxStreamsPerEdge = maxStreamsPerEdge;
|
||||
} else {
|
||||
delete next.maxStreamsPerEdge;
|
||||
}
|
||||
|
||||
if (Object.keys(next).length > 0) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return base ? {} : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +304,16 @@ export class OpsViewConfig extends DeesElement {
|
||||
{ key: 'Connected Edge IPs', value: ri.connectedEdgeIps?.length > 0 ? ri.connectedEdgeIps : null, type: 'pills' },
|
||||
];
|
||||
|
||||
if (ri.performance) {
|
||||
fields.push(
|
||||
{ key: 'Performance Profile', value: ri.performance.profile || null, type: 'badge' },
|
||||
{ key: 'Max Connections / Edge', value: ri.performance.maxStreamsPerEdge ?? null },
|
||||
{ key: 'Client Write Timeout', value: ri.performance.clientWriteTimeoutMs ? `${ri.performance.clientWriteTimeoutMs} ms` : null },
|
||||
{ key: 'First Data Timeout', value: ri.performance.firstDataConnectTimeoutMs ? `${ri.performance.firstDataConnectTimeoutMs} ms` : null },
|
||||
{ key: 'Server-first Ports', value: ri.performance.serverFirstPorts?.length ? ri.performance.serverFirstPorts.map(String) : null, type: 'pills' },
|
||||
);
|
||||
}
|
||||
|
||||
const actions: IConfigSectionAction[] = [
|
||||
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'network', subview: 'remoteingress' } },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user