@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/reachability.ts
CHANGED
|
@@ -1,143 +1,155 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reachability verdicts: one per hosted node, and the host-level roll-up the
|
|
3
|
-
* dashboard badge shows. Heuristics over the mapping table, not a dial-back —
|
|
4
|
-
* libp2p AutoNAT is a later ticket.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
DirectReachability,
|
|
9
|
-
NodeReachability,
|
|
10
|
-
NodeVerdict,
|
|
11
|
-
PortKind,
|
|
12
|
-
PortRoute,
|
|
13
|
-
} from './types.js';
|
|
14
|
-
|
|
15
|
-
export interface NodeVerdictInput {
|
|
16
|
-
tcp: PortRoute;
|
|
17
|
-
/** Null for a handle persisted by an older build without a WebSocket port. */
|
|
18
|
-
ws: PortRoute | null;
|
|
19
|
-
/** Whether a public host part exists: a DDNS hostname, or a public IPv4. */
|
|
20
|
-
hostKnown: boolean;
|
|
21
|
-
cgnatDetected: boolean;
|
|
22
|
-
upnpEnabled: boolean;
|
|
23
|
-
gatewayFound: boolean;
|
|
24
|
-
/** The address a user forwards to; null when no gateway was found. */
|
|
25
|
-
lanAddress: string | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface NodeVerdictResult {
|
|
29
|
-
verdict: NodeVerdict;
|
|
30
|
-
reason: string | null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* `unreachable` when either port has no usable route or the host part is
|
|
35
|
-
* unknown; otherwise `manual` when either port is forwarded by hand,
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
if (
|
|
95
|
-
return '
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
case '
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
case '
|
|
122
|
-
return `
|
|
123
|
-
case '
|
|
124
|
-
return `
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Reachability verdicts: one per hosted node, and the host-level roll-up the
|
|
3
|
+
* dashboard badge shows. Heuristics over the mapping table, not a dial-back —
|
|
4
|
+
* libp2p AutoNAT is a later ticket.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
DirectReachability,
|
|
9
|
+
NodeReachability,
|
|
10
|
+
NodeVerdict,
|
|
11
|
+
PortKind,
|
|
12
|
+
PortRoute,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
export interface NodeVerdictInput {
|
|
16
|
+
tcp: PortRoute;
|
|
17
|
+
/** Null for a handle persisted by an older build without a WebSocket port. */
|
|
18
|
+
ws: PortRoute | null;
|
|
19
|
+
/** Whether a public host part exists: a DDNS hostname, or a public IPv4. */
|
|
20
|
+
hostKnown: boolean;
|
|
21
|
+
cgnatDetected: boolean;
|
|
22
|
+
upnpEnabled: boolean;
|
|
23
|
+
gatewayFound: boolean;
|
|
24
|
+
/** The address a user forwards to; null when no gateway was found. */
|
|
25
|
+
lanAddress: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NodeVerdictResult {
|
|
29
|
+
verdict: NodeVerdict;
|
|
30
|
+
reason: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `unreachable` when either port has no usable route or the host part is
|
|
35
|
+
* unknown; otherwise `manual` when either port is forwarded by hand, `direct`
|
|
36
|
+
* when the ports are reached as-is (public mode, with the firewall reminder as
|
|
37
|
+
* the reason), else `mapped`. A UPnP route under CGNAT is not usable: the
|
|
38
|
+
* router's mapping is on a carrier-private address.
|
|
39
|
+
*/
|
|
40
|
+
export function evaluateNodeReachability(input: NodeVerdictInput): NodeVerdictResult {
|
|
41
|
+
const ports = portRoutes(input);
|
|
42
|
+
const failing = ports.filter(({ route }) => !usable(route, input.cgnatDetected));
|
|
43
|
+
if (!input.hostKnown) {
|
|
44
|
+
return { verdict: 'unreachable', reason: HOST_UNKNOWN_REASON };
|
|
45
|
+
}
|
|
46
|
+
if (failing.length > 0) {
|
|
47
|
+
return { verdict: 'unreachable', reason: describeFailures(failing, input) };
|
|
48
|
+
}
|
|
49
|
+
if (ports.some(({ route }) => route.source === 'manual')) return { verdict: 'manual', reason: null };
|
|
50
|
+
if (ports.some(({ route }) => route.source === 'direct')) return { verdict: 'direct', reason: firewallReminder(ports) };
|
|
51
|
+
return { verdict: 'mapped', reason: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Host roll-up: `cgnat` when CGNAT is detected and no node is reachable
|
|
56
|
+
* through a manual route; `unknown` with no running nodes; `reachable` when
|
|
57
|
+
* every running node is `mapped`/`manual`/`direct`; else `unreachable`.
|
|
58
|
+
*/
|
|
59
|
+
export function evaluateHostReachability(
|
|
60
|
+
nodes: ReadonlyArray<Pick<NodeReachability, 'running' | 'verdict'>>,
|
|
61
|
+
cgnatDetected: boolean,
|
|
62
|
+
): DirectReachability {
|
|
63
|
+
const running = nodes.filter((n) => n.running);
|
|
64
|
+
if (cgnatDetected && !running.some((n) => n.verdict === 'manual')) return 'cgnat';
|
|
65
|
+
if (running.length === 0) return 'unknown';
|
|
66
|
+
return running.every((n) => n.verdict !== 'unreachable') ? 'reachable' : 'unreachable';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const HOST_UNKNOWN_REASON =
|
|
70
|
+
"This host's public address is unknown: no dynamic DNS hostname is set and no public IPv4 address was detected. " +
|
|
71
|
+
'Set a DDNS hostname, or check that this machine can reach the internet.';
|
|
72
|
+
|
|
73
|
+
interface PortEntry {
|
|
74
|
+
kind: PortKind;
|
|
75
|
+
route: PortRoute;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function portRoutes(input: NodeVerdictInput): PortEntry[] {
|
|
79
|
+
const out: PortEntry[] = [{ kind: 'tcp', route: input.tcp }];
|
|
80
|
+
if (input.ws) out.push({ kind: 'ws', route: input.ws });
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function usable(route: PortRoute, cgnatDetected: boolean): boolean {
|
|
85
|
+
if (route.externalPort === null || route.source === null) return false;
|
|
86
|
+
return !(cgnatDetected && route.source === 'upnp');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type FailureCause = 'cgnat' | 'refused' | 'upnp_off' | 'no_gateway' | 'pending';
|
|
90
|
+
|
|
91
|
+
/** Behind CGNAT no router-side remedy helps, so CGNAT outranks whatever else failed the port. */
|
|
92
|
+
function causeOf(route: PortRoute, input: NodeVerdictInput): FailureCause {
|
|
93
|
+
if (input.cgnatDetected) return 'cgnat';
|
|
94
|
+
if (route.error) return 'refused';
|
|
95
|
+
if (!input.upnpEnabled) return 'upnp_off';
|
|
96
|
+
if (!input.gatewayFound) return 'no_gateway';
|
|
97
|
+
return 'pending';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** One sentence per distinct cause, each naming every port that failed for it. */
|
|
101
|
+
function describeFailures(failing: PortEntry[], input: NodeVerdictInput): string {
|
|
102
|
+
const byCause = new Map<FailureCause, PortEntry[]>();
|
|
103
|
+
for (const entry of failing) {
|
|
104
|
+
const cause = causeOf(entry.route, input);
|
|
105
|
+
byCause.set(cause, [...(byCause.get(cause) ?? []), entry]);
|
|
106
|
+
}
|
|
107
|
+
return [...byCause.entries()].map(([cause, entries]) => describeCause(cause, entries, input)).join(' ');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function describeCause(cause: FailureCause, entries: PortEntry[], input: NodeVerdictInput): string {
|
|
111
|
+
const names = entries.map(({ kind, route }) => `${kindLabel(kind)} port ${route.internalPort}`);
|
|
112
|
+
const lan = input.lanAddress ?? "this machine's LAN address";
|
|
113
|
+
const forwardHint = `Forward ${entries.length === 1 ? 'port' : 'ports'} ${listPorts(entries)} to ${lan} on your router, then tell cadre-host the external ${entries.length === 1 ? 'port' : 'ports'}.`;
|
|
114
|
+
switch (cause) {
|
|
115
|
+
case 'cgnat':
|
|
116
|
+
return `This host is behind carrier-grade NAT, so the ${joinNames(names)} cannot be reached from outside through your router, and neither UPnP nor a port forward on your router will help; a relay is needed.`;
|
|
117
|
+
case 'refused': {
|
|
118
|
+
const errors = entries.map(({ route }) => route.error).filter((e): e is string => !!e);
|
|
119
|
+
return `Router refused the ${joinNames(names)} mapping (${errors.join('; ')}). ${forwardHint}`;
|
|
120
|
+
}
|
|
121
|
+
case 'upnp_off':
|
|
122
|
+
return `UPnP is off, so the ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped. Turn UPnP on, or: ${lowerFirst(forwardHint)}`;
|
|
123
|
+
case 'no_gateway':
|
|
124
|
+
return `No UPnP router answered, so the ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped. Enable UPnP on your router, or: ${lowerFirst(forwardHint)}`;
|
|
125
|
+
case 'pending':
|
|
126
|
+
return `The ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped yet; the router is being asked.`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Public mode: nothing to map, but a host or cloud firewall can still block the ports. */
|
|
131
|
+
function firewallReminder(ports: PortEntry[]): string {
|
|
132
|
+
const numbers = ports.map(({ route }) => route.internalPort);
|
|
133
|
+
const ufw = numbers.length === 2 && Math.abs(numbers[0]! - numbers[1]!) === 1
|
|
134
|
+
? `${Math.min(...numbers)}:${Math.max(...numbers)}/tcp`
|
|
135
|
+
: `${numbers.join(',')}/tcp`;
|
|
136
|
+
return `Reached directly at this machine's public address. Allow TCP ${ports.length === 1 ? 'port' : 'ports'} ${listPorts(ports)} through any firewall `
|
|
137
|
+
+ `(for example: sudo ufw allow ${ufw}; and any cloud-provider firewall).`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function kindLabel(kind: PortKind): string {
|
|
141
|
+
return kind === 'tcp' ? 'TCP' : 'WebSocket';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function listPorts(entries: PortEntry[]): string {
|
|
145
|
+
return joinNames(entries.map(({ route }) => String(route.internalPort)));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function joinNames(names: string[]): string {
|
|
149
|
+
if (names.length <= 1) return names[0] ?? '';
|
|
150
|
+
return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function lowerFirst(s: string): string {
|
|
154
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
155
|
+
}
|