Compare commits

...

10 Commits

Author SHA1 Message Date
6efd986406 v11.20.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 23:50:51 +00:00
7370d7f0e7 feat(vpn-ui): add QR code export for WireGuard client configurations 2026-03-30 23:50:51 +00:00
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
11 changed files with 523 additions and 107 deletions

View File

@@ -1,5 +1,41 @@
# Changelog
## 2026-03-30 - 11.20.0 - feat(vpn-ui)
add QR code export for WireGuard client configurations
- adds a QR code action for newly created WireGuard configs in the VPN operations view
- adds a QR code export option for existing VPN clients alongside file downloads
- introduces qrcode and @types/qrcode dependencies and exposes the plugin for web UI use
## 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

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "11.16.0",
"version": "11.20.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -59,13 +59,15 @@
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.3.0",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartvpn": "1.14.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",
"@serve.zone/remoteingress": "^4.15.3",
"@tsclass/tsclass": "^9.5.0",
"@types/qrcode": "^1.5.6",
"lru-cache": "^11.2.7",
"qrcode": "^1.5.4",
"uuid": "^13.0.0"
},
"keywords": [

122
pnpm-lock.yaml generated
View File

@@ -96,8 +96,8 @@ importers:
specifier: ^3.0.9
version: 3.0.9
'@push.rocks/smartvpn':
specifier: 1.14.0
version: 1.14.0
specifier: 1.16.1
version: 1.16.1
'@push.rocks/taskbuffer':
specifier: ^8.0.2
version: 8.0.2
@@ -113,9 +113,15 @@ importers:
'@tsclass/tsclass':
specifier: ^9.5.0
version: 9.5.0
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
lru-cache:
specifier: ^11.2.7
version: 11.2.7
qrcode:
specifier: ^1.5.4
version: 1.5.4
uuid:
specifier: ^13.0.0
version: 13.0.0
@@ -1246,6 +1252,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 +1339,8 @@ packages:
'@push.rocks/smartversion@3.0.5':
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
'@push.rocks/smartvpn@1.14.0':
resolution: {integrity: sha512-zJmHiuLwY4OEN4jBVrJf1hAXpfO9f6Bulq/v1DrB16nR7VgE82KNqRLt0Wi/9PCsAUfmVJTvOf4yirnjBrEWQg==}
'@push.rocks/smartvpn@1.16.1':
resolution: {integrity: sha512-LQzt3ajMKIs3anYki/3drt7XcCuekoKvApCltLEjsoGEEX5JkXGSZFB+UFvqEhG8NcEuHw574rU3tB2orHzKTQ==}
'@push.rocks/smartwatch@6.4.0':
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
@@ -2044,6 +2053,9 @@ packages:
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
'@types/randomatic@3.1.5':
resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==}
@@ -2298,6 +2310,10 @@ packages:
camel-case@3.0.0:
resolution: {integrity: sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
@@ -2338,6 +2354,9 @@ packages:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -2414,6 +2433,10 @@ packages:
supports-color:
optional: true
decamelize@1.2.0:
resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=}
engines: {node: '>=0.10.0'}
decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
@@ -2467,6 +2490,9 @@ packages:
devtools-protocol@0.0.1581282:
resolution: {integrity: sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@@ -3586,6 +3612,10 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
pngjs@6.0.0:
resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==}
engines: {node: '>=12.13.0'}
@@ -3707,6 +3737,11 @@ packages:
resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==}
engines: {node: '>=16.0.0'}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
qs@6.15.0:
resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
engines: {node: '>=0.6'}
@@ -3777,6 +3812,9 @@ packages:
resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@@ -3832,6 +3870,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -4164,6 +4205,9 @@ packages:
whatwg-url@5.0.0:
resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -4219,6 +4263,9 @@ packages:
xterm@5.3.0:
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -4228,6 +4275,10 @@ packages:
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@@ -4236,6 +4287,10 @@ packages:
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
@@ -6331,6 +6386,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 +6622,9 @@ snapshots:
'@types/semver': 7.7.1
semver: 7.7.4
'@push.rocks/smartvpn@1.14.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
@@ -7435,6 +7496,10 @@ snapshots:
dependencies:
undici-types: 7.18.2
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 25.5.0
'@types/randomatic@3.1.5': {}
'@types/relateurl@0.2.33': {}
@@ -7679,6 +7744,8 @@ snapshots:
no-case: 2.3.2
upper-case: 1.1.3
camelcase@5.3.1: {}
camelcase@6.3.0: {}
ccount@2.0.1: {}
@@ -7709,6 +7776,12 @@ snapshots:
cli-width@4.1.0: {}
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@@ -7783,6 +7856,8 @@ snapshots:
dependencies:
ms: 2.1.3
decamelize@1.2.0: {}
decode-named-character-reference@1.3.0:
dependencies:
character-entities: 2.0.2
@@ -7829,6 +7904,8 @@ snapshots:
devtools-protocol@0.0.1581282: {}
dijkstrajs@1.0.3: {}
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
@@ -9207,6 +9284,8 @@ snapshots:
dependencies:
find-up: 4.1.0
pngjs@5.0.0: {}
pngjs@6.0.0: {}
pngjs@7.0.0: {}
@@ -9392,6 +9471,12 @@ snapshots:
pvutils@1.1.5: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
qs@6.15.0:
dependencies:
side-channel: 1.1.0
@@ -9490,6 +9575,8 @@ snapshots:
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
resolve-alpn@1.2.1: {}
resolve-from@4.0.0: {}
@@ -9547,6 +9634,8 @@ snapshots:
semver@7.7.4: {}
set-blocking@2.0.0: {}
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -9938,6 +10027,8 @@ snapshots:
tr46: 0.0.3
webidl-conversions: 3.0.1
which-module@2.0.1: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
@@ -9979,14 +10070,35 @@ snapshots:
xterm@5.3.0: {}
y18n@4.0.3: {}
y18n@5.0.8: {}
yaml@2.8.3: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs-parser@21.1.1: {}
yargs-parser@22.0.0: {}
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yargs@17.7.2:
dependencies:
cliui: 8.0.1

140
readme.md
View File

@@ -77,10 +77,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### 🔐 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
- **Rootless operation** — auto-detects privileges: kernel TUN when running as root, userspace NAT (smoltcp) when not
- **Client management** — create, enable, disable, rotate keys, export WireGuard `.conf` files via OpsServer API
- **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** — in socket mode, the NAT engine sends PP v2 on outbound connections to preserve VPN client identity
- **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
@@ -261,7 +264,9 @@ const router = new DcRouter({
vpnConfig: {
enabled: true,
serverEndpoint: 'vpn.example.com',
wgListenPort: 51820,
clients: [
{ clientId: 'dev-laptop', serverDefinedClientTags: ['engineering'] },
],
},
// Persistent storage
@@ -367,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
@@ -381,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
@@ -456,7 +462,17 @@ interface IDcRouterOptions {
wgListenPort?: number; // default: 51820
dns?: string[]; // DNS servers pushed to VPN clients
serverEndpoint?: string; // Hostname in generated client configs
forwardingMode?: 'tun' | 'socket'; // default: auto-detect (root → tun, else socket)
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) ────────────────────────────────────────────
@@ -1014,17 +1030,34 @@ DcRouter integrates [`@push.rocks/smartvpn`](https://code.foss.global/push.rocks
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. Routes with `vpn: { required: true }` get `security.ipAllowList` automatically injected with the VPN subnet
4. SmartProxy enforces the allowlist — only VPN-sourced traffic is accepted on those routes
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
### Two Operating Modes
### Destination Policy
| Mode | Root Required? | How It Works |
|------|---------------|-------------|
| **TUN** (`forwardingMode: 'tun'`) | Yes | Kernel TUN device — VPN traffic enters the network stack with real VPN IPs |
| **Socket** (`forwardingMode: 'socket'`) | No | Userspace NAT via smoltcp — outbound connections send PROXY protocol v2 to preserve VPN client IPs |
By default, VPN client traffic is redirected to localhost (SmartProxy) via `forceTarget`. You can customize this with a destination policy:
DcRouter auto-detects: if running as root, it uses TUN mode; otherwise, it falls back to socket mode. You can override this with the `forwardingMode` option.
```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
@@ -1032,26 +1065,47 @@ DcRouter auto-detects: if running as root, it uses TUN mode; otherwise, it falls
const router = new DcRouter({
vpnConfig: {
enabled: true,
subnet: '10.8.0.0/24', // VPN client IP pool (default)
wgListenPort: 51820, // WireGuard UDP port (default)
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
// forwardingMode: 'socket', // Override auto-detection
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: [
// This route is VPN-only — non-VPN clients are blocked
// 🔐 VPN-only: any VPN client can access
{
name: 'admin-panel',
match: { domains: ['admin.example.com'], ports: [443] },
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 }, // 🔐 Only VPN clients can access this
vpn: { required: true },
},
// This route is public — anyone can access it
// 🔐 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] },
@@ -1066,17 +1120,29 @@ const router = new DcRouter({
});
```
### Client Management via OpsServer API
### Client Tags
Once the VPN server is running, you can manage clients through the OpsServer dashboard or API:
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** — re-export in WireGuard or SmartVPN format
- **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 or QR code — no custom VPN software needed.
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
@@ -1252,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 |
@@ -1318,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
@@ -1435,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 |
@@ -1575,7 +1657,7 @@ The Docker build supports multi-platform (`linux/amd64`, `linux/arm64`) via [tsd
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

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.16.0',
version: '11.20.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -127,6 +127,10 @@ export class VpnManager {
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);
@@ -184,15 +188,6 @@ export class VpnManager {
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,
@@ -270,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) {
@@ -296,18 +282,7 @@ 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);
// 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}`,
);
}
return config;
return this.vpnServer.exportClientConfig(clientId, format);
}
// ── Tag-based access control ───────────────────────────────────────────

View File

@@ -97,13 +97,13 @@ interface IIdentity {
| `IRemoteIngressStatus` | Runtime status: connected, publicIp, activeTunnels, lastHeartbeat |
| `IRouteRemoteIngress` | Route-level config: enabled flag and optional edgeFilter |
| `IDcRouterRouteConfig` | Extended SmartProxy route config with optional `remoteIngress` and `vpn` properties |
| `IRouteVpn` | Route-level VPN config: `required` flag to restrict access to VPN clients |
| `IRouteVpn` | Route-level VPN config: `required` flag and optional `allowedServerDefinedClientTags` |
#### VPN Interfaces
| Interface | Description |
|-----------|-------------|
| `IVpnClient` | Client registration: clientId, enabled, tags, description, assignedIp, timestamps |
| `IVpnServerStatus` | Server status: running, forwardingMode, subnet, wgListenPort, publicKeys, client counts |
| `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`)

View File

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

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';
@@ -188,6 +189,7 @@ export class OpsViewVpn extends DeesElement {
return html`
<ops-sectionheading>VPN</ops-sectionheading>
<div class="vpnContainer">
${this.vpnState.newClientConfig ? html`
<div class="configDialog">
@@ -214,13 +216,36 @@ export class OpsViewVpn extends DeesElement {
URL.revokeObjectURL(url);
}}
>Download .conf</dees-button>
<dees-button
@click=${async () => {
const dataUrl = await plugins.qrcode.toDataURL(
this.vpnState.newClientConfig!,
{ width: 400, margin: 2 }
);
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'WireGuard QR Code',
content: html`
<div style="text-align: center; padding: 16px;">
<img src="${dataUrl}" style="max-width: 100%; image-rendering: pixelated;" />
<p style="margin-top: 12px; font-size: 13px; color: #9ca3af;">
Scan with the WireGuard app on your phone
</p>
</div>
`,
menuOptions: [
{ name: 'Close', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() },
],
});
}}
>Show QR Code</dees-button>
<dees-button
@click=${() => appstate.vpnStatePart.dispatchAction(appstate.clearNewClientConfigAction, null)}
>Dismiss</dees-button>
</div>
` : ''}
<dees-statsgrid .statsTiles=${statsTiles}></dees-statsgrid>
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
${status ? html`
<div class="serverInfo">
@@ -258,31 +283,230 @@ export class OpsViewVpn extends DeesElement {
'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 });
}
};
const showQrCode = async () => {
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: 'wireguard',
});
if (response.success && response.config) {
const dataUrl = await plugins.qrcode.toDataURL(
response.config,
{ width: 400, margin: 2 }
);
DeesModal.createAndShow({
heading: `QR Code: ${client.clientId}`,
content: html`
<div style="text-align: center; padding: 16px;">
<img src="${dataUrl}" style="max-width: 100%; image-rendering: pixelated;" />
<p style="margin-top: 12px; font-size: 13px; color: #9ca3af;">
Scan with the WireGuard app on your phone
</p>
</div>
`,
menuOptions: [
{ name: 'Close', iconName: 'lucide:x', action: async (m: any) => await m.destroy() },
],
});
} else {
DeesToast.createAndShow({ message: response.message || 'Export failed', type: 'error', duration: 5000 });
}
} catch (err: any) {
DeesToast.createAndShow({ message: err.message || 'QR generation 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: 'QR Code (WireGuard)',
iconName: 'lucide:qr-code',
action: async (modalArg: any) => {
await modalArg.destroy();
await showQrCode();
},
},
{
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();
},
},
],
@@ -290,37 +514,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 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,
});
modal.destroy();
},
},
],
});
}}
></dees-table>
</div>
`;
}
}

View File

@@ -8,11 +8,15 @@ import * as szCatalog from '@serve.zone/catalog';
// TypedSocket for real-time push communication
import * as typedsocket from '@api.global/typedsocket';
// QR code generation for WireGuard configs
import * as qrcode from 'qrcode';
export {
deesElement,
deesCatalog,
szCatalog,
typedsocket,
qrcode,
}
// domtools gives us TypedRequest and other utilities