@serfab/cadre-host 1.14.0 → 1.15.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.
- package/README.md +19 -4
- package/dist/bin/host.js +32 -20
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/nat-output.d.ts +5 -0
- package/dist/bin/nat-output.js +12 -1
- package/dist/bin/nat-output.js.map +1 -1
- package/dist/bin/ui-access.d.ts +22 -0
- package/dist/bin/ui-access.js +44 -0
- package/dist/bin/ui-access.js.map +1 -0
- package/dist/hosted/claim-details.d.ts +25 -3
- package/dist/hosted/claim-details.js +31 -37
- package/dist/hosted/claim-details.js.map +1 -1
- package/dist/hosted/hosted-node-service.d.ts +7 -0
- package/dist/hosted/hosted-node-service.js +16 -4
- package/dist/hosted/hosted-node-service.js.map +1 -1
- package/dist/hosted/hosted-node-watcher.d.ts +16 -4
- package/dist/hosted/hosted-node-watcher.js +45 -9
- package/dist/hosted/hosted-node-watcher.js.map +1 -1
- package/dist/hosted/node-status.d.ts +19 -3
- package/dist/hosted/node-status.js +21 -5
- package/dist/hosted/node-status.js.map +1 -1
- package/dist/hosted/types.d.ts +12 -5
- package/dist/hosted/types.js.map +1 -1
- package/dist/installer/config.d.ts +23 -0
- package/dist/installer/config.js +46 -0
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +7 -16
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/wizard.d.ts +12 -2
- package/dist/installer/wizard.js +15 -7
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/index.d.ts +2 -1
- package/dist/nat/index.js +1 -0
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +22 -5
- package/dist/nat/nat-service.js +75 -26
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.js +11 -0
- package/dist/nat/nat-store.js.map +1 -1
- package/dist/nat/network-mode.d.ts +19 -0
- package/dist/nat/network-mode.js +32 -0
- package/dist/nat/network-mode.js.map +1 -0
- package/dist/nat/reachability.d.ts +5 -4
- package/dist/nat/reachability.js +19 -6
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +26 -6
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +3 -3
- package/dist/orchestrator/host-process-orchestrator.js +6 -15
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +4 -0
- package/dist/package-version.js.map +1 -0
- package/dist/server/routes/settings.d.ts +1 -0
- package/dist/server/routes/settings.js +15 -1
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.js +2 -32
- package/dist/server/routes/status.js.map +1 -1
- package/dist/ui/assets/index-BHd8CWZA.css +1 -0
- package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
- package/dist/ui/assets/{index-4FWuU8ye.js.map → index-Ccr2GjPT.js.map} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +4 -5
- package/src/__tests__/cli.smoke.test.ts +1 -1
- package/src/__tests__/orchestrator-claim-secret.test.ts +1 -2
- package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
- package/src/__tests__/orchestrator-handle-contract.ts +1 -1
- package/src/__tests__/orchestrator-node-identity.test.ts +0 -5
- package/src/__tests__/orchestrator-push.test.ts +3 -3
- package/src/__tests__/orchestrator.test.ts +3 -5
- package/src/__tests__/ui-access.test.ts +30 -0
- package/src/bin/host.ts +30 -20
- package/src/bin/nat-output.ts +12 -1
- package/src/bin/ui-access.ts +50 -0
- package/src/hosted/__tests__/fake-orchestrator.test.ts +0 -1
- package/src/hosted/__tests__/fake-orchestrator.ts +263 -265
- package/src/hosted/__tests__/hosted-node-service.test.ts +708 -609
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -474
- package/src/hosted/claim-details.ts +44 -37
- package/src/hosted/hosted-node-service.ts +23 -4
- package/src/hosted/hosted-node-watcher.ts +288 -250
- package/src/hosted/node-status.ts +31 -5
- package/src/hosted/types.ts +160 -153
- package/src/installer/__tests__/installer.smoke.test.ts +184 -182
- package/src/installer/__tests__/wizard.test.ts +76 -56
- package/src/installer/config.ts +66 -0
- package/src/installer/index.ts +252 -259
- package/src/installer/wizard.ts +101 -87
- package/src/nat/__tests__/nat-service.test.ts +608 -540
- package/src/nat/__tests__/nat-store.test.ts +130 -127
- package/src/nat/__tests__/network-mode.test.ts +38 -0
- package/src/nat/index.ts +91 -87
- package/src/nat/nat-service.ts +79 -26
- package/src/nat/nat-store.ts +249 -237
- package/src/nat/network-mode.ts +36 -0
- package/src/nat/reachability.ts +155 -143
- package/src/nat/types.ts +232 -209
- package/src/orchestrator/host-process-orchestrator.ts +1089 -1098
- package/src/package-version.ts +4 -0
- package/src/server/__tests__/fakes.ts +41 -40
- package/src/server/__tests__/hosted-nodes-route.test.ts +161 -160
- package/src/server/__tests__/settings-route.test.ts +31 -0
- package/src/server/routes/settings.ts +128 -115
- package/src/server/routes/status.ts +72 -102
- package/dist/ui/assets/index-4FWuU8ye.js +0 -19
- package/dist/ui/assets/index-Di9Q4d01.css +0 -1
package/src/nat/nat-service.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type NatHandlers,
|
|
10
10
|
type NatSettingsFile,
|
|
11
11
|
type NatStatusSnapshot,
|
|
12
|
+
type NetworkMode,
|
|
12
13
|
type NodeReachability,
|
|
13
14
|
type PortKind,
|
|
14
15
|
type PortRoute,
|
|
@@ -26,6 +27,7 @@ import { evaluateHostReachability, evaluateNodeReachability } from './reachabili
|
|
|
26
27
|
import { DdnsUpdater } from './ddns/updater.js';
|
|
27
28
|
import { getProvider, listProviders } from './ddns/index.js';
|
|
28
29
|
import { buildPublicAddresses, isPublicIpv4 } from './address-resolver.js';
|
|
30
|
+
import { effectiveNetworkMode, publicInterfaceAddress } from './network-mode.js';
|
|
29
31
|
import { createSecretsStore, ddnsAccount, type SecretsStore } from './secrets/index.js';
|
|
30
32
|
import { AddressWatch, type NodeAddressesStaleListener } from './address-watch.js';
|
|
31
33
|
import type { ManagedNodeInfo, NodeStateListener } from '../orchestrator/types.js';
|
|
@@ -80,6 +82,8 @@ export interface NatServiceOptions {
|
|
|
80
82
|
store?: NatStore;
|
|
81
83
|
/** Lease TTL asked of the router. Default 1 h. */
|
|
82
84
|
leaseTtlMs?: number;
|
|
85
|
+
/** Test-only: stub the interface read behind `network: auto`. Default: `publicInterfaceAddress()`. */
|
|
86
|
+
publicInterfaceAddress?: () => string | null;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
/** A node's two mapped ports — `tcp` is `NodePorts.p2p`, `ws` is `NodePorts.ws`. */
|
|
@@ -105,6 +109,11 @@ function noRoute(internalPort: number): PortRoute {
|
|
|
105
109
|
return { internalPort, externalPort: null, source: null, leaseExpiresAt: null, error: null };
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
/** Public mode: the port is reached at its own number, with nothing to map. */
|
|
113
|
+
function directRoute(internalPort: number): PortRoute {
|
|
114
|
+
return { internalPort, externalPort: internalPort, source: 'direct', leaseExpiresAt: null, error: null };
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
function manualRoute(internalPort: number, externalPort: number): PortRoute {
|
|
109
118
|
return { internalPort, externalPort, source: 'manual', leaseExpiresAt: null, error: null };
|
|
110
119
|
}
|
|
@@ -168,6 +177,9 @@ export class NatService {
|
|
|
168
177
|
|
|
169
178
|
private currentSettings: NatSettingsFile;
|
|
170
179
|
private gateway: NatGatewayStatus = { found: false, lanAddress: null, routerExternalIp: null, lastError: null };
|
|
180
|
+
/** The public IPv4 an interface holds (`publicInterfaceAddress`), re-read at start and on every probe. */
|
|
181
|
+
private publicInterfaceIp: string | null = null;
|
|
182
|
+
private readonly readInterfaces: () => string | null;
|
|
171
183
|
private latestIp: ExternalIpResult | null = null;
|
|
172
184
|
private lastTestedAt: Date | null = null;
|
|
173
185
|
private readonly table = new Map<string, NodeEntry>();
|
|
@@ -193,6 +205,7 @@ export class NatService {
|
|
|
193
205
|
this.secretsStore = opts.secretsStore ?? null;
|
|
194
206
|
this.injectedDetector = opts.externalIpDetector ?? null;
|
|
195
207
|
this.secretsRootDir = opts.rootDir;
|
|
208
|
+
this.readInterfaces = opts.publicInterfaceAddress ?? (() => publicInterfaceAddress());
|
|
196
209
|
this.currentSettings = this.store.load();
|
|
197
210
|
this.addressWatch = new AddressWatch(() => this.nowFn().getTime());
|
|
198
211
|
}
|
|
@@ -211,7 +224,9 @@ export class NatService {
|
|
|
211
224
|
if (!this.secretsStore) {
|
|
212
225
|
this.secretsStore = await createSecretsStore(this.secretsRootDir);
|
|
213
226
|
}
|
|
214
|
-
|
|
227
|
+
this.publicInterfaceIp = this.readInterfaces();
|
|
228
|
+
log('network mode %s (setting %s, public interface address %s)', this.networkMode(), this.currentSettings.network, this.publicInterfaceIp ?? 'none');
|
|
229
|
+
if (this.usesUpnp()) await this.discoverGateway();
|
|
215
230
|
await this.detectIp();
|
|
216
231
|
|
|
217
232
|
this.ddnsUpdater = new DdnsUpdater({
|
|
@@ -269,11 +284,12 @@ export class NatService {
|
|
|
269
284
|
|
|
270
285
|
/** Cheap read of the current snapshot. */
|
|
271
286
|
getStatus(): NatStatusSnapshot {
|
|
272
|
-
const cgnatDetected = this.
|
|
287
|
+
const cgnatDetected = this.cgnatDetected();
|
|
273
288
|
const nodes = [...this.table.values()]
|
|
274
289
|
.sort((a, b) => a.nodeId.localeCompare(b.nodeId))
|
|
275
290
|
.map((entry) => this.describeNode(entry, cgnatDetected));
|
|
276
291
|
return {
|
|
292
|
+
network: { setting: this.currentSettings.network, mode: this.networkMode(), publicInterfaceIp: this.publicInterfaceIp },
|
|
277
293
|
upnpEnabled: this.currentSettings.upnpEnabled,
|
|
278
294
|
gateway: { ...this.gateway, routerExternalIp: this.latestIp?.routerIp ?? null },
|
|
279
295
|
externalIp: this.detectedIp(),
|
|
@@ -357,18 +373,19 @@ export class NatService {
|
|
|
357
373
|
}
|
|
358
374
|
|
|
359
375
|
/**
|
|
360
|
-
* The 5-minute probe pass:
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
376
|
+
* The 5-minute probe pass: re-read the interfaces and re-route every node when
|
|
377
|
+
* the mode `auto` lands on changes; while UPnP is in use and no router has
|
|
378
|
+
* answered, search again and map the running nodes as soon as one does — a
|
|
379
|
+
* host that boots before its network, or a router that comes up later, is
|
|
380
|
+
* found here rather than at the next restart; then re-detect the external IP.
|
|
364
381
|
*/
|
|
365
382
|
async redetect(): Promise<void> {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
});
|
|
371
|
-
}
|
|
383
|
+
await this.enqueue(async () => {
|
|
384
|
+
const modeChanged = this.refreshNetworkMode();
|
|
385
|
+
const gatewayWasFound = this.gateway.found;
|
|
386
|
+
if (this.usesUpnp() && !gatewayWasFound) await this.discoverGateway();
|
|
387
|
+
if (modeChanged || (!gatewayWasFound && this.gateway.found)) await this.reconcileOnce({ mapMissing: true });
|
|
388
|
+
});
|
|
372
389
|
await this.detectIp();
|
|
373
390
|
this.publish();
|
|
374
391
|
}
|
|
@@ -378,7 +395,8 @@ export class NatService {
|
|
|
378
395
|
/** Re-discover the router if needed, re-map everything, re-detect the IP, then return a fresh snapshot. */
|
|
379
396
|
async testReachability(): Promise<NatStatusSnapshot> {
|
|
380
397
|
await this.enqueue(async () => {
|
|
381
|
-
|
|
398
|
+
this.refreshNetworkMode();
|
|
399
|
+
if (this.usesUpnp() && !this.gateway.found) await this.discoverGateway();
|
|
382
400
|
await this.reconcileOnce({ mapMissing: false });
|
|
383
401
|
await this.renewOnce();
|
|
384
402
|
});
|
|
@@ -394,7 +412,8 @@ export class NatService {
|
|
|
394
412
|
/** Replace settings; persists to disk and reconfigures the runtime. */
|
|
395
413
|
async putSettings(patch: Partial<Omit<NatSettingsFile, 'version' | 'forwards'>>): Promise<NatStatusSnapshot> {
|
|
396
414
|
const next = this.store.update(patch);
|
|
397
|
-
const
|
|
415
|
+
const wasUsingUpnp = this.usesUpnp();
|
|
416
|
+
const modeBefore = this.networkMode();
|
|
398
417
|
const ddnsChanged =
|
|
399
418
|
next.ddns.providerId !== this.currentSettings.ddns.providerId ||
|
|
400
419
|
next.ddns.hostname !== this.currentSettings.ddns.hostname ||
|
|
@@ -406,12 +425,12 @@ export class NatService {
|
|
|
406
425
|
if (ddnsChanged && this.ddnsUpdater) {
|
|
407
426
|
await this.ddnsUpdater.updateSettings(next.ddns);
|
|
408
427
|
}
|
|
409
|
-
if (
|
|
410
|
-
//
|
|
411
|
-
// pass, so the pass can map.
|
|
412
|
-
// `upnp` route and keeps the manual ones.
|
|
428
|
+
if (this.usesUpnp() !== wasUsingUpnp || this.networkMode() !== modeBefore) {
|
|
429
|
+
// UPnP coming into use (turned on, or leaving public mode) with no gateway known yet:
|
|
430
|
+
// look for one before the pass, so the pass can map. Going out of use: the pass
|
|
431
|
+
// releases every `upnp` route and keeps the manual ones; public mode adds `direct` ones.
|
|
413
432
|
await this.enqueue(async () => {
|
|
414
|
-
if (
|
|
433
|
+
if (this.usesUpnp() && !this.gateway.found) await this.discoverGateway();
|
|
415
434
|
await this.reconcileOnce({ mapMissing: true });
|
|
416
435
|
});
|
|
417
436
|
}
|
|
@@ -587,9 +606,14 @@ export class NatService {
|
|
|
587
606
|
entry.routes[kind] = manualRoute(route.internalPort, forward);
|
|
588
607
|
continue;
|
|
589
608
|
}
|
|
590
|
-
if (route.source === 'manual') {
|
|
609
|
+
if (route.source === 'manual' || route.source === 'direct') {
|
|
591
610
|
entry.routes[kind] = noRoute(route.internalPort);
|
|
592
611
|
}
|
|
612
|
+
if (this.networkMode() === 'public') {
|
|
613
|
+
if (route.source === 'upnp') await this.unmapRoute(entry, kind);
|
|
614
|
+
entry.routes[kind] = directRoute(route.internalPort);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
593
617
|
if (!this.currentSettings.upnpEnabled) {
|
|
594
618
|
if (entry.routes[kind]!.source === 'upnp') await this.unmapRoute(entry, kind);
|
|
595
619
|
entry.routes[kind] = noRoute(route.internalPort);
|
|
@@ -606,12 +630,12 @@ export class NatService {
|
|
|
606
630
|
|
|
607
631
|
private async renewOnce(): Promise<void> {
|
|
608
632
|
if (!this.started) return;
|
|
609
|
-
if (!this.
|
|
633
|
+
if (!this.usesUpnp() || !this.gateway.found) return;
|
|
610
634
|
for (const entry of this.table.values()) {
|
|
611
635
|
if (!this.started) break;
|
|
612
636
|
if (!entry.running) continue;
|
|
613
637
|
for (const kind of this.kindsOf(entry)) {
|
|
614
|
-
if (entry.routes[kind]!.source
|
|
638
|
+
if (entry.routes[kind]!.source !== 'upnp' && entry.routes[kind]!.source !== null) continue;
|
|
615
639
|
await this.mapRoute(entry, kind);
|
|
616
640
|
}
|
|
617
641
|
}
|
|
@@ -735,12 +759,40 @@ export class NatService {
|
|
|
735
759
|
return this.latestIp?.publicIp ?? this.latestIp?.routerIp ?? null;
|
|
736
760
|
}
|
|
737
761
|
|
|
738
|
-
/** The IP used in `/ip4/` addresses: only a public IPv4 counts. */
|
|
762
|
+
/** The IP used in `/ip4/` addresses: only a public IPv4 counts. Public mode prefers the interface's own. */
|
|
739
763
|
private publicIpv4(): string | null {
|
|
764
|
+
if (this.networkMode() === 'public' && this.publicInterfaceIp) return this.publicInterfaceIp;
|
|
740
765
|
const ip = this.detectedIp();
|
|
741
766
|
return ip && isPublicIpv4(ip) ? ip : null;
|
|
742
767
|
}
|
|
743
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Re-read the interfaces (a VPS gains an address, a laptop moves networks); true when that
|
|
771
|
+
* changed the mode in effect, so the caller re-routes every node in the same pass.
|
|
772
|
+
*/
|
|
773
|
+
private refreshNetworkMode(): boolean {
|
|
774
|
+
const modeBefore = this.networkMode();
|
|
775
|
+
this.publicInterfaceIp = this.readInterfaces();
|
|
776
|
+
const changed = this.networkMode() !== modeBefore;
|
|
777
|
+
if (changed) log('network mode changed to %s (public interface address %s)', this.networkMode(), this.publicInterfaceIp ?? 'none');
|
|
778
|
+
return changed;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** The mode in effect: the `network` setting, or what `auto` reads from the interfaces. */
|
|
782
|
+
private networkMode(): NetworkMode {
|
|
783
|
+
return effectiveNetworkMode(this.currentSettings.network, this.publicInterfaceIp);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** UPnP is asked for nothing in public mode: there is no router to ask. */
|
|
787
|
+
private usesUpnp(): boolean {
|
|
788
|
+
return this.currentSettings.upnpEnabled && this.networkMode() === 'lan';
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** An interface holding the public address rules out carrier-grade NAT, whatever the probes say. */
|
|
792
|
+
private cgnatDetected(): boolean {
|
|
793
|
+
return this.networkMode() === 'public' ? false : this.latestIp?.cgnat ?? false;
|
|
794
|
+
}
|
|
795
|
+
|
|
744
796
|
// --- status assembly ---
|
|
745
797
|
|
|
746
798
|
private describeNode(entry: NodeEntry, cgnatDetected: boolean): NodeReachability {
|
|
@@ -751,7 +803,7 @@ export class NatService {
|
|
|
751
803
|
ws,
|
|
752
804
|
hostKnown: this.hostKnown(),
|
|
753
805
|
cgnatDetected,
|
|
754
|
-
upnpEnabled: this.
|
|
806
|
+
upnpEnabled: this.usesUpnp(),
|
|
755
807
|
gatewayFound: this.gateway.found,
|
|
756
808
|
lanAddress: this.gateway.lanAddress,
|
|
757
809
|
});
|
|
@@ -774,7 +826,7 @@ export class NatService {
|
|
|
774
826
|
return buildPublicAddresses({
|
|
775
827
|
ddnsHostname: this.currentSettings.ddns.hostname,
|
|
776
828
|
externalIp: this.publicIpv4(),
|
|
777
|
-
cgnatDetected: this.
|
|
829
|
+
cgnatDetected: this.cgnatDetected(),
|
|
778
830
|
tcp,
|
|
779
831
|
ws,
|
|
780
832
|
});
|
|
@@ -783,12 +835,13 @@ export class NatService {
|
|
|
783
835
|
private routeOrPrediction(nodeId: string, kind: PortKind, internalPort: number, entry: NodeEntry | undefined): PortRoute {
|
|
784
836
|
const forward = this.currentSettings.forwards[nodeId]?.[kind];
|
|
785
837
|
if (forward !== undefined) return manualRoute(internalPort, forward);
|
|
838
|
+
if (this.networkMode() === 'public') return directRoute(internalPort);
|
|
786
839
|
const existing = entry?.routes[kind];
|
|
787
840
|
if (existing && existing.internalPort === internalPort) {
|
|
788
841
|
const route = this.effectiveRoute(existing);
|
|
789
842
|
if (route.source !== null || route.error !== null) return route;
|
|
790
843
|
}
|
|
791
|
-
if (this.
|
|
844
|
+
if (this.usesUpnp() && this.gateway.found) {
|
|
792
845
|
return { internalPort, externalPort: internalPort, source: 'upnp', leaseExpiresAt: null, error: null };
|
|
793
846
|
}
|
|
794
847
|
return noRoute(internalPort);
|