@serve.zone/dcrouter 13.20.0 → 13.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist_serve/bundle.js +519 -519
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +5 -0
  4. package/dist_ts/classes.dcrouter.js +34 -10
  5. package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
  6. package/dist_ts/config/classes.route-config-manager.js +4 -1
  7. package/dist_ts/monitoring/classes.metricsmanager.d.ts +6 -2
  8. package/dist_ts/monitoring/classes.metricsmanager.js +67 -41
  9. package/dist_ts/opsserver/handlers/security.handler.js +11 -5
  10. package/dist_ts/opsserver/handlers/stats.handler.js +2 -1
  11. package/dist_ts/vpn/classes.vpn-manager.d.ts +5 -1
  12. package/dist_ts/vpn/classes.vpn-manager.js +55 -17
  13. package/dist_ts_interfaces/data/stats.d.ts +10 -0
  14. package/dist_ts_web/00_commitinfo_data.js +1 -1
  15. package/dist_ts_web/appstate.js +23 -16
  16. package/dist_ts_web/elements/network/ops-view-network-activity.js +7 -3
  17. package/dist_ts_web/elements/network/ops-view-vpn.d.ts +3 -0
  18. package/dist_ts_web/elements/network/ops-view-vpn.js +44 -16
  19. package/package.json +3 -3
  20. package/readme.md +123 -155
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/classes.dcrouter.ts +51 -14
  23. package/ts/config/classes.route-config-manager.ts +6 -0
  24. package/ts/monitoring/classes.metricsmanager.ts +71 -40
  25. package/ts/opsserver/handlers/security.handler.ts +11 -5
  26. package/ts/opsserver/handlers/stats.handler.ts +1 -0
  27. package/ts/readme.md +46 -103
  28. package/ts/vpn/classes.vpn-manager.ts +66 -15
  29. package/ts_apiclient/readme.md +57 -59
  30. package/ts_web/00_commitinfo_data.ts +1 -1
  31. package/ts_web/appstate.ts +23 -18
  32. package/ts_web/elements/network/ops-view-network-activity.ts +6 -2
  33. package/ts_web/elements/network/ops-view-vpn.ts +50 -14
  34. package/ts_web/readme.md +27 -47
@@ -560,7 +560,9 @@ export class MetricsManager {
560
560
  requestsPerSecond: 0,
561
561
  requestsTotal: 0,
562
562
  backends: [] as Array<any>,
563
- domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number; requestCount: number }>,
563
+ domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number; requestCount: number; requestsPerSecond?: number; requestsLastMinute?: number }>,
564
+ frontendProtocols: null,
565
+ backendProtocols: null,
564
566
  };
565
567
  }
566
568
 
@@ -592,6 +594,7 @@ export class MetricsManager {
592
594
  // Get HTTP request rates
593
595
  const requestsPerSecond = proxyMetrics.requests.perSecond();
594
596
  const requestsTotal = proxyMetrics.requests.total();
597
+ const domainRequestRates = proxyMetrics.requests.byDomain();
595
598
 
596
599
  // Get frontend/backend protocol distribution
597
600
  const frontendProtocols = proxyMetrics.connections.frontendProtocols() ?? null;
@@ -619,47 +622,48 @@ export class MetricsManager {
619
622
  const seenCacheKeys = new Set<string>();
620
623
 
621
624
  for (const [key, bm] of backendMetrics) {
625
+ backends.push({
626
+ id: `backend:${key}`,
627
+ backend: key,
628
+ domain: null,
629
+ protocol: bm.protocol,
630
+ activeConnections: bm.activeConnections,
631
+ totalConnections: bm.totalConnections,
632
+ connectErrors: bm.connectErrors,
633
+ handshakeErrors: bm.handshakeErrors,
634
+ requestErrors: bm.requestErrors,
635
+ avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
636
+ poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
637
+ h2Failures: bm.h2Failures,
638
+ h2Suppressed: false,
639
+ h3Suppressed: false,
640
+ h2CooldownRemainingSecs: null,
641
+ h3CooldownRemainingSecs: null,
642
+ h2ConsecutiveFailures: null,
643
+ h3ConsecutiveFailures: null,
644
+ h3Port: null,
645
+ cacheAgeSecs: null,
646
+ });
647
+
622
648
  const cacheEntries = cacheByBackend.get(key);
623
- if (!cacheEntries || cacheEntries.length === 0) {
624
- // No protocol cache entry emit one row with backend metrics only
625
- backends.push({
626
- backend: key,
627
- domain: null,
628
- protocol: bm.protocol,
629
- activeConnections: bm.activeConnections,
630
- totalConnections: bm.totalConnections,
631
- connectErrors: bm.connectErrors,
632
- handshakeErrors: bm.handshakeErrors,
633
- requestErrors: bm.requestErrors,
634
- avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
635
- poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
636
- h2Failures: bm.h2Failures,
637
- h2Suppressed: false,
638
- h3Suppressed: false,
639
- h2CooldownRemainingSecs: null,
640
- h3CooldownRemainingSecs: null,
641
- h2ConsecutiveFailures: null,
642
- h3ConsecutiveFailures: null,
643
- h3Port: null,
644
- cacheAgeSecs: null,
645
- });
646
- } else {
647
- // One row per domain, each enriched with the shared backend metrics
649
+ if (cacheEntries && cacheEntries.length > 0) {
650
+ // Protocol cache rows are domain-scoped metadata, not live backend connections.
648
651
  for (const cache of cacheEntries) {
649
652
  const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
650
653
  seenCacheKeys.add(compositeKey);
651
654
  backends.push({
655
+ id: `cache:${compositeKey}`,
652
656
  backend: key,
653
657
  domain: cache.domain ?? null,
654
658
  protocol: cache.protocol ?? bm.protocol,
655
- activeConnections: bm.activeConnections,
656
- totalConnections: bm.totalConnections,
657
- connectErrors: bm.connectErrors,
658
- handshakeErrors: bm.handshakeErrors,
659
- requestErrors: bm.requestErrors,
660
- avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
661
- poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
662
- h2Failures: bm.h2Failures,
659
+ activeConnections: 0,
660
+ totalConnections: 0,
661
+ connectErrors: 0,
662
+ handshakeErrors: 0,
663
+ requestErrors: 0,
664
+ avgConnectTimeMs: 0,
665
+ poolHitRate: 0,
666
+ h2Failures: 0,
663
667
  h2Suppressed: cache.h2Suppressed,
664
668
  h3Suppressed: cache.h3Suppressed,
665
669
  h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
@@ -678,6 +682,7 @@ export class MetricsManager {
678
682
  const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
679
683
  if (!seenCacheKeys.has(compositeKey)) {
680
684
  backends.push({
685
+ id: `cache:${compositeKey}`,
681
686
  backend: `${entry.host}:${entry.port}`,
682
687
  domain: entry.domain,
683
688
  protocol: entry.protocol,
@@ -750,6 +755,9 @@ export class MetricsManager {
750
755
 
751
756
  // Resolve wildcards using domains seen in request metrics
752
757
  const allKnownDomains = new Set<string>(domainRequestTotals.keys());
758
+ for (const domain of domainRequestRates.keys()) {
759
+ allKnownDomains.add(domain);
760
+ }
753
761
  for (const entry of protocolCache) {
754
762
  if (entry.domain) allKnownDomains.add(entry.domain);
755
763
  }
@@ -775,11 +783,20 @@ export class MetricsManager {
775
783
  }
776
784
  }
777
785
 
778
- // For each route, compute the total request count across all its resolved domains
779
- // so we can distribute throughput/connections proportionally
786
+ const hasLiveDomainRates = domainRequestRates.size > 0;
787
+ const getDomainWeight = (domain: string): number => {
788
+ const liveRate = domainRequestRates.get(domain);
789
+ return hasLiveDomainRates
790
+ ? (liveRate?.lastMinute ?? 0)
791
+ : (domainRequestTotals.get(domain) || 0);
792
+ };
793
+
794
+ // For each route, compute the total activity weight across all resolved domains
795
+ // so we can distribute route-level throughput/connections. Prefer live domain
796
+ // request rates from SmartProxy 27.8+, falling back to lifetime counters.
780
797
  const routeTotalRequests = new Map<string, number>();
781
798
  for (const [domain, routeKeys] of domainToRoutes) {
782
- const reqs = domainRequestTotals.get(domain) || 0;
799
+ const reqs = getDomainWeight(domain);
783
800
  for (const routeKey of routeKeys) {
784
801
  routeTotalRequests.set(routeKey, (routeTotalRequests.get(routeKey) || 0) + reqs);
785
802
  }
@@ -792,10 +809,13 @@ export class MetricsManager {
792
809
  bytesOutPerSec: number;
793
810
  routeCount: number;
794
811
  requestCount: number;
812
+ requestsPerSecond: number;
813
+ requestsLastMinute: number;
795
814
  }>();
796
815
 
797
816
  for (const [domain, routeKeys] of domainToRoutes) {
798
- const domainReqs = domainRequestTotals.get(domain) || 0;
817
+ const domainReqs = getDomainWeight(domain);
818
+ const requestRate = domainRequestRates.get(domain);
799
819
  let totalConns = 0;
800
820
  let totalIn = 0;
801
821
  let totalOut = 0;
@@ -816,7 +836,9 @@ export class MetricsManager {
816
836
  bytesInPerSec: totalIn,
817
837
  bytesOutPerSec: totalOut,
818
838
  routeCount: routeKeys.length,
819
- requestCount: domainReqs,
839
+ requestCount: domainRequestTotals.get(domain) || 0,
840
+ requestsPerSecond: requestRate?.perSecond ?? 0,
841
+ requestsLastMinute: requestRate?.lastMinute ?? 0,
820
842
  });
821
843
  }
822
844
 
@@ -828,8 +850,17 @@ export class MetricsManager {
828
850
  activeConnections: data.activeConnections,
829
851
  routeCount: data.routeCount,
830
852
  requestCount: data.requestCount,
853
+ requestsPerSecond: data.requestsPerSecond,
854
+ requestsLastMinute: data.requestsLastMinute,
831
855
  }))
832
- .sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
856
+ .sort((a, b) => {
857
+ if (hasLiveDomainRates) {
858
+ return (b.requestsPerSecond - a.requestsPerSecond) ||
859
+ (b.requestsLastMinute - a.requestsLastMinute) ||
860
+ ((b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
861
+ }
862
+ return (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond);
863
+ });
833
864
 
834
865
  return {
835
866
  connectionsByIP,
@@ -50,19 +50,21 @@ export class SecurityHandler {
50
50
  localAddress: conn.destination.ip,
51
51
  startTime: conn.startTime,
52
52
  protocol: conn.type === 'http' ? 'https' : conn.type as any,
53
- state: conn.status as any,
53
+ state: conn.status === 'active' ? 'connected' : conn.status as any,
54
54
  bytesReceived: (conn as any)._throughputIn || 0,
55
55
  bytesSent: (conn as any)._throughputOut || 0,
56
+ connectionCount: conn.bytesTransferred || 1,
56
57
  }));
58
+ const totalConnections = connectionInfos.reduce((sum, conn) => sum + (conn.connectionCount || 1), 0);
57
59
 
58
60
  const summary = {
59
- total: connectionInfos.length,
61
+ total: totalConnections,
60
62
  byProtocol: connectionInfos.reduce((acc, conn) => {
61
- acc[conn.protocol] = (acc[conn.protocol] || 0) + 1;
63
+ acc[conn.protocol] = (acc[conn.protocol] || 0) + (conn.connectionCount || 1);
62
64
  return acc;
63
65
  }, {} as { [protocol: string]: number }),
64
66
  byState: connectionInfos.reduce((acc, conn) => {
65
- acc[conn.state] = (acc[conn.state] || 0) + 1;
67
+ acc[conn.state] = (acc[conn.state] || 0) + (conn.connectionCount || 1);
66
68
  return acc;
67
69
  }, {} as { [state: string]: number }),
68
70
  };
@@ -104,6 +106,8 @@ export class SecurityHandler {
104
106
  requestsPerSecond: networkStats.requestsPerSecond || 0,
105
107
  requestsTotal: networkStats.requestsTotal || 0,
106
108
  backends: networkStats.backends || [],
109
+ frontendProtocols: networkStats.frontendProtocols || null,
110
+ backendProtocols: networkStats.backendProtocols || null,
107
111
  };
108
112
  }
109
113
 
@@ -120,6 +124,8 @@ export class SecurityHandler {
120
124
  requestsPerSecond: 0,
121
125
  requestsTotal: 0,
122
126
  backends: [],
127
+ frontendProtocols: null,
128
+ backendProtocols: null,
123
129
  };
124
130
  }
125
131
  )
@@ -335,4 +341,4 @@ export class SecurityHandler {
335
341
  limits: [],
336
342
  };
337
343
  }
338
- }
344
+ }
@@ -302,6 +302,7 @@ export class StatsHandler {
302
302
  startTime: 0,
303
303
  bytesIn: tp?.in || 0,
304
304
  bytesOut: tp?.out || 0,
305
+ connectionCount: count,
305
306
  });
306
307
  }
307
308
 
package/ts/readme.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # @serve.zone/dcrouter
2
2
 
3
- The core DcRouter package a unified datacenter gateway orchestrator. 🚀
4
-
5
- This is the main entry point for DcRouter. It provides the `DcRouter` class that wires together SmartProxy, smartmta, SmartDNS, SmartRadius, RemoteIngress, and the OpsServer dashboard into a single cohesive service.
3
+ The `ts/` directory is the main dcrouter runtime package. It exposes the `DcRouter` orchestrator, `IDcRouterOptions`, `runCli()`, and the server-side exports that matter when you want to boot the full router stack from code.
6
4
 
7
5
  ## Issue Reporting and Security
8
6
 
@@ -14,7 +12,19 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
14
12
  pnpm add @serve.zone/dcrouter
15
13
  ```
16
14
 
17
- ## Usage
15
+ ## Core Exports
16
+
17
+ | Export | Purpose |
18
+ | --- | --- |
19
+ | `DcRouter` | Main orchestrator for proxying, DNS, email, VPN, RADIUS, remote ingress, DB, and OpsServer |
20
+ | `IDcRouterOptions` | Top-level configuration shape |
21
+ | `runCli()` | Bootstrap helper; uses OCI env-driven config when `DCROUTER_MODE=OCI_CONTAINER` |
22
+ | `UnifiedEmailServer` and smartmta types | Re-exported email server primitives |
23
+ | `RadiusServer` and related types | RADIUS server runtime exports |
24
+ | `RemoteIngressManager` and `TunnelManager` | Remote ingress orchestration exports |
25
+ | `IHttp3Config` | HTTP/3 configuration for qualifying HTTPS routes |
26
+
27
+ ## Quick Start
18
28
 
19
29
  ```typescript
20
30
  import { DcRouter } from '@serve.zone/dcrouter';
@@ -23,120 +33,53 @@ const router = new DcRouter({
23
33
  smartProxyConfig: {
24
34
  routes: [
25
35
  {
26
- name: 'web-app',
27
- match: { domains: ['example.com'], ports: [443] },
36
+ name: 'local-app',
37
+ match: {
38
+ domains: ['localhost'],
39
+ ports: [18080],
40
+ },
28
41
  action: {
29
42
  type: 'forward',
30
- targets: [{ host: '192.168.1.10', port: 8080 }],
31
- tls: { mode: 'terminate', certificate: 'auto' }
32
- }
33
- }
43
+ targets: [{ host: '127.0.0.1', port: 3001 }],
44
+ },
45
+ },
34
46
  ],
35
- acme: { email: 'admin@example.com', enabled: true, useProduction: true }
36
- }
47
+ },
48
+ opsServerPort: 3000,
37
49
  });
38
50
 
39
51
  await router.start();
40
- // OpsServer dashboard at http://localhost:3000 (configurable via opsServerPort)
41
-
42
- // Graceful shutdown
43
- await router.stop();
44
- ```
45
-
46
- ## Module Structure
47
-
48
- ```
49
- ts/
50
- ├── index.ts # Main exports (DcRouter, re-exported smartmta types)
51
- ├── classes.dcrouter.ts # DcRouter orchestrator class + IDcRouterOptions
52
- ├── classes.cert-provision-scheduler.ts # Per-domain cert backoff scheduler
53
- ├── classes.storage-cert-manager.ts # SmartAcme cert manager backed by StorageManager
54
- ├── logger.ts # Structured logging utility
55
- ├── paths.ts # Centralized data directory paths
56
- ├── plugins.ts # All dependency imports
57
- ├── cache/ # Cache database (smartdata + LocalTsmDb)
58
- │ ├── classes.cachedb.ts # CacheDb singleton
59
- │ ├── classes.cachecleaner.ts # TTL-based cleanup
60
- │ └── documents/ # Cached document models
61
- ├── config/ # Configuration utilities
62
- ├── errors/ # Error classes and retry logic
63
- ├── http3/ # HTTP/3 (QUIC) route augmentation
64
- │ ├── index.ts # Barrel export
65
- │ └── http3-route-augmentation.ts # Pure utility: augmentRoutesWithHttp3(), IHttp3Config
66
- ├── monitoring/ # MetricsManager (SmartMetrics integration)
67
- ├── opsserver/ # OpsServer dashboard + API handlers
68
- │ ├── classes.opsserver.ts # HTTP server + TypedRouter setup
69
- │ └── handlers/ # TypedRequest handlers by domain
70
- │ ├── admin.handler.ts # Auth (login/logout/verify)
71
- │ ├── stats.handler.ts # Statistics + health
72
- │ ├── config.handler.ts # Configuration (read-only)
73
- │ ├── logs.handler.ts # Log retrieval
74
- │ ├── email.handler.ts # Email operations
75
- │ ├── certificate.handler.ts # Certificate management
76
- │ ├── radius.handler.ts # RADIUS management
77
- │ ├── remoteingress.handler.ts # Remote ingress edge + token management
78
- │ ├── route-management.handler.ts # Programmatic route CRUD
79
- │ ├── api-token.handler.ts # API token management
80
- │ └── security.handler.ts # Security metrics + connections
81
- ├── radius/ # RADIUS server integration
82
- ├── remoteingress/ # Remote ingress hub integration
83
- │ ├── classes.remoteingress-manager.ts # Edge CRUD + port derivation
84
- │ └── classes.tunnel-manager.ts # Rust hub lifecycle + status tracking
85
- ├── security/ # Security utilities
86
- ├── sms/ # SMS integration
87
- └── storage/ # StorageManager (filesystem/custom/memory)
88
- ```
89
-
90
- ## Exports
91
-
92
- ```typescript
93
- // Main class
94
- export { DcRouter, IDcRouterOptions } from './classes.dcrouter.js';
95
-
96
- // Re-exported from smartmta
97
- export { UnifiedEmailServer } from '@push.rocks/smartmta';
98
- export type { IUnifiedEmailServerOptions, IEmailRoute, IEmailDomainConfig } from '@push.rocks/smartmta';
99
-
100
- // RADIUS
101
- export { RadiusServer, IRadiusServerConfig } from './radius/index.js';
102
-
103
- // Remote Ingress
104
- export { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
105
-
106
- // HTTP/3
107
- export type { IHttp3Config } from './http3/index.js';
108
52
  ```
109
53
 
110
- ## Key Classes
111
-
112
- ### `DcRouter`
113
-
114
- The central orchestrator. Accepts `IDcRouterOptions` and manages the lifecycle of all sub-services:
54
+ ## What `DcRouter` Manages
115
55
 
116
- | Config Section | Service Started | Package |
117
- |----------------|----------------|---------|
118
- | `smartProxyConfig` | SmartProxy (HTTP/HTTPS/TCP/SNI) | `@push.rocks/smartproxy` |
119
- | `emailConfig` | UnifiedEmailServer (SMTP) | `@push.rocks/smartmta` |
120
- | `dnsNsDomains` + `dnsScopes` | DnsServer (UDP + DoH) | `@push.rocks/smartdns` |
121
- | `radiusConfig` | RadiusServer (auth + accounting) | `@push.rocks/smartradius` |
122
- | `remoteIngressConfig` | RemoteIngressManager + TunnelManager | `@serve.zone/remoteingress` |
123
- | `tls` + `dnsChallenge` | SmartAcme (ACME cert provisioning) | `@push.rocks/smartacme` |
124
- | `http3` | HTTP/3 route augmentation (enabled by default) | built-in |
125
- | `cacheConfig` | CacheDb (embedded MongoDB) | `@push.rocks/smartdata` |
126
- | *(always)* | OpsServer (dashboard + API) | `@api.global/typedserver` |
127
- | *(always)* | MetricsManager | `@push.rocks/smartmetrics` |
56
+ - SmartProxy for HTTP/HTTPS/TCP routes
57
+ - `UnifiedEmailServer` for SMTP ingress and delivery when `emailConfig` is present
58
+ - DB-backed managers for routes, API tokens, target profiles, domains, records, ACME config, and email domains when the DB is enabled
59
+ - embedded authoritative DNS and DoH route generation from `dnsNsDomains` and `dnsScopes`
60
+ - VPN, RADIUS, and remote ingress services when their config blocks are enabled
61
+ - OpsServer and the dashboard, which start on every boot
128
62
 
129
- ### `RemoteIngressManager`
63
+ ## Important Runtime Behavior
130
64
 
131
- Manages CRUD for remote ingress edge registrations. Persists edges via StorageManager. Provides port derivation from routes tagged with `remoteIngress.enabled`.
65
+ - The DB is enabled by default and uses an embedded local database when no external MongoDB URL is provided.
66
+ - System routes from config, email, and DNS are persisted with stable ownership and are toggle-only.
67
+ - API-created routes are the only routes intended for full CRUD from the dashboard or client SDK.
68
+ - Qualifying HTTPS forward routes on port `443` get HTTP/3 augmentation by default.
69
+ - `runCli()` is the supported code-level bootstrap entrypoint; the package does not expose a separate npm `bin` command.
132
70
 
133
- ### `TunnelManager`
71
+ ## Use Another Module When...
134
72
 
135
- Manages the Rust-based RemoteIngressHub lifecycle. Syncs allowed edges, tracks connection status, and exposes edge statuses (connected, publicIp, activeTunnels, lastHeartbeat).
73
+ | Need | Module |
74
+ | --- | --- |
75
+ | A higher-level client SDK for a running router | `@serve.zone/dcrouter-apiclient` or `@serve.zone/dcrouter/apiclient` |
76
+ | Raw TypedRequest request/data contracts | `@serve.zone/dcrouter-interfaces` or `@serve.zone/dcrouter/interfaces` |
77
+ | The standalone migration runner | `@serve.zone/dcrouter-migrations` |
78
+ | The browser dashboard module boundary | `@serve.zone/dcrouter-web` |
136
79
 
137
80
  ## License and Legal Information
138
81
 
139
- This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
82
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../license) file.
140
83
 
141
84
  **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.
142
85
 
@@ -148,7 +91,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
148
91
 
149
92
  ### Company Information
150
93
 
151
- Task Venture Capital GmbH
94
+ Task Venture Capital GmbH
152
95
  Registered at District Court Bremen HRB 35230 HB, Germany
153
96
 
154
97
  For any legal inquiries or further information, please contact us via email at hello@task.vc.
@@ -112,14 +112,11 @@ export class VpnManager {
112
112
  const subnet = this.getSubnet();
113
113
  const wgListenPort = this.config.wgListenPort ?? 51820;
114
114
 
115
- // Auto-detect hybrid mode: if any persisted client uses host IP and mode is
116
- // 'socket' (or unset), upgrade to 'hybrid' so the daemon can handle both
117
- let configuredMode = this.forwardingModeOverride ?? this.config.forwardingMode ?? 'socket';
118
- if (anyClientUsesHostIp && configuredMode === 'socket') {
119
- configuredMode = 'hybrid';
115
+ const desiredForwardingMode = this.getDesiredForwardingMode(anyClientUsesHostIp);
116
+ if (anyClientUsesHostIp && desiredForwardingMode === 'hybrid') {
120
117
  logger.log('info', 'VPN: Auto-upgrading forwarding mode to hybrid (client with useHostIp detected)');
121
118
  }
122
- const forwardingMode = configuredMode === 'hybrid' ? 'hybrid' : configuredMode;
119
+ const forwardingMode = desiredForwardingMode;
123
120
  const isBridge = forwardingMode === 'bridge';
124
121
  this.resolvedForwardingMode = forwardingMode;
125
122
  this.forwardingModeOverride = undefined;
@@ -218,7 +215,7 @@ export class VpnManager {
218
215
  throw new Error('VPN server not running');
219
216
  }
220
217
 
221
- await this.ensureForwardingModeForHostIpClient(opts.useHostIp === true);
218
+ await this.ensureForwardingModeForNextClient(opts.useHostIp === true);
222
219
 
223
220
  const doc = new VpnClientDoc();
224
221
  doc.clientId = opts.clientId;
@@ -298,6 +295,7 @@ export class VpnManager {
298
295
  if (doc) {
299
296
  await doc.delete();
300
297
  }
298
+ await this.reconcileForwardingMode();
301
299
  this.config.onClientChanged?.();
302
300
  }
303
301
 
@@ -368,8 +366,10 @@ export class VpnManager {
368
366
  await this.persistClient(client);
369
367
 
370
368
  if (this.vpnServer) {
371
- await this.ensureForwardingModeForHostIpClient(client.useHostIp === true);
372
- await this.vpnServer.updateClient(clientId, this.buildClientRuntimeUpdate(client));
369
+ const restarted = await this.reconcileForwardingMode();
370
+ if (!restarted) {
371
+ await this.vpnServer.updateClient(clientId, this.buildClientRuntimeUpdate(client));
372
+ }
373
373
  }
374
374
 
375
375
  this.config.onClientChanged?.();
@@ -563,6 +563,28 @@ export class VpnManager {
563
563
  ?? 'socket';
564
564
  }
565
565
 
566
+ private hasHostIpClients(extraHostIpClient = false): boolean {
567
+ if (extraHostIpClient) {
568
+ return true;
569
+ }
570
+
571
+ for (const client of this.clients.values()) {
572
+ if (client.useHostIp) {
573
+ return true;
574
+ }
575
+ }
576
+
577
+ return false;
578
+ }
579
+
580
+ private getDesiredForwardingMode(hasHostIpClients = this.hasHostIpClients()): 'socket' | 'bridge' | 'hybrid' {
581
+ const configuredMode = this.forwardingModeOverride ?? this.config.forwardingMode ?? 'socket';
582
+ if (configuredMode !== 'socket') {
583
+ return configuredMode;
584
+ }
585
+ return hasHostIpClients ? 'hybrid' : 'socket';
586
+ }
587
+
566
588
  private getDefaultDestinationPolicy(
567
589
  forwardingMode: 'socket' | 'bridge' | 'hybrid',
568
590
  useHostIp = false,
@@ -633,16 +655,45 @@ export class VpnManager {
633
655
  };
634
656
  }
635
657
 
636
- private async ensureForwardingModeForHostIpClient(useHostIp: boolean): Promise<void> {
637
- if (!useHostIp || !this.vpnServer) return;
638
- if (this.getResolvedForwardingMode() !== 'socket') return;
639
-
640
- logger.log('info', 'VPN: Restarting server in hybrid mode to support a host-IP client');
641
- this.forwardingModeOverride = 'hybrid';
658
+ private async restartWithForwardingMode(
659
+ forwardingMode: 'socket' | 'bridge' | 'hybrid',
660
+ reason: string,
661
+ ): Promise<void> {
662
+ logger.log('info', `VPN: Restarting server in ${forwardingMode} mode ${reason}`);
663
+ this.forwardingModeOverride = forwardingMode;
642
664
  await this.stop();
643
665
  await this.start();
644
666
  }
645
667
 
668
+ private async ensureForwardingModeForNextClient(useHostIp: boolean): Promise<void> {
669
+ if (!this.vpnServer) return;
670
+
671
+ const desiredForwardingMode = this.getDesiredForwardingMode(this.hasHostIpClients(useHostIp));
672
+ if (desiredForwardingMode === this.getResolvedForwardingMode()) {
673
+ return;
674
+ }
675
+
676
+ await this.restartWithForwardingMode(desiredForwardingMode, 'to support a host-IP client');
677
+ }
678
+
679
+ private async reconcileForwardingMode(): Promise<boolean> {
680
+ if (!this.vpnServer) {
681
+ return false;
682
+ }
683
+
684
+ const desiredForwardingMode = this.getDesiredForwardingMode();
685
+ const currentForwardingMode = this.getResolvedForwardingMode();
686
+ if (desiredForwardingMode === currentForwardingMode) {
687
+ return false;
688
+ }
689
+
690
+ const reason = desiredForwardingMode === 'socket'
691
+ ? 'because no host-IP clients remain'
692
+ : 'to support host-IP clients';
693
+ await this.restartWithForwardingMode(desiredForwardingMode, reason);
694
+ return true;
695
+ }
696
+
646
697
  private async persistClient(client: VpnClientDoc): Promise<void> {
647
698
  await client.save();
648
699
  }