Compare commits

...

14 Commits

Author SHA1 Message Date
e733067c25 v11.19.1
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-30 18:14:51 +00:00
bc2ed808f9 fix(vpn): configure SmartVPN client exports with explicit server endpoint and split-tunnel allowed IPs 2026-03-30 18:14:51 +00:00
61d856f371 v11.19.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-30 17:24:18 +00:00
a8d52a4709 feat(vpn): document tag-based VPN access control, declarative clients, and destination policy options 2026-03-30 17:24:17 +00:00
f685ce9928 v11.18.0
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-30 17:08:57 +00:00
699aa8a8e1 feat(vpn-ui): add format selection for VPN client config exports 2026-03-30 17:08:57 +00:00
6fa7206f86 v11.17.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-30 16:49:58 +00:00
11cce23e21 feat(vpn): expand VPN operations view with client management and config export actions 2026-03-30 16:49:58 +00:00
d109554134 v11.16.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-30 13:06:14 +00:00
cc3a7cb5b6 feat(vpn): add destination-based VPN routing policy and standardize socket proxy forwarding 2026-03-30 13:06:14 +00:00
d53cff6a94 v11.15.0
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-30 12:07:58 +00:00
eb211348d2 feat(vpn): add tag-based VPN route access control and support configured initial VPN clients 2026-03-30 12:07:58 +00:00
43618abeba v11.14.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-30 08:59:38 +00:00
dd9769b814 feat(docs): document VPN access control and add OpsServer VPN navigation 2026-03-30 08:59:38 +00:00
19 changed files with 614 additions and 141 deletions

View File

@@ -1,5 +1,57 @@
# Changelog
## 2026-03-30 - 11.19.1 - fix(vpn)
configure SmartVPN client exports with explicit server endpoint and split-tunnel allowed IPs
- Pass the configured WireGuard server endpoint directly to SmartVPN instead of rewriting generated client configs in dcrouter.
- Set client allowed IPs to the VPN subnet so generated WireGuard configs default to split-tunnel routing.
- Update documentation to reflect SmartVPN startup, dashboard/API coverage, and the new split-tunnel behavior.
- Bump @push.rocks/smartvpn from 1.14.0 to 1.16.1 to support the updated VPN configuration flow.
## 2026-03-30 - 11.19.0 - feat(vpn)
document tag-based VPN access control, declarative clients, and destination policy options
- Adds documentation for restricting VPN-protected routes with allowedServerDefinedClientTags.
- Documents pre-defined VPN clients in configuration via vpnConfig.clients.
- Describes destinationPolicy behavior for forceTarget, allow, and block traffic handling.
- Updates interface docs to reflect serverDefinedClientTags and revised VPN server status fields.
## 2026-03-30 - 11.18.0 - feat(vpn-ui)
add format selection for VPN client config exports
- Show an export modal that lets operators choose between WireGuard (.conf) and SmartVPN (.json) client configs.
- Update VPN client row actions to read the selected item from actionData for toggle, export, rotate keys, and delete handlers.
## 2026-03-30 - 11.17.0 - feat(vpn)
expand VPN operations view with client management and config export actions
- adds predefined VPN clients to the dev server configuration for local testing
- adds table actions to create clients, export WireGuard configs, rotate client keys, toggle access, and delete clients
- updates the VPN view layout and stats grid binding to match the current component API
## 2026-03-30 - 11.16.0 - feat(vpn)
add destination-based VPN routing policy and standardize socket proxy forwarding
- replace configurable VPN forwarding mode with socket-based forwarding and always enable proxy protocol support to SmartProxy from localhost
- add destinationPolicy configuration for controlling default VPN traffic handling, including forceTarget, allow, and block rules
- remove forwarding mode reporting from VPN status APIs, logs, and ops UI to reflect the simplified VPN runtime model
- update @push.rocks/smartvpn to 1.14.0 to support the new VPN routing behavior
## 2026-03-30 - 11.15.0 - feat(vpn)
add tag-based VPN route access control and support configured initial VPN clients
- allow VPN-protected routes to restrict access to clients with matching server-defined tags instead of always permitting the full VPN subnet
- create configured VPN clients automatically on startup and re-apply routes when VPN clients change
- rename VPN client tag fields to serverDefinedClientTags across APIs, interfaces, handlers, and UI with legacy tag migration on load
- upgrade @push.rocks/smartvpn from 1.12.0 to 1.13.0
## 2026-03-30 - 11.14.0 - feat(docs)
document VPN access control and add OpsServer VPN navigation
- Adds comprehensive README documentation for VPN access control, configuration, operating modes, and client management
- Updates TypeScript interface documentation with VPN-related route, client, status, telemetry, and API request types
- Extends web dashboard documentation and router view list to include VPN management
## 2026-03-30 - 11.13.0 - feat(vpn)
add VPN server management and route-based VPN access control

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "11.13.0",
"version": "11.19.1",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -59,7 +59,7 @@
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.3.0",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartvpn": "1.12.0",
"@push.rocks/smartvpn": "1.16.1",
"@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/catalog": "^2.9.0",
"@serve.zone/interfaces": "^5.3.0",

19
pnpm-lock.yaml generated
View File

@@ -96,8 +96,8 @@ importers:
specifier: ^3.0.9
version: 3.0.9
'@push.rocks/smartvpn':
specifier: 1.12.0
version: 1.12.0
specifier: 1.16.1
version: 1.16.1
'@push.rocks/taskbuffer':
specifier: ^8.0.2
version: 8.0.2
@@ -1246,6 +1246,9 @@ packages:
'@push.rocks/smartnftables@1.0.1':
resolution: {integrity: sha512-o822GH4J8dlEBvNLbm+CwU4h6isMUEh03tf2ZnOSWXc5iewRDdKdOCDwI/e+WdnGYWyv7gvH0DHztCmne6rTCg==}
'@push.rocks/smartnftables@1.1.0':
resolution: {integrity: sha512-7JNzerlW20HEl2wKMBIHltwneCQRpXiD2lJkXZZc02ctnfjgFejXVDIeWomhPx6PZ0Z6zmqdF6rrFDtDHyqqfA==}
'@push.rocks/smartnpm@2.0.6':
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
@@ -1330,8 +1333,8 @@ packages:
'@push.rocks/smartversion@3.0.5':
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
'@push.rocks/smartvpn@1.12.0':
resolution: {integrity: sha512-lwZCK8fopkms3c6ZSrUghuVNFi7xOXMSkGDSptQM2K3tu2UbajhpdxlAVMODY8n6caQr5ZXp0kHdtwVU9WKi5Q==}
'@push.rocks/smartvpn@1.16.1':
resolution: {integrity: sha512-LQzt3ajMKIs3anYki/3drt7XcCuekoKvApCltLEjsoGEEX5JkXGSZFB+UFvqEhG8NcEuHw574rU3tB2orHzKTQ==}
'@push.rocks/smartwatch@6.4.0':
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
@@ -6331,6 +6334,11 @@ snapshots:
'@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartnftables@1.1.0':
dependencies:
'@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartnpm@2.0.6':
dependencies:
'@push.rocks/consolecolor': 2.0.3
@@ -6562,8 +6570,9 @@ snapshots:
'@types/semver': 7.7.1
semver: 7.7.4
'@push.rocks/smartvpn@1.12.0':
'@push.rocks/smartvpn@1.16.1':
dependencies:
'@push.rocks/smartnftables': 1.1.0
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrust': 1.3.2

194
readme.md
View File

@@ -4,7 +4,7 @@
**dcrouter: The all-in-one gateway for your datacenter.** 🚀
A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), DNS, RADIUS, and remote edge ingress — all from a single process. Designed for enterprises requiring robust traffic management, automatic TLS certificate provisioning, distributed edge networking, and enterprise-grade email infrastructure.
A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), DNS, RADIUS, VPN, and remote edge ingress — all from a single process. Designed for enterprises requiring robust traffic management, automatic TLS certificate provisioning, VPN-based access control, distributed edge networking, and enterprise-grade email infrastructure.
## Issue Reporting and Security
@@ -23,6 +23,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- [DNS Server](#dns-server)
- [RADIUS Server](#radius-server)
- [Remote Ingress](#remote-ingress)
- [VPN Access Control](#vpn-access-control)
- [Certificate Management](#certificate-management)
- [Storage & Caching](#storage--caching)
- [Security Features](#security-features)
@@ -73,6 +74,17 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Real-time status monitoring** — connected/disconnected state, public IP, active tunnels, heartbeat tracking
- **OpsServer dashboard** with enable/disable, edit, secret regeneration, token copy, and delete actions
### 🔐 VPN Access Control (powered by [smartvpn](https://code.foss.global/push.rocks/smartvpn))
- **WireGuard + native transports** — standard WireGuard clients (iOS, Android, macOS, Windows, Linux) plus custom WebSocket/QUIC tunnels
- **Route-level VPN gating** — mark any route with `vpn: { required: true }` to restrict access to VPN clients only
- **Tag-based access control** — assign `serverDefinedClientTags` to clients and restrict routes with `allowedServerDefinedClientTags`
- **Constructor-defined clients** — pre-define VPN clients with tags in config for declarative, code-driven setup
- **Rootless operation** — uses userspace NAT (smoltcp) with no root required
- **Destination policy** — configurable `forceTarget`, `block`, or `allow` with allowList/blockList for granular traffic control
- **Client management** — create, enable, disable, rotate keys, export WireGuard/SmartVPN configs via OpsServer API and dashboard
- **IP-based enforcement** — VPN clients get IPs from a configurable subnet; SmartProxy enforces `ipAllowList` per route
- **PROXY protocol v2** — the NAT engine sends PP v2 on outbound connections to preserve VPN client identity
### ⚡ High Performance
- **Rust-powered proxy engine** via SmartProxy for maximum throughput
- **Rust-powered MTA engine** via smartmta (TypeScript + Rust hybrid) for reliable email delivery
@@ -89,7 +101,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### 🖥️ OpsServer Dashboard
- **Web-based management interface** with real-time monitoring
- **JWT authentication** with session persistence
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, remote ingress edges, and security events
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, remote ingress edges, VPN clients, and security events
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
- **Remote ingress management** with connection token generation and one-click copy
- **Read-only configuration display** — DcRouter is configured through code
@@ -248,6 +260,15 @@ const router = new DcRouter({
hubDomain: 'hub.example.com',
},
// VPN — restrict sensitive routes to VPN clients
vpnConfig: {
enabled: true,
serverEndpoint: 'vpn.example.com',
clients: [
{ clientId: 'dev-laptop', serverDefinedClientTags: ['engineering'] },
],
},
// Persistent storage
storage: { fsPath: '/var/lib/dcrouter/data' },
@@ -276,6 +297,7 @@ graph TB
DNS[DNS Queries]
RAD[RADIUS Clients]
EDGE[Edge Nodes]
VPN[VPN Clients]
end
subgraph "DcRouter Core"
@@ -285,6 +307,7 @@ graph TB
DS[SmartDNS Server<br/><i>Rust-powered</i>]
RS[SmartRadius Server]
RI[RemoteIngress Hub<br/><i>Rust data plane</i>]
VS[SmartVPN Server<br/><i>Rust data plane</i>]
CM[Certificate Manager<br/><i>smartacme v9</i>]
OS[OpsServer Dashboard]
MM[Metrics Manager]
@@ -305,12 +328,14 @@ graph TB
DNS --> DS
RAD --> RS
EDGE --> RI
VPN --> VS
DC --> SP
DC --> ES
DC --> DS
DC --> RS
DC --> RI
DC --> VS
DC --> CM
DC --> OS
DC --> MM
@@ -347,8 +372,8 @@ 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:
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.
1. **On `start()`**: DcRouter initializes OpsServer (default port 3000, configurable via `opsServerPort`), then spins up SmartProxy, smartmta, SmartDNS, SmartRadius, RemoteIngress, and SmartVPN based on which configs are provided. Services start in dependency order via `ServiceManager`.
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. SmartVPN runs a Rust data plane for WireGuard and custom transports. 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.
### Rust-Powered Architecture
@@ -361,6 +386,7 @@ DcRouter itself is a pure TypeScript orchestrator, but several of its core sub-c
| **smartmta** | `mailer-bin` | SMTP server + client, DKIM/SPF/DMARC, content scanning, IP reputation |
| **SmartDNS** | `smartdns-bin` | DNS server (UDP + DNS-over-HTTPS), DNSSEC, DNS client resolution |
| **RemoteIngress** | `remoteingress-bin` | Edge tunnel data plane, multiplexed streams, heartbeat management |
| **SmartVPN** | `smartvpn_daemon` | WireGuard (boringtun), Noise IK handshake, QUIC/WS transports, userspace NAT (smoltcp) |
| **SmartRadius** | — | Pure TypeScript (no Rust component) |
## Configuration Reference
@@ -428,6 +454,27 @@ interface IDcRouterOptions {
};
};
// ── VPN ───────────────────────────────────────────────────────
/** VPN server for route-level access control */
vpnConfig?: {
enabled?: boolean; // default: false
subnet?: string; // default: '10.8.0.0/24'
wgListenPort?: number; // default: 51820
dns?: string[]; // DNS servers pushed to VPN clients
serverEndpoint?: string; // Hostname in generated client configs
clients?: Array<{ // Pre-defined VPN clients
clientId: string;
serverDefinedClientTags?: string[];
description?: string;
}>;
destinationPolicy?: { // Traffic routing policy
default: 'forceTarget' | 'block' | 'allow';
target?: string; // IP for forceTarget (default: '127.0.0.1')
allowList?: string[]; // Pass through directly
blockList?: string[]; // Always block (overrides allowList)
};
};
// ── HTTP/3 (QUIC) ────────────────────────────────────────────
/** HTTP/3 config — enabled by default on qualifying HTTPS routes */
http3?: {
@@ -975,6 +1022,128 @@ The OpsServer Remote Ingress view provides:
| **Copy Token** | Generate and copy a base64url connection token to clipboard |
| **Delete** | Remove the edge registration |
## VPN Access Control
DcRouter integrates [`@push.rocks/smartvpn`](https://code.foss.global/push.rocks/smartvpn) to provide VPN-based route access control. VPN clients connect via standard WireGuard or native WebSocket/QUIC transports, receive an IP from a configurable subnet, and can then access routes that are restricted to VPN-only traffic.
### How It Works
1. **SmartVPN daemon** runs inside dcrouter with a Rust data plane (WireGuard via `boringtun`, custom protocol via Noise IK)
2. Clients connect and get assigned an IP from the VPN subnet (e.g. `10.8.0.0/24`)
3. **Split tunnel** by default — generated WireGuard configs only route VPN subnet traffic through the tunnel (`AllowedIPs = 10.8.0.0/24`), so regular internet traffic stays direct
4. Routes with `vpn: { required: true }` get `security.ipAllowList` automatically injected
5. When `allowedServerDefinedClientTags` is set, only matching client IPs are injected (not the whole subnet)
6. SmartProxy enforces the allowlist — only authorized VPN clients can access protected routes
7. All VPN traffic is forced through SmartProxy via userspace NAT with PROXY protocol v2 — no root required
### Destination Policy
By default, VPN client traffic is redirected to localhost (SmartProxy) via `forceTarget`. You can customize this with a destination policy:
```typescript
// Default: all traffic → SmartProxy
destinationPolicy: { default: 'forceTarget', target: '127.0.0.1' }
// Allow direct access to a backend subnet
destinationPolicy: {
default: 'forceTarget',
target: '127.0.0.1',
allowList: ['192.168.190.*'], // direct access to this subnet
blockList: ['192.168.190.1'], // except the gateway
}
// Block everything except specific IPs
destinationPolicy: {
default: 'block',
allowList: ['10.0.0.*', '192.168.1.*'],
}
```
### Configuration
```typescript
const router = new DcRouter({
vpnConfig: {
enabled: true,
subnet: '10.8.0.0/24', // VPN client IP pool (default)
wgListenPort: 51820, // WireGuard UDP port (default)
serverEndpoint: 'vpn.example.com', // Hostname in generated client configs
dns: ['1.1.1.1', '8.8.8.8'], // DNS servers pushed to clients
// Pre-define VPN clients with server-defined tags
clients: [
{ clientId: 'alice-laptop', serverDefinedClientTags: ['engineering'], description: 'Dev laptop' },
{ clientId: 'bob-phone', serverDefinedClientTags: ['engineering', 'mobile'] },
{ clientId: 'carol-desktop', serverDefinedClientTags: ['finance'] },
],
// Optional: customize destination policy (default: forceTarget → localhost)
// destinationPolicy: { default: 'forceTarget', target: '127.0.0.1', allowList: ['192.168.1.*'] },
},
smartProxyConfig: {
routes: [
// 🔐 VPN-only: any VPN client can access
{
name: 'internal-app',
match: { domains: ['internal.example.com'], ports: [443] },
action: {
type: 'forward',
targets: [{ host: '192.168.1.50', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' },
},
vpn: { required: true },
},
// 🔐 VPN + tag-restricted: only 'engineering' tagged clients
{
name: 'eng-dashboard',
match: { domains: ['eng.example.com'], ports: [443] },
action: {
type: 'forward',
targets: [{ host: '192.168.1.51', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' },
},
vpn: { required: true, allowedServerDefinedClientTags: ['engineering'] },
// → alice + bob can access, carol cannot
},
// 🌐 Public: no VPN required
{
name: 'public-site',
match: { domains: ['example.com'], ports: [443] },
action: {
type: 'forward',
targets: [{ host: '192.168.1.10', port: 80 }],
tls: { mode: 'terminate', certificate: 'auto' },
},
},
],
},
});
```
### Client Tags
SmartVPN distinguishes between two types of client tags:
| Tag Type | Set By | Purpose |
|----------|--------|---------|
| `serverDefinedClientTags` | Admin (via config or API) | **Trusted** — used for route access control |
| `clientDefinedClientTags` | Connecting client | **Informational** — displayed in dashboard, never used for security |
Routes with `allowedServerDefinedClientTags` only permit VPN clients whose admin-assigned tags match. Clients cannot influence their own server-defined tags.
### Client Management via OpsServer
The OpsServer dashboard and API provide full VPN client lifecycle management:
- **Create client** — generates WireGuard keypairs, assigns IP, returns a ready-to-use `.conf` file
- **Enable / Disable** — toggle client access without deleting
- **Rotate keys** — generate fresh keypairs (invalidates old ones)
- **Export config** — download in WireGuard (`.conf`) or SmartVPN (`.json`) format
- **Telemetry** — per-client bytes sent/received, keepalives, rate limiting
- **Delete** — remove a client and revoke access
Standard WireGuard clients on any platform (iOS, Android, macOS, Windows, Linux) can connect using the generated `.conf` file — no custom VPN software needed.
## Certificate Management
DcRouter uses [`@push.rocks/smartacme`](https://code.foss.global/push.rocks/smartacme) v9 for ACME certificate provisioning. smartacme v9 brings significant improvements over previous versions:
@@ -1149,8 +1318,12 @@ The OpsServer provides a web-based management interface served on port 3000 by d
| 📊 **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput |
| 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics |
| 📧 **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents |
| 🛣️ **Routes** | Merged route list (hardcoded + programmatic), create/edit/toggle/override routes |
| 🔑 **API Tokens** | Token management with scopes, create/revoke/roll/toggle |
| 🔐 **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning, import/export |
| 🌍 **RemoteIngress** | Edge node management, connection status, token generation, enable/disable |
| 🔐 **VPN** | VPN client management, server status, create/toggle/export/rotate/delete clients |
| 📡 **RADIUS** | NAS client management, VLAN mappings, session monitoring, accounting |
| 📜 **Logs** | Real-time log viewer with level filtering and search |
| ⚙️ **Configuration** | Read-only view of current system configuration |
| 🛡️ **Security** | IP reputation, rate limit status, blocked connections |
@@ -1215,6 +1388,17 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
'getRecentLogs' // Retrieve system logs with filtering
'getLogStream' // Stream live logs
// VPN
'getVpnClients' // List all registered VPN clients
'getVpnStatus' // VPN server status (running, subnet, port, keys)
'createVpnClient' // Create client → returns WireGuard config (shown once)
'deleteVpnClient' // Remove a VPN client
'enableVpnClient' // Enable a disabled client
'disableVpnClient' // Disable a client
'rotateVpnClientKey' // Generate new keys (invalidates old ones)
'exportVpnClientConfig' // Export WireGuard (.conf) or SmartVPN (.json) config
'getVpnClientTelemetry' // Per-client bytes sent/received, keepalives
// RADIUS
'getRadiusSessions' // Active RADIUS sessions
'getRadiusClients' // List NAS clients
@@ -1332,6 +1516,7 @@ const router = new DcRouter(options: IDcRouterOptions);
| `radiusServer` | `RadiusServer` | RADIUS server instance |
| `remoteIngressManager` | `RemoteIngressManager` | Edge registration CRUD manager |
| `tunnelManager` | `TunnelManager` | Tunnel lifecycle and status manager |
| `vpnManager` | `VpnManager` | VPN server lifecycle and client CRUD manager |
| `storageManager` | `StorageManager` | Storage backend |
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
| `metricsManager` | `MetricsManager` | Metrics collector |
@@ -1458,6 +1643,7 @@ The container exposes all service ports:
| 1812, 1813 | UDP | RADIUS auth/acct |
| 3000 | TCP | OpsServer dashboard |
| 8443 | TCP | Remote ingress tunnels |
| 51820 | UDP | WireGuard VPN |
| 2900030000 | TCP | Dynamic port range |
### Building the Image

View File

@@ -25,6 +25,16 @@ const devRouter = new DcRouter({
},
],
},
// VPN with pre-defined clients
vpnConfig: {
enabled: true,
serverEndpoint: 'vpn.dev.local',
clients: [
{ clientId: 'dev-laptop', serverDefinedClientTags: ['engineering', 'dev'], description: 'Developer laptop' },
{ clientId: 'ci-runner', serverDefinedClientTags: ['engineering', 'ci'], description: 'CI/CD pipeline' },
{ clientId: 'admin-desktop', serverDefinedClientTags: ['admin'], description: 'Admin workstation' },
],
},
// Disable cache/mongo for dev
cacheConfig: { enabled: false },
});

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '11.13.0',
version: '11.19.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -206,8 +206,21 @@ export interface IDcRouterOptions {
dns?: string[];
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
serverEndpoint?: string;
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
forwardingMode?: 'tun' | 'socket';
/** Pre-defined VPN clients created on startup */
clients?: Array<{
clientId: string;
serverDefinedClientTags?: string[];
description?: string;
}>;
/** Destination routing policy for VPN client traffic.
* Default in socket mode: { default: 'forceTarget', target: '127.0.0.1' } (all traffic → SmartProxy).
* Default in tun mode: not set (all traffic passes through). */
destinationPolicy?: {
default: 'forceTarget' | 'block' | 'allow';
target?: string;
allowList?: string[];
blockList?: string[];
};
};
}
@@ -453,7 +466,14 @@ export class DcRouter {
() => this.getConstructorRoutes(),
() => this.smartProxy,
() => this.options.http3,
() => this.options.vpnConfig?.enabled ? (this.options.vpnConfig.subnet || '10.8.0.0/24') : undefined,
this.options.vpnConfig?.enabled
? (tags?: string[]) => {
if (tags?.length && this.vpnManager) {
return this.vpnManager.getClientIpsForServerDefinedTags(tags);
}
return [this.options.vpnConfig?.subnet || '10.8.0.0/24'];
}
: undefined,
);
this.apiTokenManager = new ApiTokenManager(this.storageManager);
await this.apiTokenManager.initialize();
@@ -664,9 +684,8 @@ export class DcRouter {
if (this.vpnManager && this.options.vpnConfig?.enabled) {
const subnet = this.vpnManager.getSubnet();
const wgPort = this.options.vpnConfig.wgListenPort ?? 51820;
const mode = this.vpnManager.forwardingMode;
const clientCount = this.vpnManager.listClients().length;
logger.log('info', `VPN Service: mode=${mode}, subnet=${subnet}, wg=:${wgPort}, clients=${clientCount}`);
logger.log('info', `VPN Service: subnet=${subnet}, wg=:${wgPort}, clients=${clientCount}`);
}
// Remote Ingress summary
@@ -950,19 +969,14 @@ export class DcRouter {
smartProxyConfig.proxyIPs = ['127.0.0.1'];
}
// When VPN is in socket mode, the userspace NAT engine sends PP v2 headers
// on outbound connections to SmartProxy to preserve VPN client tunnel IPs.
// VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost
if (this.options.vpnConfig?.enabled) {
const vpnForwardingMode = this.options.vpnConfig.forwardingMode
?? (process.getuid?.() === 0 ? 'tun' : 'socket');
if (vpnForwardingMode === 'socket') {
smartProxyConfig.acceptProxyProtocol = true;
if (!smartProxyConfig.proxyIPs) {
smartProxyConfig.proxyIPs = [];
}
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
smartProxyConfig.proxyIPs.push('127.0.0.1');
}
smartProxyConfig.acceptProxyProtocol = true;
if (!smartProxyConfig.proxyIPs) {
smartProxyConfig.proxyIPs = [];
}
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
smartProxyConfig.proxyIPs.push('127.0.0.1');
}
}
@@ -2085,7 +2099,12 @@ export class DcRouter {
wgListenPort: this.options.vpnConfig.wgListenPort,
dns: this.options.vpnConfig.dns,
serverEndpoint: this.options.vpnConfig.serverEndpoint,
forwardingMode: this.options.vpnConfig.forwardingMode,
initialClients: this.options.vpnConfig.clients,
destinationPolicy: this.options.vpnConfig.destinationPolicy,
onClientChanged: () => {
// Re-apply routes so tag-based ipAllowLists get updated
this.routeConfigManager?.applyRoutes();
},
});
await this.vpnManager.start();
@@ -2104,11 +2123,23 @@ export class DcRouter {
if (dcrouterRoute.vpn?.required) {
injectedCount++;
const existing = route.security?.ipAllowList || [];
let vpnAllowList: string[];
if (dcrouterRoute.vpn.allowedServerDefinedClientTags?.length && this.vpnManager) {
// Tag-based: only specific client IPs
vpnAllowList = this.vpnManager.getClientIpsForServerDefinedTags(
dcrouterRoute.vpn.allowedServerDefinedClientTags,
);
} else {
// No tags specified: entire VPN subnet
vpnAllowList = [vpnSubnet];
}
return {
...route,
security: {
...route.security,
ipAllowList: [...existing, vpnSubnet],
ipAllowList: [...existing, ...vpnAllowList],
},
};
}
@@ -2116,7 +2147,7 @@ export class DcRouter {
});
if (injectedCount > 0) {
logger.log('info', `VPN: Injected ipAllowList (${vpnSubnet}) into ${injectedCount} VPN-protected route(s)`);
logger.log('info', `VPN: Injected ipAllowList into ${injectedCount} VPN-protected route(s)`);
}
return result;

View File

@@ -23,7 +23,7 @@ export class RouteConfigManager {
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
private getHttp3Config?: () => IHttp3Config | undefined,
private getVpnSubnet?: () => string | undefined,
private getVpnAllowList?: (tags?: string[]) => string[],
) {}
/**
@@ -246,7 +246,7 @@ export class RouteConfigManager {
// Private: apply merged routes to SmartProxy
// =========================================================================
private async applyRoutes(): Promise<void> {
public async applyRoutes(): Promise<void> {
const smartProxy = this.getSmartProxy();
if (!smartProxy) return;
@@ -262,9 +262,9 @@ export class RouteConfigManager {
enabledRoutes.push(route);
}
// Add enabled programmatic routes (with HTTP/3 augmentation if enabled)
// Add enabled programmatic routes (with HTTP/3 and VPN augmentation)
const http3Config = this.getHttp3Config?.();
const vpnSubnet = this.getVpnSubnet?.();
const vpnAllowList = this.getVpnAllowList;
for (const stored of this.storedRoutes.values()) {
if (stored.enabled) {
let route = stored.route;
@@ -272,15 +272,16 @@ export class RouteConfigManager {
route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
}
// Inject VPN security for programmatic routes with vpn.required
if (vpnSubnet) {
if (vpnAllowList) {
const dcRoute = route as IDcRouterRouteConfig;
if (dcRoute.vpn?.required) {
const existing = route.security?.ipAllowList || [];
const allowList = vpnAllowList(dcRoute.vpn.allowedServerDefinedClientTags);
route = {
...route,
security: {
...route.security,
ipAllowList: [...existing, vpnSubnet],
ipAllowList: [...existing, ...allowList],
},
};
}

View File

@@ -25,7 +25,7 @@ export class VpnHandler {
const clients = manager.listClients().map((c) => ({
clientId: c.clientId,
enabled: c.enabled,
tags: c.tags,
serverDefinedClientTags: c.serverDefinedClientTags,
description: c.description,
assignedIp: c.assignedIp,
createdAt: c.createdAt,
@@ -48,7 +48,6 @@ export class VpnHandler {
return {
status: {
running: false,
forwardingMode: 'socket' as const,
subnet: vpnConfig?.subnet || '10.8.0.0/24',
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
serverPublicKeys: null,
@@ -62,7 +61,6 @@ export class VpnHandler {
return {
status: {
running: manager.running,
forwardingMode: manager.forwardingMode,
subnet: manager.getSubnet(),
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
serverPublicKeys: manager.getServerPublicKeys(),
@@ -89,7 +87,7 @@ export class VpnHandler {
try {
const bundle = await manager.createClient({
clientId: dataArg.clientId,
tags: dataArg.tags,
serverDefinedClientTags: dataArg.serverDefinedClientTags,
description: dataArg.description,
});
@@ -98,7 +96,7 @@ export class VpnHandler {
client: {
clientId: bundle.entry.clientId,
enabled: bundle.entry.enabled ?? true,
tags: bundle.entry.tags,
serverDefinedClientTags: bundle.entry.serverDefinedClientTags,
description: bundle.entry.description,
assignedIp: bundle.entry.assignedIp,
createdAt: Date.now(),

View File

@@ -14,8 +14,21 @@ export interface IVpnManagerConfig {
dns?: string[];
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
serverEndpoint?: string;
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
forwardingMode?: 'tun' | 'socket';
/** Pre-defined VPN clients created on startup (idempotent — skips already-persisted clients) */
initialClients?: Array<{
clientId: string;
serverDefinedClientTags?: string[];
description?: string;
}>;
/** Called when clients are created/deleted/toggled — triggers route re-application */
onClientChanged?: () => void;
/** Destination routing policy override. Default: forceTarget to 127.0.0.1 */
destinationPolicy?: {
default: 'forceTarget' | 'block' | 'allow';
target?: string;
allowList?: string[];
blockList?: string[];
};
}
interface IPersistedServerKeys {
@@ -28,7 +41,7 @@ interface IPersistedServerKeys {
interface IPersistedClient {
clientId: string;
enabled: boolean;
tags?: string[];
serverDefinedClientTags?: string[];
description?: string;
assignedIp?: string;
noisePublicKey: string;
@@ -36,6 +49,8 @@ interface IPersistedClient {
createdAt: number;
updatedAt: number;
expiresAt?: string;
/** @deprecated Legacy field — migrated to serverDefinedClientTags on load */
tags?: string[];
}
/**
@@ -48,19 +63,10 @@ export class VpnManager {
private vpnServer?: plugins.smartvpn.VpnServer;
private clients: Map<string, IPersistedClient> = new Map();
private serverKeys?: IPersistedServerKeys;
private _forwardingMode: 'tun' | 'socket';
constructor(storageManager: StorageManager, config: IVpnManagerConfig) {
this.storageManager = storageManager;
this.config = config;
// Auto-detect forwarding mode: tun if root, socket otherwise
this._forwardingMode = config.forwardingMode
?? (process.getuid?.() === 0 ? 'tun' : 'socket');
}
/** The effective forwarding mode (tun or socket). */
public get forwardingMode(): 'tun' | 'socket' {
return this._forwardingMode;
}
/** The VPN subnet CIDR. */
@@ -92,7 +98,7 @@ export class VpnManager {
publicKey: client.noisePublicKey,
wgPublicKey: client.wgPublicKey,
enabled: client.enabled,
tags: client.tags,
serverDefinedClientTags: client.serverDefinedClientTags,
description: client.description,
assignedIp: client.assignedIp,
expiresAt: client.expiresAt,
@@ -113,16 +119,37 @@ export class VpnManager {
publicKey: this.serverKeys.noisePublicKey,
subnet,
dns: this.config.dns,
forwardingMode: this._forwardingMode,
forwardingMode: 'socket',
transportMode: 'all',
wgPrivateKey: this.serverKeys.wgPrivateKey,
wgListenPort,
clients: clientEntries,
socketForwardProxyProtocol: this._forwardingMode === 'socket',
socketForwardProxyProtocol: true,
destinationPolicy: this.config.destinationPolicy
?? { default: 'forceTarget' as const, target: '127.0.0.1' },
serverEndpoint: this.config.serverEndpoint
? `${this.config.serverEndpoint}:${wgListenPort}`
: undefined,
clientAllowedIPs: [subnet],
};
await this.vpnServer.start(serverConfig);
logger.log('info', `VPN server started: mode=${this._forwardingMode}, subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
// Create initial clients from config (idempotent — skip already-persisted)
if (this.config.initialClients) {
for (const initial of this.config.initialClients) {
if (!this.clients.has(initial.clientId)) {
const bundle = await this.createClient({
clientId: initial.clientId,
serverDefinedClientTags: initial.serverDefinedClientTags,
description: initial.description,
});
logger.log('info', `VPN: Created initial client '${initial.clientId}' (IP: ${bundle.entry.assignedIp})`);
}
}
}
logger.log('info', `VPN server started: subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
}
/**
@@ -148,7 +175,7 @@ export class VpnManager {
*/
public async createClient(opts: {
clientId: string;
tags?: string[];
serverDefinedClientTags?: string[];
description?: string;
}): Promise<plugins.smartvpn.IClientConfigBundle> {
if (!this.vpnServer) {
@@ -157,24 +184,15 @@ export class VpnManager {
const bundle = await this.vpnServer.createClient({
clientId: opts.clientId,
tags: opts.tags,
serverDefinedClientTags: opts.serverDefinedClientTags,
description: opts.description,
});
// Update WireGuard config endpoint if serverEndpoint is configured
if (this.config.serverEndpoint && bundle.wireguardConfig) {
const wgPort = this.config.wgListenPort ?? 51820;
bundle.wireguardConfig = bundle.wireguardConfig.replace(
/Endpoint\s*=\s*.+/,
`Endpoint = ${this.config.serverEndpoint}:${wgPort}`,
);
}
// Persist client entry (without private keys)
const persisted: IPersistedClient = {
clientId: bundle.entry.clientId,
enabled: bundle.entry.enabled ?? true,
tags: bundle.entry.tags,
serverDefinedClientTags: bundle.entry.serverDefinedClientTags,
description: bundle.entry.description,
assignedIp: bundle.entry.assignedIp,
noisePublicKey: bundle.entry.publicKey,
@@ -186,6 +204,7 @@ export class VpnManager {
this.clients.set(persisted.clientId, persisted);
await this.persistClient(persisted);
this.config.onClientChanged?.();
return bundle;
}
@@ -199,6 +218,7 @@ export class VpnManager {
await this.vpnServer.removeClient(clientId);
this.clients.delete(clientId);
await this.storageManager.delete(`${STORAGE_PREFIX_CLIENTS}${clientId}`);
this.config.onClientChanged?.();
}
/**
@@ -220,6 +240,7 @@ export class VpnManager {
client.updatedAt = Date.now();
await this.persistClient(client);
}
this.config.onClientChanged?.();
}
/**
@@ -234,6 +255,7 @@ export class VpnManager {
client.updatedAt = Date.now();
await this.persistClient(client);
}
this.config.onClientChanged?.();
}
/**
@@ -243,15 +265,6 @@ export class VpnManager {
if (!this.vpnServer) throw new Error('VPN server not running');
const bundle = await this.vpnServer.rotateClientKey(clientId);
// Update endpoint in WireGuard config
if (this.config.serverEndpoint && bundle.wireguardConfig) {
const wgPort = this.config.wgListenPort ?? 51820;
bundle.wireguardConfig = bundle.wireguardConfig.replace(
/Endpoint\s*=\s*.+/,
`Endpoint = ${this.config.serverEndpoint}:${wgPort}`,
);
}
// Update persisted entry with new public keys
const client = this.clients.get(clientId);
if (client) {
@@ -269,18 +282,23 @@ export class VpnManager {
*/
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
if (!this.vpnServer) throw new Error('VPN server not running');
let config = await this.vpnServer.exportClientConfig(clientId, format);
return this.vpnServer.exportClientConfig(clientId, format);
}
// Update endpoint in WireGuard config
if (format === 'wireguard' && this.config.serverEndpoint) {
const wgPort = this.config.wgListenPort ?? 51820;
config = config.replace(
/Endpoint\s*=\s*.+/,
`Endpoint = ${this.config.serverEndpoint}:${wgPort}`,
);
// ── Tag-based access control ───────────────────────────────────────────
/**
* Get assigned IPs for all enabled clients matching any of the given server-defined tags.
*/
public getClientIpsForServerDefinedTags(tags: string[]): string[] {
const ips: string[] = [];
for (const client of this.clients.values()) {
if (!client.enabled || !client.assignedIp) continue;
if (client.serverDefinedClientTags?.some(t => tags.includes(t))) {
ips.push(client.assignedIp);
}
}
return config;
return ips;
}
// ── Status and telemetry ───────────────────────────────────────────────
@@ -364,6 +382,12 @@ export class VpnManager {
for (const key of keys) {
const client = await this.storageManager.getJSON<IPersistedClient>(key);
if (client) {
// Migrate legacy `tags` → `serverDefinedClientTags`
if (!client.serverDefinedClientTags && client.tags) {
client.serverDefinedClientTags = client.tags;
delete client.tags;
await this.persistClient(client);
}
this.clients.set(client.clientId, client);
}
}

View File

@@ -58,6 +58,8 @@ export interface IRouteRemoteIngress {
export interface IRouteVpn {
/** Whether this route requires VPN access */
required: boolean;
/** Only allow VPN clients with these server-defined tags. Omitted = all VPN clients. */
allowedServerDefinedClientTags?: string[];
}
/**

View File

@@ -4,7 +4,7 @@
export interface IVpnClient {
clientId: string;
enabled: boolean;
tags?: string[];
serverDefinedClientTags?: string[];
description?: string;
assignedIp?: string;
createdAt: number;
@@ -17,7 +17,6 @@ export interface IVpnClient {
*/
export interface IVpnServerStatus {
running: boolean;
forwardingMode: 'tun' | 'socket';
subnet: string;
wgListenPort: number;
serverPublicKeys: {

View File

@@ -96,7 +96,15 @@ interface IIdentity {
| `IRemoteIngress` | Edge registration: id, name, secret, listenPorts, enabled, autoDerivePorts, tags |
| `IRemoteIngressStatus` | Runtime status: connected, publicIp, activeTunnels, lastHeartbeat |
| `IRouteRemoteIngress` | Route-level config: enabled flag and optional edgeFilter |
| `IDcRouterRouteConfig` | Extended SmartProxy route config with optional `remoteIngress` property |
| `IDcRouterRouteConfig` | Extended SmartProxy route config with optional `remoteIngress` and `vpn` properties |
| `IRouteVpn` | Route-level VPN config: `required` flag and optional `allowedServerDefinedClientTags` |
#### VPN Interfaces
| Interface | Description |
|-----------|-------------|
| `IVpnClient` | Client registration: clientId, enabled, serverDefinedClientTags, description, assignedIp, timestamps |
| `IVpnServerStatus` | Server status: running, subnet, wgListenPort, publicKeys, client counts |
| `IVpnClientTelemetry` | Per-client metrics: bytes sent/received, packets dropped, keepalives, rate limits |
### Request Interfaces (`requests`)
@@ -205,6 +213,19 @@ interface ICertificateInfo {
| `IReq_GetRemoteIngressStatus` | `getRemoteIngressStatus` | Runtime status of all edges |
| `IReq_GetRemoteIngressConnectionToken` | `getRemoteIngressConnectionToken` | Generate a connection token |
#### 🔐 VPN
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_GetVpnClients` | `getVpnClients` | List all registered VPN clients |
| `IReq_GetVpnStatus` | `getVpnStatus` | VPN server status |
| `IReq_CreateVpnClient` | `createVpnClient` | Create a new VPN client (returns WireGuard config) |
| `IReq_DeleteVpnClient` | `deleteVpnClient` | Remove a VPN client |
| `IReq_EnableVpnClient` | `enableVpnClient` | Enable a disabled client |
| `IReq_DisableVpnClient` | `disableVpnClient` | Disable a client |
| `IReq_RotateVpnClientKey` | `rotateVpnClientKey` | Generate new keys for a client |
| `IReq_ExportVpnClientConfig` | `exportVpnClientConfig` | Export WireGuard or SmartVPN config |
| `IReq_GetVpnClientTelemetry` | `getVpnClientTelemetry` | Per-client traffic metrics |
#### 📡 RADIUS
| Interface | Method | Description |
|-----------|--------|-------------|

View File

@@ -49,7 +49,7 @@ export interface IReq_CreateVpnClient extends plugins.typedrequestInterfaces.imp
request: {
identity: authInterfaces.IIdentity;
clientId: string;
tags?: string[];
serverDefinedClientTags?: string[];
description?: string;
};
response: {

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '11.13.0',
version: '11.19.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -974,7 +974,7 @@ export const fetchVpnAction = vpnStatePart.createAction(async (statePartArg): Pr
export const createVpnClientAction = vpnStatePart.createAction<{
clientId: string;
tags?: string[];
serverDefinedClientTags?: string[];
description?: string;
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
const context = getActionContext();
@@ -988,7 +988,7 @@ export const createVpnClientAction = vpnStatePart.createAction<{
const response = await request.fire({
identity: context.identity!,
clientId: dataArg.clientId,
tags: dataArg.tags,
serverDefinedClientTags: dataArg.serverDefinedClientTags,
description: dataArg.description,
});

View File

@@ -7,6 +7,7 @@ import {
state,
cssManager,
} from '@design.estate/dees-element';
import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { viewHostCss } from './shared/css.js';
@@ -181,13 +182,14 @@ export class OpsViewVpn extends DeesElement {
type: 'text',
value: status?.running ? 'Running' : 'Stopped',
icon: 'lucide:server',
description: status?.running ? `${status.forwardingMode} mode` : 'VPN server not running',
description: status?.running ? 'Active' : 'VPN server not running',
color: status?.running ? '#10b981' : '#ef4444',
},
];
return html`
<ops-sectionheading>VPN</ops-sectionheading>
<div class="vpnContainer">
${this.vpnState.newClientConfig ? html`
<div class="configDialog">
@@ -220,7 +222,7 @@ export class OpsViewVpn extends DeesElement {
</div>
` : ''}
<dees-statsgrid .statsTiles=${statsTiles}></dees-statsgrid>
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
${status ? html`
<div class="serverInfo">
@@ -232,10 +234,6 @@ export class OpsViewVpn extends DeesElement {
<span class="infoLabel">WireGuard Port</span>
<span class="infoValue">${status.wgListenPort}</span>
</div>
<div class="infoItem">
<span class="infoLabel">Forwarding Mode</span>
<span class="infoValue">${status.forwardingMode}</span>
</div>
${status.serverPublicKeys ? html`
<div class="infoItem">
<span class="infoLabel">WG Public Key</span>
@@ -255,38 +253,192 @@ export class OpsViewVpn extends DeesElement {
? html`<span class="statusBadge enabled">enabled</span>`
: html`<span class="statusBadge disabled">disabled</span>`,
'VPN IP': client.assignedIp || '-',
'Tags': client.tags?.length
? html`${client.tags.map(t => html`<span class="tagBadge">${t}</span>`)}`
'Tags': client.serverDefinedClientTags?.length
? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
: '-',
'Description': client.description || '-',
'Created': new Date(client.createdAt).toLocaleDateString(),
})}
.dataActions=${[
{
name: 'Create Client',
iconName: 'lucide:plus',
type: ['header'],
actionFunc: async () => {
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: 'Create VPN Client',
content: html`
<dees-form>
<dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{
name: 'Cancel',
iconName: 'lucide:x',
action: async (modalArg: any) => await modalArg.destroy(),
},
{
name: 'Create',
iconName: 'lucide:plus',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
if (!data.clientId) return;
const serverDefinedClientTags = data.tags
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
: undefined;
await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
clientId: data.clientId,
description: data.description || undefined,
serverDefinedClientTags,
});
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'Toggle',
iconName: 'lucide:power',
action: async (client: interfaces.data.IVpnClient) => {
type: ['contextmenu', 'inRow'],
actionFunc: async (actionData: any) => {
const client = actionData.item as interfaces.data.IVpnClient;
await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
clientId: client.clientId,
enabled: !client.enabled,
});
},
},
{
name: 'Export Config',
iconName: 'lucide:download',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionData: any) => {
const client = actionData.item as interfaces.data.IVpnClient;
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
const exportConfig = async (format: 'wireguard' | 'smartvpn') => {
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_ExportVpnClientConfig
>('/typedrequest', 'exportVpnClientConfig');
const response = await request.fire({
identity: appstate.loginStatePart.getState()!.identity!,
clientId: client.clientId,
format,
});
if (response.success && response.config) {
const ext = format === 'wireguard' ? 'conf' : 'json';
const blob = new Blob([response.config], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${client.clientId}.${ext}`;
a.click();
URL.revokeObjectURL(url);
DeesToast.createAndShow({ message: `${format} config downloaded`, type: 'success', duration: 3000 });
} else {
DeesToast.createAndShow({ message: response.message || 'Export failed', type: 'error', duration: 5000 });
}
} catch (err: any) {
DeesToast.createAndShow({ message: err.message || 'Export failed', type: 'error', duration: 5000 });
}
};
DeesModal.createAndShow({
heading: `Export Config: ${client.clientId}`,
content: html`<p>Choose a config format to download.</p>`,
menuOptions: [
{
name: 'WireGuard (.conf)',
iconName: 'lucide:shield',
action: async (modalArg: any) => {
await modalArg.destroy();
await exportConfig('wireguard');
},
},
{
name: 'SmartVPN (.json)',
iconName: 'lucide:braces',
action: async (modalArg: any) => {
await modalArg.destroy();
await exportConfig('smartvpn');
},
},
{
name: 'Cancel',
iconName: 'lucide:x',
action: async (modalArg: any) => await modalArg.destroy(),
},
],
});
},
},
{
name: 'Rotate Keys',
iconName: 'lucide:rotate-cw',
type: ['contextmenu'],
actionFunc: async (actionData: any) => {
const client = actionData.item as interfaces.data.IVpnClient;
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Rotate Client Keys',
content: html`<p>Generate new keys for "${client.clientId}"? The old keys will be invalidated and the client will need the new config to reconnect.</p>`,
menuOptions: [
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() },
{
name: 'Rotate',
iconName: 'lucide:rotate-cw',
action: async (modalArg: any) => {
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_RotateVpnClientKey
>('/typedrequest', 'rotateVpnClientKey');
const response = await request.fire({
identity: appstate.loginStatePart.getState()!.identity!,
clientId: client.clientId,
});
if (response.success && response.wireguardConfig) {
appstate.vpnStatePart.setState({
...appstate.vpnStatePart.getState()!,
newClientConfig: response.wireguardConfig,
});
}
await modalArg.destroy();
} catch (err: any) {
DeesToast.createAndShow({ message: err.message || 'Rotate failed', type: 'error', duration: 5000 });
}
},
},
],
});
},
},
{
name: 'Delete',
iconName: 'lucide:trash2',
action: async (client: interfaces.data.IVpnClient) => {
type: ['contextmenu'],
actionFunc: async (actionData: any) => {
const client = actionData.item as interfaces.data.IVpnClient;
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Delete VPN Client',
content: html`<p>Are you sure you want to delete client "${client.clientId}"?</p>`,
menuOptions: [
{ name: 'Cancel', action: async (modal: any) => modal.destroy() },
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() },
{
name: 'Delete',
action: async (modal: any) => {
iconName: 'lucide:trash2',
action: async (modalArg: any) => {
await appstate.vpnStatePart.dispatchAction(appstate.deleteVpnClientAction, client.clientId);
modal.destroy();
await modalArg.destroy();
},
},
],
@@ -294,37 +446,8 @@ export class OpsViewVpn extends DeesElement {
},
},
]}
.createNewItem=${async () => {
const { DeesModal, DeesForm, DeesInputText } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Create VPN Client',
content: html`
<dees-form>
<dees-input-text id="clientId" .label=${'Client ID'} .key=${'clientId'} required></dees-input-text>
<dees-input-text id="description" .label=${'Description'} .key=${'description'}></dees-input-text>
<dees-input-text id="tags" .label=${'Tags (comma-separated)'} .key=${'tags'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modal: any) => modal.destroy() },
{
name: 'Create',
action: async (modal: any) => {
const form = modal.shadowRoot!.querySelector('dees-form') as any;
const data = await form.collectFormData();
const tags = data.tags ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined;
await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
clientId: data.clientId,
description: data.description || undefined,
tags,
});
modal.destroy();
},
},
],
});
}}
></dees-table>
</div>
`;
}
}

View File

@@ -50,6 +50,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Connection token generation** — one-click "Copy Token" for easy edge provisioning
- Enable/disable, edit, secret regeneration, and delete actions
### 🔐 VPN Management
- VPN server status with forwarding mode, subnet, and WireGuard port
- Client registration table with create, enable/disable, and delete actions
- WireGuard config download and clipboard copy on client creation
- Per-client telemetry (bytes sent/received, keepalives)
- Server public key display for manual client configuration
### 📜 Log Viewer
- Real-time log streaming
- Filter by log level (error, warning, info, debug)
@@ -100,6 +107,7 @@ ts_web/
├── ops-view-emails.ts # Email queue management
├── ops-view-certificates.ts # Certificate overview & reprovisioning
├── ops-view-remoteingress.ts # Remote ingress edge management
├── ops-view-vpn.ts # VPN client management
├── ops-view-logs.ts # Log viewer
├── ops-view-routes.ts # Route & API token management
├── ops-view-config.ts # Configuration display
@@ -124,6 +132,7 @@ The app uses `@push.rocks/smartstate` v2.3+ with multiple state parts, scheduled
| `emailOpsStatePart` | Soft | Email queues, bounces, suppression list |
| `certificateStatePart` | Soft | Certificate list, summary, loading state |
| `remoteIngressStatePart` | Soft | Edge list, statuses, new edge secret |
| `vpnStatePart` | Soft | VPN clients, server status, new client config |
### Tab Visibility Optimization
@@ -173,6 +182,13 @@ regenerateRemoteIngressSecretAction(id) // New secret
toggleRemoteIngressAction(id, enabled) // Enable/disable
clearNewEdgeSecretAction() // Dismiss secret banner
fetchConnectionToken(edgeId) // Get connection token (standalone function)
// VPN
fetchVpnAction() // Clients + server status
createVpnClientAction(data) // Create new VPN client
deleteVpnClientAction(clientId) // Remove VPN client
toggleVpnClientAction(id, enabled) // Enable/disable
clearNewClientConfigAction() // Dismiss config banner
```
### Client-Side Routing
@@ -187,6 +203,7 @@ fetchConnectionToken(edgeId) // Get connection token (standalone function)
/emails/security → Security incidents
/certificates → Certificate management
/remoteingress → Remote ingress edge management
/vpn → VPN client management
/routes → Route & API token management
/logs → Log viewer
/configuration → System configuration

View File

@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress', 'vpn'] as const;
export type TValidView = typeof validViews[number];