Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 193a4bb180 | |||
| 0d9e6a4925 | |||
| ece9e46be9 | |||
| 918390a6a4 | |||
| 4ec0b67a71 | |||
| 356d6eca77 | |||
| 39c77accf8 | |||
| b8fba52cb3 | |||
| f247c77807 | |||
| e88938cf95 | |||
| 4f705a591e | |||
| 29687670e8 |
32
changelog.md
32
changelog.md
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-31 - 11.23.5 - fix(config)
|
||||
correct VPN mandatory flag default handling in route config manager
|
||||
|
||||
- Changes the VPN mandatory check so it only applies when explicitly set to true, matching the updated default behavior of false.
|
||||
- Prevents routes from being treated as VPN-mandatory when the setting is omitted.
|
||||
|
||||
## 2026-03-31 - 11.23.4 - fix(deps)
|
||||
bump @push.rocks/smartvpn to 1.17.1
|
||||
|
||||
- Updates the @push.rocks/smartvpn dependency from 1.16.5 to 1.17.1.
|
||||
|
||||
## 2026-03-31 - 11.23.3 - fix(ts_web)
|
||||
update appstate to import interfaces from source TypeScript module path
|
||||
|
||||
- Replaces the appstate interfaces import from ../dist_ts_interfaces/index.js with ../ts_interfaces/index.js.
|
||||
- Aligns the web app state module with the source interface location instead of the built distribution path.
|
||||
|
||||
## 2026-03-31 - 11.23.2 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-31 - 11.23.1 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-31 - 11.23.0 - feat(vpn)
|
||||
support optional non-mandatory VPN route access and align route config with enabled semantics
|
||||
|
||||
- rename route VPN configuration from `required` to `enabled` across code, docs, and examples
|
||||
- add `vpn.mandatory` to control whether VPN allowlists replace or extend existing `security.ipAllowList` rules
|
||||
- improve VPN client status matching in the ops view by falling back to assigned IP when client IDs differ
|
||||
|
||||
## 2026-03-31 - 11.22.0 - feat(vpn)
|
||||
add VPN client editing and connected client visibility in ops server
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "11.22.0",
|
||||
"version": "11.23.5",
|
||||
"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.16.5",
|
||||
"@push.rocks/smartvpn": "1.17.1",
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.9.0",
|
||||
"@serve.zone/interfaces": "^5.3.0",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -96,8 +96,8 @@ importers:
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9
|
||||
'@push.rocks/smartvpn':
|
||||
specifier: 1.16.5
|
||||
version: 1.16.5
|
||||
specifier: 1.17.1
|
||||
version: 1.17.1
|
||||
'@push.rocks/taskbuffer':
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2
|
||||
@@ -1339,8 +1339,8 @@ packages:
|
||||
'@push.rocks/smartversion@3.0.5':
|
||||
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
|
||||
|
||||
'@push.rocks/smartvpn@1.16.5':
|
||||
resolution: {integrity: sha512-wUau/Ad18p36AeIF5R33S45WEM78R7Y4SZSkWdxMdvKNIqSfn1mhf4Zd57iAtxvq+2iO246xfifBrATZWfjPeQ==}
|
||||
'@push.rocks/smartvpn@1.17.1':
|
||||
resolution: {integrity: sha512-oTOxNUrh+doL9AocgPnMbcYZKrWJhCeuqNotu1RfiteIV9DDdznvA+cl3nOgxD/ImUYrFPz6PUp5BEMogWcS8Q==}
|
||||
|
||||
'@push.rocks/smartwatch@6.4.0':
|
||||
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
||||
@@ -6622,7 +6622,7 @@ snapshots:
|
||||
'@types/semver': 7.7.1
|
||||
semver: 7.7.4
|
||||
|
||||
'@push.rocks/smartvpn@1.16.5':
|
||||
'@push.rocks/smartvpn@1.17.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartnftables': 1.1.0
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
|
||||
12
readme.md
12
readme.md
@@ -76,7 +76,7 @@ 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
|
||||
- **Route-level VPN gating** — mark any route with `vpn: { enabled: true }` to restrict access to VPN clients only, or `vpn: { enabled: true, mandatory: false }` to add VPN clients alongside existing access rules
|
||||
- **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
|
||||
@@ -1030,8 +1030,8 @@ 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. **Smart split tunnel** — generated WireGuard configs auto-include the VPN subnet plus DNS-resolved IPs of VPN-gated domains. Domains from routes with `vpn.required` are resolved at config generation time, so clients route only the necessary traffic through the tunnel
|
||||
4. Routes with `vpn: { required: true }` get `security.ipAllowList` dynamically injected (re-computed on every client change)
|
||||
3. **Smart split tunnel** — generated WireGuard configs auto-include the VPN subnet plus DNS-resolved IPs of VPN-gated domains. Domains from routes with `vpn.enabled` are resolved at config generation time, so clients route only the necessary traffic through the tunnel
|
||||
4. Routes with `vpn: { enabled: true }` get `security.ipAllowList` dynamically injected (re-computed on every client change). With `mandatory: true` (default), the allowlist is replaced; with `mandatory: false`, VPN IPs are appended to existing rules
|
||||
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
|
||||
@@ -1091,7 +1091,7 @@ const router = new DcRouter({
|
||||
targets: [{ host: '192.168.1.50', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
},
|
||||
vpn: { required: true },
|
||||
vpn: { enabled: true },
|
||||
},
|
||||
// 🔐 VPN + tag-restricted: only 'engineering' tagged clients
|
||||
{
|
||||
@@ -1102,10 +1102,10 @@ const router = new DcRouter({
|
||||
targets: [{ host: '192.168.1.51', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
},
|
||||
vpn: { required: true, allowedServerDefinedClientTags: ['engineering'] },
|
||||
vpn: { enabled: true, allowedServerDefinedClientTags: ['engineering'] },
|
||||
// → alice + bob can access, carol cannot
|
||||
},
|
||||
// 🌐 Public: no VPN required
|
||||
// 🌐 Public: no VPN
|
||||
{
|
||||
name: 'public-site',
|
||||
match: { domains: ['example.com'], ports: [443] },
|
||||
|
||||
@@ -29,13 +29,13 @@ const devRouter = new DcRouter({
|
||||
name: 'vpn-internal-app',
|
||||
match: { ports: [18080], domains: ['internal.example.com'] },
|
||||
action: { type: 'forward', targets: [{ host: 'localhost', port: 5000 }] },
|
||||
vpn: { required: true },
|
||||
vpn: { enabled: true },
|
||||
},
|
||||
{
|
||||
name: 'vpn-eng-dashboard',
|
||||
match: { ports: [18080], domains: ['eng.example.com'] },
|
||||
action: { type: 'forward', targets: [{ host: 'localhost', port: 5001 }] },
|
||||
vpn: { required: true, allowedServerDefinedClientTags: ['engineering'] },
|
||||
vpn: { enabled: true, allowedServerDefinedClientTags: ['engineering'] },
|
||||
},
|
||||
] as any[],
|
||||
},
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '11.22.0',
|
||||
version: '11.23.5',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ export interface IDcRouterOptions {
|
||||
|
||||
/**
|
||||
* VPN server configuration.
|
||||
* Enables VPN-based access control: routes with vpn.required are only
|
||||
* Enables VPN-based access control: routes with vpn.enabled are only
|
||||
* accessible from VPN clients. Supports WireGuard + native (WS/QUIC) transports.
|
||||
*/
|
||||
vpnConfig?: {
|
||||
@@ -2110,7 +2110,7 @@ export class DcRouter {
|
||||
const domainsToResolve = new Set<string>();
|
||||
for (const route of routes) {
|
||||
const dcRoute = route as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig;
|
||||
if (!dcRoute.vpn?.required) continue;
|
||||
if (!dcRoute.vpn?.enabled) continue;
|
||||
|
||||
const routeTags = dcRoute.vpn.allowedServerDefinedClientTags;
|
||||
if (!routeTags?.length || clientTags.some(t => routeTags.includes(t))) {
|
||||
|
||||
@@ -255,17 +255,20 @@ export class RouteConfigManager {
|
||||
const http3Config = this.getHttp3Config?.();
|
||||
const vpnAllowList = this.getVpnAllowList;
|
||||
|
||||
// Helper: inject VPN security into a route if vpn.required is set
|
||||
// Helper: inject VPN security into a route if vpn.enabled is set
|
||||
const injectVpn = (route: plugins.smartproxy.IRouteConfig): plugins.smartproxy.IRouteConfig => {
|
||||
if (!vpnAllowList) return route;
|
||||
const dcRoute = route as IDcRouterRouteConfig;
|
||||
if (!dcRoute.vpn?.required) return route;
|
||||
if (!dcRoute.vpn?.enabled) return route;
|
||||
const allowList = vpnAllowList(dcRoute.vpn.allowedServerDefinedClientTags);
|
||||
const mandatory = dcRoute.vpn.mandatory === true; // defaults to false
|
||||
return {
|
||||
...route,
|
||||
security: {
|
||||
...route.security,
|
||||
ipAllowList: [...(route.security?.ipAllowList || []), ...allowList],
|
||||
ipAllowList: mandatory
|
||||
? allowList
|
||||
: [...(route.security?.ipAllowList || []), ...allowList],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,11 +53,14 @@ export interface IRouteRemoteIngress {
|
||||
|
||||
/**
|
||||
* Route-level VPN access configuration.
|
||||
* When attached to a route, restricts access to VPN clients only.
|
||||
* When attached to a route, controls VPN client access.
|
||||
*/
|
||||
export interface IRouteVpn {
|
||||
/** Whether this route requires VPN access */
|
||||
required: boolean;
|
||||
/** Enable VPN client access for this route */
|
||||
enabled: boolean;
|
||||
/** When true (default), ONLY VPN clients can access this route (replaces ipAllowList).
|
||||
* When false, VPN client IPs are added alongside the existing allowlist. */
|
||||
mandatory?: boolean;
|
||||
/** Only allow VPN clients with these server-defined tags. Omitted = all VPN clients. */
|
||||
allowedServerDefinedClientTags?: string[];
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ 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 and optional `allowedServerDefinedClientTags` |
|
||||
| `IRouteVpn` | Route-level VPN config: `enabled`/`mandatory` flags and optional `allowedServerDefinedClientTags` |
|
||||
|
||||
#### VPN Interfaces
|
||||
| Interface | Description |
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '11.22.0',
|
||||
version: '11.23.5',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
|
||||
// Create main app state instance
|
||||
export const appState = new plugins.domtools.plugins.smartstate.Smartstate();
|
||||
|
||||
@@ -141,9 +141,11 @@ export class OpsViewVpn extends DeesElement {
|
||||
`,
|
||||
];
|
||||
|
||||
/** Look up connected client info by clientId */
|
||||
private getConnectedInfo(clientId: string): interfaces.data.IVpnConnectedClient | undefined {
|
||||
return this.vpnState.connectedClients?.find(c => c.clientId === clientId);
|
||||
/** Look up connected client info by clientId or assignedIp */
|
||||
private getConnectedInfo(client: interfaces.data.IVpnClient): interfaces.data.IVpnConnectedClient | undefined {
|
||||
return this.vpnState.connectedClients?.find(
|
||||
c => c.clientId === client.clientId || (client.assignedIp && c.assignedIp === client.assignedIp)
|
||||
);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
@@ -277,7 +279,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
.heading2=${'Manage WireGuard and SmartVPN client registrations'}
|
||||
.data=${clients}
|
||||
.displayFunction=${(client: interfaces.data.IVpnClient) => {
|
||||
const conn = this.getConnectedInfo(client.clientId);
|
||||
const conn = this.getConnectedInfo(client);
|
||||
let statusHtml;
|
||||
if (!client.enabled) {
|
||||
statusHtml = html`<span class="statusBadge disabled">disabled</span>`;
|
||||
@@ -349,7 +351,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
type: ['doubleClick'],
|
||||
actionFunc: async (actionData: any) => {
|
||||
const client = actionData.item as interfaces.data.IVpnClient;
|
||||
const conn = this.getConnectedInfo(client.clientId);
|
||||
const conn = this.getConnectedInfo(client);
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
|
||||
// Fetch telemetry on-demand
|
||||
|
||||
Reference in New Issue
Block a user