@livedesk/hub 0.1.35 → 0.1.37
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/package.json +2 -2
- package/src/agents/agent-audit-store.js +6 -16
- package/src/agents/agent-permissions.js +27 -33
- package/src/agents/agent-tool-registry.js +322 -340
- package/src/captures/capture-store.js +252 -299
- package/src/filesystem/shared-folders.js +181 -189
- package/src/filesystem/transfer-jobs.js +314 -345
- package/src/live-desk-update.js +615 -683
- package/src/remote-hub.js +59 -643
- package/src/server.js +111 -924
- package/src/settings/settings-schema.js +40 -32
- package/src/settings/settings-store.js +2 -7
- package/src/transport/relay-hub-control.js +1376 -1703
- package/src/transport/udp-hub-transport.js +520 -543
- package/src/transport/udp-rendezvous.js +408 -574
- package/src/security/device-credential-authority.js +0 -406
- package/src/security/security-audit-store.js +0 -260
- package/src/transport/secure-direct-acceptor.js +0 -543
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import { lookup } from 'node:dns/promises';
|
|
3
|
-
import dgram from 'node:dgram';
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { lookup } from 'node:dns/promises';
|
|
3
|
+
import dgram from 'node:dgram';
|
|
4
4
|
import {
|
|
5
|
-
UDP_MAX_DATAGRAM_BYTES,
|
|
6
|
-
UDP_FRAME_CHUNK_PROTOCOL,
|
|
7
|
-
UDP_P2P_PROTOCOL,
|
|
8
|
-
UdpFrameReassembler,
|
|
9
|
-
UdpReplayWindow,
|
|
10
|
-
decodeRendezvousMessage,
|
|
11
|
-
decodeUdpPacket,
|
|
5
|
+
UDP_MAX_DATAGRAM_BYTES,
|
|
6
|
+
UDP_FRAME_CHUNK_PROTOCOL,
|
|
7
|
+
UDP_P2P_PROTOCOL,
|
|
8
|
+
UdpFrameReassembler,
|
|
9
|
+
UdpReplayWindow,
|
|
10
|
+
decodeRendezvousMessage,
|
|
11
|
+
decodeUdpPacket,
|
|
12
12
|
encodeRendezvousMessage,
|
|
13
13
|
encodeUdpPacket,
|
|
14
|
-
createUdpSessionKey,
|
|
15
|
-
isUdpBinaryDatagram,
|
|
16
|
-
peekUdpSessionRouteKey,
|
|
17
|
-
udpSessionRouteKey
|
|
18
|
-
} from './udp-protocol.js';
|
|
14
|
+
createUdpSessionKey,
|
|
15
|
+
isUdpBinaryDatagram,
|
|
16
|
+
peekUdpSessionRouteKey,
|
|
17
|
+
udpSessionRouteKey
|
|
18
|
+
} from './udp-protocol.js';
|
|
19
19
|
|
|
20
20
|
function isEnabled(value, fallback = false) {
|
|
21
21
|
if (value === undefined || value === null || value === '') return fallback;
|
|
@@ -31,207 +31,206 @@ function numberEnv(value, min, max, fallback) {
|
|
|
31
31
|
return Number.isFinite(number) ? Math.max(min, Math.min(max, Math.floor(number))) : fallback;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function sameEndpoint(left, right) {
|
|
35
|
-
return !!left && !!right && String(left.address) === String(right.address) && Number(left.port) === Number(right.port);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function normalizedAddress(value) {
|
|
39
|
-
const address = safeText(value, 128).toLowerCase();
|
|
40
|
-
return address.startsWith('::ffff:') ? address.slice(7) : address;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isPublicIPv4(value) {
|
|
44
|
-
const address = normalizedAddress(value);
|
|
45
|
-
const octets = address.split('.').map(Number);
|
|
46
|
-
if (octets.length !== 4
|
|
47
|
-
|| octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
const [a, b] = octets;
|
|
51
|
-
const c = octets[2];
|
|
52
|
-
return a !== 0
|
|
53
|
-
&& a !== 10
|
|
54
|
-
&& a !== 127
|
|
55
|
-
&& !(a === 100 && b >= 64 && b <= 127)
|
|
56
|
-
&& !(a === 169 && b === 254)
|
|
57
|
-
&& !(a === 172 && b >= 16 && b <= 31)
|
|
58
|
-
&& !(a === 192 && (b === 0 || b === 168 || (b === 88 && c === 99)))
|
|
59
|
-
&& !(a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100)))
|
|
60
|
-
&& !(a === 203 && b === 0 && c === 113)
|
|
61
|
-
&& !(a >= 224);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
|
|
65
|
-
const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, true);
|
|
66
|
-
const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, false);
|
|
34
|
+
function sameEndpoint(left, right) {
|
|
35
|
+
return !!left && !!right && String(left.address) === String(right.address) && Number(left.port) === Number(right.port);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizedAddress(value) {
|
|
39
|
+
const address = safeText(value, 128).toLowerCase();
|
|
40
|
+
return address.startsWith('::ffff:') ? address.slice(7) : address;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isPublicIPv4(value) {
|
|
44
|
+
const address = normalizedAddress(value);
|
|
45
|
+
const octets = address.split('.').map(Number);
|
|
46
|
+
if (octets.length !== 4
|
|
47
|
+
|| octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const [a, b] = octets;
|
|
51
|
+
const c = octets[2];
|
|
52
|
+
return a !== 0
|
|
53
|
+
&& a !== 10
|
|
54
|
+
&& a !== 127
|
|
55
|
+
&& !(a === 100 && b >= 64 && b <= 127)
|
|
56
|
+
&& !(a === 169 && b === 254)
|
|
57
|
+
&& !(a === 172 && b >= 16 && b <= 31)
|
|
58
|
+
&& !(a === 192 && (b === 0 || b === 168 || (b === 88 && c === 99)))
|
|
59
|
+
&& !(a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100)))
|
|
60
|
+
&& !(a === 203 && b === 0 && c === 113)
|
|
61
|
+
&& !(a >= 224);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
|
|
65
|
+
const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, true);
|
|
66
|
+
const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, false);
|
|
67
67
|
const bindHost = safeText(env.LIVEDESK_UDP_BIND_HOST || '0.0.0.0', 128) || '0.0.0.0';
|
|
68
68
|
const requestedPort = numberEnv(env.LIVEDESK_UDP_PORT, 0, 65535, 0);
|
|
69
69
|
const advertiseHost = safeText(env.LIVEDESK_UDP_ADVERTISE_HOST || env.LIVEDESK_REMOTE_PUBLIC_HOST || '', 128);
|
|
70
|
-
const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
|
|
71
|
-
const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
|
|
72
|
-
const rendezvousRefreshMs = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_REFRESH_MS, 1000, 50_000, 25_000);
|
|
70
|
+
const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
|
|
71
|
+
const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
|
|
72
|
+
const rendezvousRefreshMs = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_REFRESH_MS, 1000, 50_000, 25_000);
|
|
73
73
|
const punchTimeoutMs = numberEnv(env.LIVEDESK_UDP_PUNCH_TIMEOUT_MS, 500, 10_000, 3000);
|
|
74
|
-
const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 500);
|
|
74
|
+
const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 500);
|
|
75
75
|
const maxPendingFrames = numberEnv(env.LIVEDESK_UDP_MAX_PENDING_FRAMES, 1, 16, 2);
|
|
76
|
-
const trace = isEnabled(env.LIVEDESK_UDP_TRACE, false);
|
|
77
|
-
const socket = dgram.createSocket('udp4');
|
|
78
|
-
const sessions = new Map();
|
|
79
|
-
const sessionsBySessionId = new Map();
|
|
80
|
-
const sessionsByRouteKey = new Map();
|
|
81
|
-
const resource = {
|
|
82
|
-
datagramsReceived: 0,
|
|
83
|
-
binaryDatagramsReceived: 0,
|
|
84
|
-
rendezvousJsonParseAttempts: 0,
|
|
85
|
-
binaryJsonParseAttempts: 0,
|
|
86
|
-
sessionLookupCount: 0,
|
|
87
|
-
linearSessionScanCount: 0,
|
|
88
|
-
authenticatedDatagrams: 0,
|
|
89
|
-
rejectedDatagrams: 0
|
|
90
|
-
};
|
|
76
|
+
const trace = isEnabled(env.LIVEDESK_UDP_TRACE, false);
|
|
77
|
+
const socket = dgram.createSocket('udp4');
|
|
78
|
+
const sessions = new Map();
|
|
79
|
+
const sessionsBySessionId = new Map();
|
|
80
|
+
const sessionsByRouteKey = new Map();
|
|
81
|
+
const resource = {
|
|
82
|
+
datagramsReceived: 0,
|
|
83
|
+
binaryDatagramsReceived: 0,
|
|
84
|
+
rendezvousJsonParseAttempts: 0,
|
|
85
|
+
binaryJsonParseAttempts: 0,
|
|
86
|
+
sessionLookupCount: 0,
|
|
87
|
+
linearSessionScanCount: 0,
|
|
88
|
+
authenticatedDatagrams: 0,
|
|
89
|
+
rejectedDatagrams: 0
|
|
90
|
+
};
|
|
91
91
|
let frameHandler = () => {};
|
|
92
|
-
let eventHandler = () => {};
|
|
93
|
-
let started = false;
|
|
94
|
-
let boundPort = requestedPort;
|
|
95
|
-
let sequence = crypto.randomInt(1, 0x7fffffff);
|
|
96
|
-
let rendezvousAddresses = new Set();
|
|
97
|
-
let rendezvousRefreshTimer = null;
|
|
98
|
-
let rendezvousRefreshPromise = null;
|
|
99
|
-
let frameExpiryTimer = null;
|
|
100
|
-
|
|
101
|
-
const peerProbeBurstDelaysMs = [0, 120, 360];
|
|
92
|
+
let eventHandler = () => {};
|
|
93
|
+
let started = false;
|
|
94
|
+
let boundPort = requestedPort;
|
|
95
|
+
let sequence = crypto.randomInt(1, 0x7fffffff);
|
|
96
|
+
let rendezvousAddresses = new Set();
|
|
97
|
+
let rendezvousRefreshTimer = null;
|
|
98
|
+
let rendezvousRefreshPromise = null;
|
|
99
|
+
let frameExpiryTimer = null;
|
|
100
|
+
const peerProbeBurstDelaysMs = [0, 120, 360];
|
|
102
101
|
|
|
103
102
|
function emit(type, event = {}) {
|
|
104
103
|
eventHandler(type, event);
|
|
105
104
|
if (trace) logEvent('udp', `${type} ${JSON.stringify({ deviceId: event.deviceId || '', state: event.state || '', reason: event.reason || '' })}`);
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
function nextSequence() {
|
|
109
|
-
sequence = (sequence + 1) >>> 0;
|
|
110
|
-
return sequence;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.clientEndpoint || session.peerEndpoint) {
|
|
114
|
-
if (!address || !session) return false;
|
|
115
|
-
let packet;
|
|
116
|
-
try {
|
|
117
|
-
packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
|
|
118
|
-
} catch (error) {
|
|
119
|
-
logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
107
|
+
function nextSequence() {
|
|
108
|
+
sequence = (sequence + 1) >>> 0;
|
|
109
|
+
return sequence;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.clientEndpoint || session.peerEndpoint) {
|
|
113
|
+
if (!address || !session) return false;
|
|
114
|
+
let packet;
|
|
115
|
+
try {
|
|
116
|
+
packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
122
121
|
socket.send(packet, address.port, address.address);
|
|
123
122
|
return true;
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
function sendRendezvousRegister(session) {
|
|
127
|
-
if (!rendezvousHost || rendezvousAddresses.size === 0 || !started) return false;
|
|
128
|
-
const message = encodeRendezvousMessage({
|
|
129
|
-
type: 'rendezvous.register',
|
|
125
|
+
function sendRendezvousRegister(session) {
|
|
126
|
+
if (!rendezvousHost || rendezvousAddresses.size === 0 || !started) return false;
|
|
127
|
+
const message = encodeRendezvousMessage({
|
|
128
|
+
type: 'rendezvous.register',
|
|
130
129
|
protocol: UDP_P2P_PROTOCOL,
|
|
131
130
|
roomId: session.sessionId,
|
|
132
|
-
role: 'hub',
|
|
133
|
-
token: session.rendezvousToken
|
|
134
|
-
});
|
|
135
|
-
let sent = false;
|
|
136
|
-
for (const address of rendezvousAddresses) {
|
|
137
|
-
try {
|
|
138
|
-
socket.send(message, rendezvousPort, address, error => {
|
|
139
|
-
if (error) logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
|
|
140
|
-
});
|
|
141
|
-
sent = true;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return sent;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function resolveRendezvousAddresses() {
|
|
150
|
-
if (!rendezvousHost) return false;
|
|
151
|
-
try {
|
|
152
|
-
const records = await lookup(rendezvousHost, { all: true, family: 4, verbatim: true });
|
|
153
|
-
const resolved = new Set(records.map(record => normalizedAddress(record.address)).filter(Boolean));
|
|
154
|
-
if (resolved.size === 0) throw new Error('no-ipv4-address');
|
|
155
|
-
rendezvousAddresses = resolved;
|
|
156
|
-
return true;
|
|
157
|
-
} catch (error) {
|
|
158
|
-
logWarn('udp', `UDP rendezvous host resolution failed: ${error?.message || error}`);
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function refreshRendezvousRegistrations(reason = 'periodic') {
|
|
164
|
-
if (!started || !rendezvousHost) return Promise.resolve(false);
|
|
165
|
-
if (rendezvousRefreshPromise) return rendezvousRefreshPromise;
|
|
166
|
-
rendezvousRefreshPromise = (async () => {
|
|
167
|
-
await resolveRendezvousAddresses();
|
|
168
|
-
if (!started) return false;
|
|
169
|
-
let refreshed = false;
|
|
170
|
-
for (const session of sessions.values()) {
|
|
171
|
-
refreshed = sendRendezvousRegister(session) || refreshed;
|
|
172
|
-
}
|
|
173
|
-
if (trace && refreshed) emit('UdpRendezvousRegistrationRefreshed', { reason });
|
|
174
|
-
return refreshed;
|
|
175
|
-
})().finally(() => {
|
|
176
|
-
rendezvousRefreshPromise = null;
|
|
177
|
-
});
|
|
178
|
-
return rendezvousRefreshPromise;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function startRendezvousRefreshTimer() {
|
|
182
|
-
if (!rendezvousHost || rendezvousRefreshTimer) return;
|
|
183
|
-
rendezvousRefreshTimer = setInterval(() => {
|
|
184
|
-
void refreshRendezvousRegistrations('periodic');
|
|
185
|
-
}, rendezvousRefreshMs);
|
|
186
|
-
rendezvousRefreshTimer.unref?.();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function stopRendezvousRefreshTimer() {
|
|
190
|
-
if (rendezvousRefreshTimer) clearInterval(rendezvousRefreshTimer);
|
|
191
|
-
rendezvousRefreshTimer = null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function startFrameExpiryTimer() {
|
|
195
|
-
if (frameExpiryTimer) return;
|
|
196
|
-
frameExpiryTimer = setInterval(() => {
|
|
197
|
-
const now = Date.now();
|
|
198
|
-
for (const session of sessions.values()) {
|
|
199
|
-
session.reassembler.expire(now);
|
|
200
|
-
}
|
|
201
|
-
}, Math.max(20, Math.min(250, frameTimeoutMs)));
|
|
202
|
-
frameExpiryTimer.unref?.();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function stopFrameExpiryTimer() {
|
|
206
|
-
if (frameExpiryTimer) clearInterval(frameExpiryTimer);
|
|
207
|
-
frameExpiryTimer = null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function isTrustedRendezvousSource(rinfo) {
|
|
211
|
-
return Number(rinfo?.port) === rendezvousPort
|
|
212
|
-
&& rendezvousAddresses.has(normalizedAddress(rinfo?.address));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function sessionForPacket(packet) {
|
|
216
|
-
resource.sessionLookupCount += 1;
|
|
217
|
-
const routeKey = peekUdpSessionRouteKey(packet);
|
|
218
|
-
return routeKey === null ? null : sessionsByRouteKey.get(routeKey) || null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function bindAuthenticatedClientEndpoint(session, rinfo) {
|
|
222
|
-
const endpoint = { address: rinfo.address, port: rinfo.port };
|
|
223
|
-
if (sameEndpoint(session.clientEndpoint, endpoint)) return false;
|
|
224
|
-
session.clientEndpoint = endpoint;
|
|
225
|
-
session.lastPeerAt = Date.now();
|
|
226
|
-
sendRendezvousRegister(session);
|
|
227
|
-
void refreshRendezvousRegistrations('authenticated-endpoint-change');
|
|
228
|
-
emit('UdpAuthenticatedEndpointObserved', {
|
|
229
|
-
deviceId: session.deviceId,
|
|
230
|
-
endpoint: `${endpoint.address}:${endpoint.port}`
|
|
231
|
-
});
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
|
|
131
|
+
role: 'hub',
|
|
132
|
+
token: session.rendezvousToken
|
|
133
|
+
});
|
|
134
|
+
let sent = false;
|
|
135
|
+
for (const address of rendezvousAddresses) {
|
|
136
|
+
try {
|
|
137
|
+
socket.send(message, rendezvousPort, address, error => {
|
|
138
|
+
if (error) logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
|
|
139
|
+
});
|
|
140
|
+
sent = true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return sent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function resolveRendezvousAddresses() {
|
|
149
|
+
if (!rendezvousHost) return false;
|
|
150
|
+
try {
|
|
151
|
+
const records = await lookup(rendezvousHost, { all: true, family: 4, verbatim: true });
|
|
152
|
+
const resolved = new Set(records.map(record => normalizedAddress(record.address)).filter(Boolean));
|
|
153
|
+
if (resolved.size === 0) throw new Error('no-ipv4-address');
|
|
154
|
+
rendezvousAddresses = resolved;
|
|
155
|
+
return true;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logWarn('udp', `UDP rendezvous host resolution failed: ${error?.message || error}`);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function refreshRendezvousRegistrations(reason = 'periodic') {
|
|
163
|
+
if (!started || !rendezvousHost) return Promise.resolve(false);
|
|
164
|
+
if (rendezvousRefreshPromise) return rendezvousRefreshPromise;
|
|
165
|
+
rendezvousRefreshPromise = (async () => {
|
|
166
|
+
await resolveRendezvousAddresses();
|
|
167
|
+
if (!started) return false;
|
|
168
|
+
let refreshed = false;
|
|
169
|
+
for (const session of sessions.values()) {
|
|
170
|
+
refreshed = sendRendezvousRegister(session) || refreshed;
|
|
171
|
+
}
|
|
172
|
+
if (trace && refreshed) emit('UdpRendezvousRegistrationRefreshed', { reason });
|
|
173
|
+
return refreshed;
|
|
174
|
+
})().finally(() => {
|
|
175
|
+
rendezvousRefreshPromise = null;
|
|
176
|
+
});
|
|
177
|
+
return rendezvousRefreshPromise;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function startRendezvousRefreshTimer() {
|
|
181
|
+
if (!rendezvousHost || rendezvousRefreshTimer) return;
|
|
182
|
+
rendezvousRefreshTimer = setInterval(() => {
|
|
183
|
+
void refreshRendezvousRegistrations('periodic');
|
|
184
|
+
}, rendezvousRefreshMs);
|
|
185
|
+
rendezvousRefreshTimer.unref?.();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function stopRendezvousRefreshTimer() {
|
|
189
|
+
if (rendezvousRefreshTimer) clearInterval(rendezvousRefreshTimer);
|
|
190
|
+
rendezvousRefreshTimer = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function startFrameExpiryTimer() {
|
|
194
|
+
if (frameExpiryTimer) return;
|
|
195
|
+
frameExpiryTimer = setInterval(() => {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
for (const session of sessions.values()) {
|
|
198
|
+
session.reassembler.expire(now);
|
|
199
|
+
}
|
|
200
|
+
}, Math.max(20, Math.min(250, frameTimeoutMs)));
|
|
201
|
+
frameExpiryTimer.unref?.();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function stopFrameExpiryTimer() {
|
|
205
|
+
if (frameExpiryTimer) clearInterval(frameExpiryTimer);
|
|
206
|
+
frameExpiryTimer = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isTrustedRendezvousSource(rinfo) {
|
|
210
|
+
return Number(rinfo?.port) === rendezvousPort
|
|
211
|
+
&& rendezvousAddresses.has(normalizedAddress(rinfo?.address));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sessionForPacket(packet) {
|
|
215
|
+
resource.sessionLookupCount += 1;
|
|
216
|
+
const routeKey = peekUdpSessionRouteKey(packet);
|
|
217
|
+
return routeKey === null ? null : sessionsByRouteKey.get(routeKey) || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function bindAuthenticatedClientEndpoint(session, rinfo) {
|
|
221
|
+
const endpoint = { address: rinfo.address, port: rinfo.port };
|
|
222
|
+
if (sameEndpoint(session.clientEndpoint, endpoint)) return false;
|
|
223
|
+
session.clientEndpoint = endpoint;
|
|
224
|
+
session.lastPeerAt = Date.now();
|
|
225
|
+
sendRendezvousRegister(session);
|
|
226
|
+
void refreshRendezvousRegistrations('authenticated-endpoint-change');
|
|
227
|
+
emit('UdpAuthenticatedEndpointObserved', {
|
|
228
|
+
deviceId: session.deviceId,
|
|
229
|
+
endpoint: `${endpoint.address}:${endpoint.port}`
|
|
230
|
+
});
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
235
234
|
function clearPeerProbeTimers(session) {
|
|
236
235
|
if (!session?.peerProbeTimers) return;
|
|
237
236
|
for (const timer of session.peerProbeTimers) clearTimeout(timer);
|
|
@@ -242,7 +241,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
242
241
|
clearPeerProbeTimers(session);
|
|
243
242
|
for (const delayMs of peerProbeBurstDelaysMs) {
|
|
244
243
|
const sendProbe = () => {
|
|
245
|
-
if (sessions.get(session.deviceId) !== session
|
|
244
|
+
if (sessions.get(session.deviceId) !== session
|
|
246
245
|
|| !sameEndpoint(session.peerEndpoint, endpoint)) return;
|
|
247
246
|
sendPacket(session, 'probe', { punch: true }, Buffer.alloc(0), endpoint);
|
|
248
247
|
};
|
|
@@ -259,78 +258,78 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
259
258
|
}
|
|
260
259
|
}
|
|
261
260
|
|
|
262
|
-
function onRendezvousPeer(message, rinfo) {
|
|
263
|
-
const session = sessionsBySessionId.get(safeText(message.roomId, 160));
|
|
264
|
-
if (!session || message.protocol !== UDP_P2P_PROTOCOL || !isTrustedRendezvousSource(rinfo)) return;
|
|
265
|
-
const address = safeText(message.host, 128);
|
|
266
|
-
const port = Number(message.port);
|
|
267
|
-
if (!address || !Number.isInteger(port) || port < 1 || port > 65535) return;
|
|
268
|
-
const observedAddress = normalizedAddress(message.observedHost);
|
|
269
|
-
const observedPort = Number(message.observedPort);
|
|
270
|
-
if (observedAddress
|
|
271
|
-
&& Number.isInteger(observedPort)
|
|
272
|
-
&& observedPort >= 1
|
|
273
|
-
&& observedPort <= 65535) {
|
|
274
|
-
session.hubRendezvousEndpoint = {
|
|
275
|
-
address: observedAddress,
|
|
276
|
-
port: observedPort
|
|
277
|
-
};
|
|
278
|
-
session.hubRendezvousObservedAt = Date.now();
|
|
279
|
-
}
|
|
261
|
+
function onRendezvousPeer(message, rinfo) {
|
|
262
|
+
const session = sessionsBySessionId.get(safeText(message.roomId, 160));
|
|
263
|
+
if (!session || message.protocol !== UDP_P2P_PROTOCOL || !isTrustedRendezvousSource(rinfo)) return;
|
|
264
|
+
const address = safeText(message.host, 128);
|
|
265
|
+
const port = Number(message.port);
|
|
266
|
+
if (!address || !Number.isInteger(port) || port < 1 || port > 65535) return;
|
|
267
|
+
const observedAddress = normalizedAddress(message.observedHost);
|
|
268
|
+
const observedPort = Number(message.observedPort);
|
|
269
|
+
if (observedAddress
|
|
270
|
+
&& Number.isInteger(observedPort)
|
|
271
|
+
&& observedPort >= 1
|
|
272
|
+
&& observedPort <= 65535) {
|
|
273
|
+
session.hubRendezvousEndpoint = {
|
|
274
|
+
address: observedAddress,
|
|
275
|
+
port: observedPort
|
|
276
|
+
};
|
|
277
|
+
session.hubRendezvousObservedAt = Date.now();
|
|
278
|
+
}
|
|
280
279
|
session.peerEndpoint = { address, port };
|
|
281
280
|
session.lastPeerAt = Date.now();
|
|
282
281
|
schedulePeerProbeBurst(session, session.peerEndpoint);
|
|
283
282
|
emit('UdpPeerEndpointObserved', { deviceId: session.deviceId, endpoint: `${address}:${port}` });
|
|
284
283
|
}
|
|
285
284
|
|
|
286
|
-
socket.on('message', (payload, rinfo) => {
|
|
287
|
-
resource.datagramsReceived += 1;
|
|
288
|
-
if (!isUdpBinaryDatagram(payload)) {
|
|
289
|
-
// Rendezvous messages are the only JSON datagrams. Binary packets
|
|
290
|
-
// never pay a speculative JSON parse/exception before routing.
|
|
291
|
-
if (payload?.[0] !== 0x7b) {
|
|
292
|
-
resource.rejectedDatagrams += 1;
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
resource.rendezvousJsonParseAttempts += 1;
|
|
296
|
-
const rendezvous = decodeRendezvousMessage(payload);
|
|
297
|
-
if (rendezvous?.type === 'rendezvous.peer') {
|
|
298
|
-
onRendezvousPeer(rendezvous, rinfo);
|
|
299
|
-
} else {
|
|
300
|
-
resource.rejectedDatagrams += 1;
|
|
301
|
-
}
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
resource.binaryDatagramsReceived += 1;
|
|
305
|
-
const session = sessionForPacket(payload);
|
|
306
|
-
if (!session) {
|
|
307
|
-
resource.rejectedDatagrams += 1;
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
session.resource.binaryDatagramsReceived += 1;
|
|
311
|
-
session.resource.decryptAttempts += 1;
|
|
312
|
-
const packet = decodeUdpPacket(payload, session.key, {
|
|
313
|
-
id: session.sessionId,
|
|
314
|
-
bytes: session.sessionIdBytes
|
|
315
|
-
});
|
|
316
|
-
if (!packet || packet.sessionId !== session.sessionId || !session.replay.accept(packet.sequence)) {
|
|
317
|
-
session.resource.rejectedDatagrams += 1;
|
|
318
|
-
resource.rejectedDatagrams += 1;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
resource.authenticatedDatagrams += 1;
|
|
322
|
-
session.resource.authenticatedDatagrams += 1;
|
|
323
|
-
bindAuthenticatedClientEndpoint(session, rinfo);
|
|
324
|
-
session.lastPacketAt = Date.now();
|
|
325
|
-
if (packet.type === 'probe') {
|
|
326
|
-
sendPacket(session, 'probe-ack', { observedHost: rinfo.address, observedPort: rinfo.port });
|
|
285
|
+
socket.on('message', (payload, rinfo) => {
|
|
286
|
+
resource.datagramsReceived += 1;
|
|
287
|
+
if (!isUdpBinaryDatagram(payload)) {
|
|
288
|
+
// Rendezvous messages are the only JSON datagrams. Binary packets
|
|
289
|
+
// never pay a speculative JSON parse/exception before routing.
|
|
290
|
+
if (payload?.[0] !== 0x7b) {
|
|
291
|
+
resource.rejectedDatagrams += 1;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
resource.rendezvousJsonParseAttempts += 1;
|
|
295
|
+
const rendezvous = decodeRendezvousMessage(payload);
|
|
296
|
+
if (rendezvous?.type === 'rendezvous.peer') {
|
|
297
|
+
onRendezvousPeer(rendezvous, rinfo);
|
|
298
|
+
} else {
|
|
299
|
+
resource.rejectedDatagrams += 1;
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
resource.binaryDatagramsReceived += 1;
|
|
304
|
+
const session = sessionForPacket(payload);
|
|
305
|
+
if (!session) {
|
|
306
|
+
resource.rejectedDatagrams += 1;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
session.resource.binaryDatagramsReceived += 1;
|
|
310
|
+
session.resource.decryptAttempts += 1;
|
|
311
|
+
const packet = decodeUdpPacket(payload, session.key, {
|
|
312
|
+
id: session.sessionId,
|
|
313
|
+
bytes: session.sessionIdBytes
|
|
314
|
+
});
|
|
315
|
+
if (!packet || packet.sessionId !== session.sessionId || !session.replay.accept(packet.sequence)) {
|
|
316
|
+
session.resource.rejectedDatagrams += 1;
|
|
317
|
+
resource.rejectedDatagrams += 1;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
resource.authenticatedDatagrams += 1;
|
|
321
|
+
session.resource.authenticatedDatagrams += 1;
|
|
322
|
+
bindAuthenticatedClientEndpoint(session, rinfo);
|
|
323
|
+
session.lastPacketAt = Date.now();
|
|
324
|
+
if (packet.type === 'probe') {
|
|
325
|
+
sendPacket(session, 'probe-ack', { observedHost: rinfo.address, observedPort: rinfo.port });
|
|
327
326
|
emit('UdpProbeReceived', { deviceId: session.deviceId });
|
|
328
327
|
return;
|
|
329
328
|
}
|
|
330
329
|
if (packet.type === 'ready') {
|
|
331
330
|
clearPeerProbeTimers(session);
|
|
332
|
-
session.ready = true;
|
|
333
|
-
sendPacket(session, 'pong', { state: 'ready' });
|
|
331
|
+
session.ready = true;
|
|
332
|
+
sendPacket(session, 'pong', { state: 'ready' });
|
|
334
333
|
session.sendControl?.({
|
|
335
334
|
type: 'udp.session.ready',
|
|
336
335
|
protocol: UDP_P2P_PROTOCOL,
|
|
@@ -347,53 +346,53 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
347
346
|
return;
|
|
348
347
|
}
|
|
349
348
|
if (packet.type !== 'frame') return;
|
|
350
|
-
const complete = session.reassembler.ingest(packet);
|
|
351
|
-
if (!complete) return;
|
|
352
|
-
session.framesReceived += 1;
|
|
353
|
-
const reassembly = session.reassembler.getStats();
|
|
354
|
-
const handled = frameHandler({
|
|
355
|
-
deviceId: session.deviceId,
|
|
356
|
-
sessionId: session.sessionId,
|
|
357
|
-
header: {
|
|
358
|
-
...complete.header,
|
|
359
|
-
hubIngressDropped: Math.max(
|
|
360
|
-
Number(complete.header?.hubIngressDropped || 0) || 0,
|
|
361
|
-
reassembly.droppedFrames
|
|
362
|
-
),
|
|
363
|
-
udpIncompleteFrames: reassembly.droppedFrames
|
|
364
|
-
},
|
|
365
|
-
payload: complete.payload,
|
|
366
|
-
transport: 'udp-p2p'
|
|
367
|
-
});
|
|
368
|
-
const accepted = handled === true || handled?.accepted === true;
|
|
369
|
-
const proofId = safeText(complete.header?.frameTransportProofId, 96);
|
|
370
|
-
if (proofId) {
|
|
371
|
-
if (handled?.suppressProofResponse === true) return;
|
|
372
|
-
const proofAccepted = accepted && handled?.proofAccepted === true;
|
|
373
|
-
sendPacket(session, 'pong', {
|
|
374
|
-
state: proofAccepted ? 'frame-proof-ack' : 'frame-proof-reject',
|
|
375
|
-
proofId,
|
|
376
|
-
protocol: 'livedesk.frame-transport-proof.v1',
|
|
377
|
-
streamId: safeText(complete.header?.streamId, 128),
|
|
378
|
-
commandId: safeText(complete.header?.commandId, 128),
|
|
379
|
-
streamPurpose: safeText(complete.header?.streamPurpose, 24),
|
|
380
|
-
captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
|
|
381
|
-
monitorIndex: Number(complete.header?.monitorIndex ?? -1),
|
|
382
|
-
error: proofAccepted ? '' : safeText(handled?.error || 'frame-proof-rejected', 160)
|
|
383
|
-
});
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (accepted) {
|
|
387
|
-
sendPacket(session, 'pong', {
|
|
388
|
-
state: 'frame-ack',
|
|
389
|
-
frameSeq: Number(complete.header?.frameSeq || 0) || 0,
|
|
390
|
-
streamId: safeText(complete.header?.streamId, 128),
|
|
391
|
-
commandId: safeText(complete.header?.commandId, 128),
|
|
392
|
-
streamPurpose: safeText(complete.header?.streamPurpose, 24),
|
|
393
|
-
captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
|
|
394
|
-
monitorIndex: Number(complete.header?.monitorIndex ?? -1)
|
|
395
|
-
});
|
|
396
|
-
}
|
|
349
|
+
const complete = session.reassembler.ingest(packet);
|
|
350
|
+
if (!complete) return;
|
|
351
|
+
session.framesReceived += 1;
|
|
352
|
+
const reassembly = session.reassembler.getStats();
|
|
353
|
+
const handled = frameHandler({
|
|
354
|
+
deviceId: session.deviceId,
|
|
355
|
+
sessionId: session.sessionId,
|
|
356
|
+
header: {
|
|
357
|
+
...complete.header,
|
|
358
|
+
hubIngressDropped: Math.max(
|
|
359
|
+
Number(complete.header?.hubIngressDropped || 0) || 0,
|
|
360
|
+
reassembly.droppedFrames
|
|
361
|
+
),
|
|
362
|
+
udpIncompleteFrames: reassembly.droppedFrames
|
|
363
|
+
},
|
|
364
|
+
payload: complete.payload,
|
|
365
|
+
transport: 'udp-p2p'
|
|
366
|
+
});
|
|
367
|
+
const accepted = handled === true || handled?.accepted === true;
|
|
368
|
+
const proofId = safeText(complete.header?.frameTransportProofId, 96);
|
|
369
|
+
if (proofId) {
|
|
370
|
+
if (handled?.suppressProofResponse === true) return;
|
|
371
|
+
const proofAccepted = accepted && handled?.proofAccepted === true;
|
|
372
|
+
sendPacket(session, 'pong', {
|
|
373
|
+
state: proofAccepted ? 'frame-proof-ack' : 'frame-proof-reject',
|
|
374
|
+
proofId,
|
|
375
|
+
protocol: 'livedesk.frame-transport-proof.v1',
|
|
376
|
+
streamId: safeText(complete.header?.streamId, 128),
|
|
377
|
+
commandId: safeText(complete.header?.commandId, 128),
|
|
378
|
+
streamPurpose: safeText(complete.header?.streamPurpose, 24),
|
|
379
|
+
captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
|
|
380
|
+
monitorIndex: Number(complete.header?.monitorIndex ?? -1),
|
|
381
|
+
error: proofAccepted ? '' : safeText(handled?.error || 'frame-proof-rejected', 160)
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (accepted) {
|
|
386
|
+
sendPacket(session, 'pong', {
|
|
387
|
+
state: 'frame-ack',
|
|
388
|
+
frameSeq: Number(complete.header?.frameSeq || 0) || 0,
|
|
389
|
+
streamId: safeText(complete.header?.streamId, 128),
|
|
390
|
+
commandId: safeText(complete.header?.commandId, 128),
|
|
391
|
+
streamPurpose: safeText(complete.header?.streamPurpose, 24),
|
|
392
|
+
captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
|
|
393
|
+
monitorIndex: Number(complete.header?.monitorIndex ?? -1)
|
|
394
|
+
});
|
|
395
|
+
}
|
|
397
396
|
});
|
|
398
397
|
socket.on('error', error => logWarn('udp', `UDP socket error: ${error?.message || error}`));
|
|
399
398
|
|
|
@@ -405,93 +404,74 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
405
404
|
socket.once('error', onError);
|
|
406
405
|
socket.once('listening', onListening);
|
|
407
406
|
socket.bind(requestedPort, bindHost);
|
|
408
|
-
});
|
|
409
|
-
started = true;
|
|
410
|
-
boundPort = socket.address().port;
|
|
411
|
-
try {
|
|
412
|
-
socket.setRecvBufferSize(8 * 1024 * 1024);
|
|
413
|
-
socket.setSendBufferSize(1024 * 1024);
|
|
414
|
-
} catch (error) {
|
|
415
|
-
logWarn('udp', `UDP socket buffer tuning was unavailable: ${error?.message || error}`);
|
|
416
|
-
}
|
|
417
|
-
logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
|
|
418
|
-
void refreshRendezvousRegistrations('startup');
|
|
419
|
-
startRendezvousRefreshTimer();
|
|
420
|
-
startFrameExpiryTimer();
|
|
421
|
-
return getStatus();
|
|
407
|
+
});
|
|
408
|
+
started = true;
|
|
409
|
+
boundPort = socket.address().port;
|
|
410
|
+
try {
|
|
411
|
+
socket.setRecvBufferSize(8 * 1024 * 1024);
|
|
412
|
+
socket.setSendBufferSize(1024 * 1024);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
logWarn('udp', `UDP socket buffer tuning was unavailable: ${error?.message || error}`);
|
|
415
|
+
}
|
|
416
|
+
logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
|
|
417
|
+
void refreshRendezvousRegistrations('startup');
|
|
418
|
+
startRendezvousRefreshTimer();
|
|
419
|
+
startFrameExpiryTimer();
|
|
420
|
+
return getStatus();
|
|
422
421
|
}
|
|
423
422
|
|
|
424
|
-
function registerDevice({ deviceId, sessionId: tcpSessionId, sendControl, capabilities } = {}) {
|
|
425
|
-
if (!enabled || capabilities?.udpP2p !== true || !started) return null;
|
|
426
|
-
const id = safeText(deviceId, 160);
|
|
427
|
-
if (!id) return null;
|
|
428
|
-
const previous = sessions.get(id);
|
|
423
|
+
function registerDevice({ deviceId, sessionId: tcpSessionId, sendControl, capabilities } = {}) {
|
|
424
|
+
if (!enabled || capabilities?.udpP2p !== true || !started) return null;
|
|
425
|
+
const id = safeText(deviceId, 160);
|
|
426
|
+
if (!id) return null;
|
|
427
|
+
const previous = sessions.get(id);
|
|
429
428
|
previous?.sendControl?.({ type: 'udp.session.close', reason: 'replaced' });
|
|
430
429
|
if (previous) {
|
|
431
|
-
clearPeerProbeTimers(previous);
|
|
432
|
-
sessions.delete(id);
|
|
433
|
-
sessionsBySessionId.delete(previous.sessionId);
|
|
434
|
-
sessionsByRouteKey.delete(previous.routeKey);
|
|
435
|
-
previous.reassembler.clear();
|
|
436
|
-
}
|
|
437
|
-
const key = createUdpSessionKey();
|
|
438
|
-
let udpSessionId;
|
|
439
|
-
let routeKey;
|
|
440
|
-
do {
|
|
441
|
-
udpSessionId = crypto.randomUUID();
|
|
442
|
-
routeKey = udpSessionRouteKey(udpSessionId);
|
|
443
|
-
} while (sessionsByRouteKey.has(routeKey));
|
|
444
|
-
const
|
|
445
|
-
? rendezvousProofIssuer({ roomId: udpSessionId, deviceId: id, role: 'hub', ttlMs: 60_000 })
|
|
446
|
-
: null;
|
|
447
|
-
const clientProof = typeof rendezvousProofIssuer === 'function'
|
|
448
|
-
? rendezvousProofIssuer({ roomId: udpSessionId, deviceId: id, role: 'client', ttlMs: 60_000 })
|
|
449
|
-
: null;
|
|
450
|
-
const signedHubRendezvousProof = safeText(hubProof?.proof, 2048);
|
|
451
|
-
const signedClientRendezvousProof = safeText(clientProof?.proof, 2048);
|
|
452
|
-
const allowLegacyUnsignedProofForTests = env.LIVEDESK_TEST_MODE === '1'
|
|
453
|
-
|| env.LIVEDESK_TEST_ALLOW_LEGACY_UDP_RENDEZVOUS === '1';
|
|
454
|
-
const legacyRendezvousToken = allowLegacyUnsignedProofForTests
|
|
455
|
-
&& (!signedHubRendezvousProof || !signedClientRendezvousProof)
|
|
456
|
-
? crypto.randomBytes(16).toString('hex')
|
|
457
|
-
: '';
|
|
458
|
-
const proofExpiries = [hubProof?.payload?.expiresAt, clientProof?.payload?.expiresAt]
|
|
459
|
-
.map(Number)
|
|
460
|
-
.filter(Number.isSafeInteger);
|
|
461
|
-
const session = {
|
|
430
|
+
clearPeerProbeTimers(previous);
|
|
431
|
+
sessions.delete(id);
|
|
432
|
+
sessionsBySessionId.delete(previous.sessionId);
|
|
433
|
+
sessionsByRouteKey.delete(previous.routeKey);
|
|
434
|
+
previous.reassembler.clear();
|
|
435
|
+
}
|
|
436
|
+
const key = createUdpSessionKey();
|
|
437
|
+
let udpSessionId;
|
|
438
|
+
let routeKey;
|
|
439
|
+
do {
|
|
440
|
+
udpSessionId = crypto.randomUUID();
|
|
441
|
+
routeKey = udpSessionRouteKey(udpSessionId);
|
|
442
|
+
} while (sessionsByRouteKey.has(routeKey));
|
|
443
|
+
const session = {
|
|
462
444
|
deviceId: id,
|
|
463
445
|
tcpSessionId: safeText(tcpSessionId, 160),
|
|
464
|
-
sessionId: udpSessionId,
|
|
465
|
-
sessionIdBytes: Buffer.from(udpSessionId, 'utf8'),
|
|
466
|
-
routeKey,
|
|
446
|
+
sessionId: udpSessionId,
|
|
447
|
+
sessionIdBytes: Buffer.from(udpSessionId, 'utf8'),
|
|
448
|
+
routeKey,
|
|
467
449
|
key,
|
|
468
450
|
keyBase64: key.toString('base64'),
|
|
469
|
-
rendezvousToken:
|
|
470
|
-
clientRendezvousToken: signedClientRendezvousProof || legacyRendezvousToken,
|
|
471
|
-
rendezvousProofExpiresAt: proofExpiries.length === 2 ? Math.min(...proofExpiries) : 0,
|
|
451
|
+
rendezvousToken: crypto.randomBytes(16).toString('hex'),
|
|
472
452
|
sendControl,
|
|
473
|
-
clientEndpoint: null,
|
|
474
|
-
peerEndpoint: null,
|
|
475
|
-
hubRendezvousEndpoint: null,
|
|
476
|
-
hubRendezvousObservedAt: 0,
|
|
477
|
-
replay: new UdpReplayWindow(),
|
|
478
|
-
reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
|
|
479
|
-
resource: {
|
|
480
|
-
binaryDatagramsReceived: 0,
|
|
481
|
-
decryptAttempts: 0,
|
|
482
|
-
authenticatedDatagrams: 0,
|
|
483
|
-
rejectedDatagrams: 0
|
|
484
|
-
},
|
|
453
|
+
clientEndpoint: null,
|
|
454
|
+
peerEndpoint: null,
|
|
455
|
+
hubRendezvousEndpoint: null,
|
|
456
|
+
hubRendezvousObservedAt: 0,
|
|
457
|
+
replay: new UdpReplayWindow(),
|
|
458
|
+
reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
|
|
459
|
+
resource: {
|
|
460
|
+
binaryDatagramsReceived: 0,
|
|
461
|
+
decryptAttempts: 0,
|
|
462
|
+
authenticatedDatagrams: 0,
|
|
463
|
+
rejectedDatagrams: 0
|
|
464
|
+
},
|
|
485
465
|
ready: false,
|
|
486
466
|
peerProbeTimers: new Set(),
|
|
487
467
|
createdAt: Date.now(),
|
|
488
468
|
lastPacketAt: 0,
|
|
489
469
|
framesReceived: 0
|
|
490
|
-
};
|
|
491
|
-
sessions.set(id, session);
|
|
492
|
-
sessionsBySessionId.set(session.sessionId, session);
|
|
493
|
-
sessionsByRouteKey.set(session.routeKey, session);
|
|
494
|
-
sendControl?.({
|
|
470
|
+
};
|
|
471
|
+
sessions.set(id, session);
|
|
472
|
+
sessionsBySessionId.set(session.sessionId, session);
|
|
473
|
+
sessionsByRouteKey.set(session.routeKey, session);
|
|
474
|
+
sendControl?.({
|
|
495
475
|
type: 'udp.session.offer',
|
|
496
476
|
protocol: UDP_P2P_PROTOCOL,
|
|
497
477
|
sessionId: session.sessionId,
|
|
@@ -500,206 +480,203 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
500
480
|
port: boundPort,
|
|
501
481
|
expiresInMs: punchTimeoutMs + 15_000,
|
|
502
482
|
punchTimeoutMs,
|
|
503
|
-
maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
|
|
504
|
-
frameChunkHeader: UDP_FRAME_CHUNK_PROTOCOL,
|
|
505
|
-
frameTimeoutMs,
|
|
483
|
+
maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
|
|
484
|
+
frameChunkHeader: UDP_FRAME_CHUNK_PROTOCOL,
|
|
485
|
+
frameTimeoutMs,
|
|
506
486
|
maxPendingFrames,
|
|
507
|
-
rendezvous: rendezvousHost
|
|
508
|
-
? { host: rendezvousHost, port: rendezvousPort, roomId: session.sessionId, token: session.
|
|
509
|
-
: null
|
|
487
|
+
rendezvous: rendezvousHost
|
|
488
|
+
? { host: rendezvousHost, port: rendezvousPort, roomId: session.sessionId, token: session.rendezvousToken }
|
|
489
|
+
: null
|
|
510
490
|
});
|
|
511
491
|
sendRendezvousRegister(session);
|
|
512
492
|
emit('UdpSessionCreated', { deviceId: id, state: 'udp-session-created' });
|
|
513
493
|
return { sessionId: session.sessionId, ready: false };
|
|
514
494
|
}
|
|
515
495
|
|
|
516
|
-
function unregisterDevice(deviceId, tcpSessionId = '') {
|
|
496
|
+
function unregisterDevice(deviceId, tcpSessionId = '') {
|
|
517
497
|
const id = safeText(deviceId, 160);
|
|
518
498
|
const session = sessions.get(id);
|
|
519
|
-
if (!session || (tcpSessionId && session.tcpSessionId !== tcpSessionId)) return;
|
|
499
|
+
if (!session || (tcpSessionId && session.tcpSessionId !== tcpSessionId)) return;
|
|
520
500
|
clearPeerProbeTimers(session);
|
|
521
501
|
sessions.delete(id);
|
|
522
|
-
sessionsBySessionId.delete(session.sessionId);
|
|
523
|
-
sessionsByRouteKey.delete(session.routeKey);
|
|
524
|
-
session.reassembler.clear();
|
|
525
|
-
emit('UdpSessionClosed', { deviceId: id, reason: 'tcp-session-closed' });
|
|
502
|
+
sessionsBySessionId.delete(session.sessionId);
|
|
503
|
+
sessionsByRouteKey.delete(session.routeKey);
|
|
504
|
+
session.reassembler.clear();
|
|
505
|
+
emit('UdpSessionClosed', { deviceId: id, reason: 'tcp-session-closed' });
|
|
526
506
|
}
|
|
527
507
|
|
|
528
|
-
function getDeviceStatus(deviceId) {
|
|
508
|
+
function getDeviceStatus(deviceId) {
|
|
529
509
|
const session = sessions.get(safeText(deviceId, 160));
|
|
530
510
|
if (!session) return { enabled, state: enabled ? 'disabled' : 'tcp-only', ready: false };
|
|
531
511
|
return {
|
|
532
512
|
enabled,
|
|
533
513
|
preferred: preferP2p,
|
|
534
|
-
state: session.ready ? 'udp-ready' : 'udp-session-created',
|
|
535
|
-
ready: session.ready,
|
|
514
|
+
state: session.ready ? 'udp-ready' : 'udp-session-created',
|
|
515
|
+
ready: session.ready,
|
|
536
516
|
sessionId: session.sessionId,
|
|
537
|
-
lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
|
|
538
|
-
framesReceived: session.framesReceived,
|
|
539
|
-
resource: {
|
|
540
|
-
...session.resource,
|
|
541
|
-
binaryJsonParseAttempts: 0,
|
|
542
|
-
sessionLookup: 'map-o1',
|
|
543
|
-
linearSessionScanCount: 0
|
|
544
|
-
},
|
|
545
|
-
...session.reassembler.getStats()
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
async function refreshDeviceDiagnosticNetwork(deviceId, { timeoutMs = 3_000 } = {}) {
|
|
550
|
-
const session = sessions.get(safeText(deviceId, 160));
|
|
551
|
-
if (!session || !started || !rendezvousHost) return false;
|
|
552
|
-
const requestedAt = Date.now();
|
|
553
|
-
await resolveRendezvousAddresses();
|
|
554
|
-
if (!sendRendezvousRegister(session)) return false;
|
|
555
|
-
const deadline = requestedAt + numberEnv(timeoutMs, 250, 5_000, 3_000);
|
|
556
|
-
while (Date.now() < deadline) {
|
|
557
|
-
if (session.hubRendezvousObservedAt >= requestedAt) return true;
|
|
558
|
-
await new Promise(resolve => {
|
|
559
|
-
const timer = setTimeout(resolve, 40);
|
|
560
|
-
timer.unref?.();
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
return false;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function getDeviceDiagnosticNetwork(deviceId, { salt = '' } = {}) {
|
|
567
|
-
const session = sessions.get(safeText(deviceId, 160));
|
|
568
|
-
const clientAddress = normalizedAddress(session?.clientEndpoint?.address);
|
|
569
|
-
const normalizedHubAddress = normalizedAddress(session?.hubRendezvousEndpoint?.address);
|
|
570
|
-
const hubAddressObservedAt = session?.hubRendezvousObservedAt
|
|
571
|
-
? new Date(session.hubRendezvousObservedAt).toISOString()
|
|
572
|
-
: '';
|
|
573
|
-
const digest = value => value
|
|
574
|
-
? crypto.createHash('sha256')
|
|
575
|
-
.update(`${safeText(salt, 160)}:${value}`)
|
|
576
|
-
.digest('hex')
|
|
577
|
-
.slice(0, 16)
|
|
578
|
-
: '';
|
|
579
|
-
if (!session?.ready || !clientAddress) {
|
|
580
|
-
return {
|
|
581
|
-
evidence: 'authenticated-udp-endpoint-unavailable',
|
|
582
|
-
differentPublicEgress: null,
|
|
583
|
-
clientAddressHash: '',
|
|
584
|
-
hubAddressHash: digest(normalizedHubAddress),
|
|
585
|
-
hubAddressObservedAt,
|
|
586
|
-
hubEndpointSource: normalizedHubAddress
|
|
587
|
-
? 'trusted-rendezvous-observation'
|
|
588
|
-
: 'unavailable'
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
const clientAddressPublic = isPublicIPv4(clientAddress);
|
|
592
|
-
const hubAddressPublic = isPublicIPv4(normalizedHubAddress);
|
|
593
|
-
return {
|
|
594
|
-
evidence: normalizedHubAddress
|
|
595
|
-
? 'authenticated-udp-endpoint-rendezvous-observed'
|
|
596
|
-
: 'authenticated-udp-endpoint-hub-egress-unavailable',
|
|
597
|
-
differentPublicEgress: clientAddressPublic && hubAddressPublic
|
|
598
|
-
? clientAddress !== normalizedHubAddress
|
|
599
|
-
: null,
|
|
600
|
-
clientAddressHash: digest(clientAddress),
|
|
601
|
-
hubAddressHash: digest(normalizedHubAddress),
|
|
602
|
-
clientAddressPublic,
|
|
603
|
-
hubAddressPublic,
|
|
604
|
-
endpointAuthenticated: true,
|
|
605
|
-
udpReady: session.ready === true,
|
|
606
|
-
observedAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
|
|
607
|
-
hubAddressObservedAt,
|
|
608
|
-
hubEndpointSource: normalizedHubAddress
|
|
609
|
-
? 'trusted-rendezvous-observation'
|
|
610
|
-
: 'unavailable'
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
async function close() {
|
|
615
|
-
stopRendezvousRefreshTimer();
|
|
616
|
-
stopFrameExpiryTimer();
|
|
517
|
+
lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
|
|
518
|
+
framesReceived: session.framesReceived,
|
|
519
|
+
resource: {
|
|
520
|
+
...session.resource,
|
|
521
|
+
binaryJsonParseAttempts: 0,
|
|
522
|
+
sessionLookup: 'map-o1',
|
|
523
|
+
linearSessionScanCount: 0
|
|
524
|
+
},
|
|
525
|
+
...session.reassembler.getStats()
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function refreshDeviceDiagnosticNetwork(deviceId, { timeoutMs = 3_000 } = {}) {
|
|
530
|
+
const session = sessions.get(safeText(deviceId, 160));
|
|
531
|
+
if (!session || !started || !rendezvousHost) return false;
|
|
532
|
+
const requestedAt = Date.now();
|
|
533
|
+
await resolveRendezvousAddresses();
|
|
534
|
+
if (!sendRendezvousRegister(session)) return false;
|
|
535
|
+
const deadline = requestedAt + numberEnv(timeoutMs, 250, 5_000, 3_000);
|
|
536
|
+
while (Date.now() < deadline) {
|
|
537
|
+
if (session.hubRendezvousObservedAt >= requestedAt) return true;
|
|
538
|
+
await new Promise(resolve => {
|
|
539
|
+
const timer = setTimeout(resolve, 40);
|
|
540
|
+
timer.unref?.();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function getDeviceDiagnosticNetwork(deviceId, { salt = '' } = {}) {
|
|
547
|
+
const session = sessions.get(safeText(deviceId, 160));
|
|
548
|
+
const clientAddress = normalizedAddress(session?.clientEndpoint?.address);
|
|
549
|
+
const normalizedHubAddress = normalizedAddress(session?.hubRendezvousEndpoint?.address);
|
|
550
|
+
const hubAddressObservedAt = session?.hubRendezvousObservedAt
|
|
551
|
+
? new Date(session.hubRendezvousObservedAt).toISOString()
|
|
552
|
+
: '';
|
|
553
|
+
const digest = value => value
|
|
554
|
+
? crypto.createHash('sha256')
|
|
555
|
+
.update(`${safeText(salt, 160)}:${value}`)
|
|
556
|
+
.digest('hex')
|
|
557
|
+
.slice(0, 16)
|
|
558
|
+
: '';
|
|
559
|
+
if (!session?.ready || !clientAddress) {
|
|
560
|
+
return {
|
|
561
|
+
evidence: 'authenticated-udp-endpoint-unavailable',
|
|
562
|
+
differentPublicEgress: null,
|
|
563
|
+
clientAddressHash: '',
|
|
564
|
+
hubAddressHash: digest(normalizedHubAddress),
|
|
565
|
+
hubAddressObservedAt,
|
|
566
|
+
hubEndpointSource: normalizedHubAddress
|
|
567
|
+
? 'trusted-rendezvous-observation'
|
|
568
|
+
: 'unavailable'
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const clientAddressPublic = isPublicIPv4(clientAddress);
|
|
572
|
+
const hubAddressPublic = isPublicIPv4(normalizedHubAddress);
|
|
573
|
+
return {
|
|
574
|
+
evidence: normalizedHubAddress
|
|
575
|
+
? 'authenticated-udp-endpoint-rendezvous-observed'
|
|
576
|
+
: 'authenticated-udp-endpoint-hub-egress-unavailable',
|
|
577
|
+
differentPublicEgress: clientAddressPublic && hubAddressPublic
|
|
578
|
+
? clientAddress !== normalizedHubAddress
|
|
579
|
+
: null,
|
|
580
|
+
clientAddressHash: digest(clientAddress),
|
|
581
|
+
hubAddressHash: digest(normalizedHubAddress),
|
|
582
|
+
clientAddressPublic,
|
|
583
|
+
hubAddressPublic,
|
|
584
|
+
endpointAuthenticated: true,
|
|
585
|
+
udpReady: session.ready === true,
|
|
586
|
+
observedAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
|
|
587
|
+
hubAddressObservedAt,
|
|
588
|
+
hubEndpointSource: normalizedHubAddress
|
|
589
|
+
? 'trusted-rendezvous-observation'
|
|
590
|
+
: 'unavailable'
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function close() {
|
|
595
|
+
stopRendezvousRefreshTimer();
|
|
596
|
+
stopFrameExpiryTimer();
|
|
617
597
|
for (const session of sessions.values()) {
|
|
618
598
|
clearPeerProbeTimers(session);
|
|
619
599
|
session.reassembler.clear();
|
|
620
|
-
}
|
|
621
|
-
sessions.clear();
|
|
622
|
-
sessionsBySessionId.clear();
|
|
623
|
-
sessionsByRouteKey.clear();
|
|
624
|
-
if (!started) return;
|
|
625
|
-
started = false;
|
|
626
|
-
await new Promise(resolve => socket.close(() => resolve()));
|
|
600
|
+
}
|
|
601
|
+
sessions.clear();
|
|
602
|
+
sessionsBySessionId.clear();
|
|
603
|
+
sessionsByRouteKey.clear();
|
|
604
|
+
if (!started) return;
|
|
605
|
+
started = false;
|
|
606
|
+
await new Promise(resolve => socket.close(() => resolve()));
|
|
627
607
|
}
|
|
628
608
|
|
|
629
609
|
return {
|
|
630
610
|
start,
|
|
631
611
|
close,
|
|
632
612
|
registerDevice,
|
|
633
|
-
unregisterDevice,
|
|
634
|
-
getDeviceStatus,
|
|
635
|
-
refreshDeviceDiagnosticNetwork,
|
|
636
|
-
getDeviceDiagnosticNetwork,
|
|
637
|
-
getStatus,
|
|
638
|
-
setRendezvousProofIssuer: issuer => {
|
|
639
|
-
rendezvousProofIssuer = typeof issuer === 'function' ? issuer : null;
|
|
640
|
-
},
|
|
613
|
+
unregisterDevice,
|
|
614
|
+
getDeviceStatus,
|
|
615
|
+
refreshDeviceDiagnosticNetwork,
|
|
616
|
+
getDeviceDiagnosticNetwork,
|
|
617
|
+
getStatus,
|
|
641
618
|
setFrameHandler: handler => { frameHandler = typeof handler === 'function' ? handler : () => {}; },
|
|
642
619
|
setEventHandler: handler => { eventHandler = typeof handler === 'function' ? handler : () => {}; }
|
|
643
620
|
};
|
|
644
621
|
|
|
645
|
-
function getStatus() {
|
|
646
|
-
let readySessionCount = 0;
|
|
647
|
-
const reassembly = {
|
|
648
|
-
pendingFrames: 0,
|
|
649
|
-
pendingBytes: 0,
|
|
650
|
-
pendingChunkBuffers: 0,
|
|
651
|
-
pendingBytesHighWatermark: 0,
|
|
652
|
-
pendingChunkBuffersHighWatermark: 0,
|
|
653
|
-
droppedFrames: 0,
|
|
654
|
-
completedFrames: 0,
|
|
655
|
-
completedPayloadCopyCount: 0,
|
|
656
|
-
completedPayloadCopyBytes: 0,
|
|
657
|
-
fullWireDuplicateCopyCount: 0,
|
|
658
|
-
binaryChunkHeaderCount: 0,
|
|
659
|
-
legacyChunkHeaderCount: 0
|
|
660
|
-
};
|
|
661
|
-
for (const session of sessions.values()) {
|
|
662
|
-
if (session.ready) readySessionCount += 1;
|
|
663
|
-
const stats = session.reassembler.getStats();
|
|
664
|
-
reassembly.pendingFrames += stats.pendingFrames;
|
|
665
|
-
reassembly.pendingBytes += stats.pendingBytes;
|
|
666
|
-
reassembly.pendingChunkBuffers += stats.pendingChunkBuffers;
|
|
667
|
-
reassembly.pendingBytesHighWatermark = Math.max(
|
|
668
|
-
reassembly.pendingBytesHighWatermark,
|
|
669
|
-
stats.pendingBytesHighWatermark
|
|
670
|
-
);
|
|
671
|
-
reassembly.pendingChunkBuffersHighWatermark = Math.max(
|
|
672
|
-
reassembly.pendingChunkBuffersHighWatermark,
|
|
673
|
-
stats.pendingChunkBuffersHighWatermark
|
|
674
|
-
);
|
|
675
|
-
reassembly.droppedFrames += stats.droppedFrames;
|
|
676
|
-
reassembly.completedFrames += stats.completedFrames;
|
|
677
|
-
reassembly.completedPayloadCopyCount += stats.completedPayloadCopyCount;
|
|
678
|
-
reassembly.completedPayloadCopyBytes += stats.completedPayloadCopyBytes;
|
|
679
|
-
reassembly.fullWireDuplicateCopyCount += stats.fullWireDuplicateCopyCount;
|
|
680
|
-
reassembly.binaryChunkHeaderCount += stats.binaryChunkHeaderCount;
|
|
681
|
-
reassembly.legacyChunkHeaderCount += stats.legacyChunkHeaderCount;
|
|
682
|
-
}
|
|
683
|
-
return {
|
|
622
|
+
function getStatus() {
|
|
623
|
+
let readySessionCount = 0;
|
|
624
|
+
const reassembly = {
|
|
625
|
+
pendingFrames: 0,
|
|
626
|
+
pendingBytes: 0,
|
|
627
|
+
pendingChunkBuffers: 0,
|
|
628
|
+
pendingBytesHighWatermark: 0,
|
|
629
|
+
pendingChunkBuffersHighWatermark: 0,
|
|
630
|
+
droppedFrames: 0,
|
|
631
|
+
completedFrames: 0,
|
|
632
|
+
completedPayloadCopyCount: 0,
|
|
633
|
+
completedPayloadCopyBytes: 0,
|
|
634
|
+
fullWireDuplicateCopyCount: 0,
|
|
635
|
+
binaryChunkHeaderCount: 0,
|
|
636
|
+
legacyChunkHeaderCount: 0
|
|
637
|
+
};
|
|
638
|
+
for (const session of sessions.values()) {
|
|
639
|
+
if (session.ready) readySessionCount += 1;
|
|
640
|
+
const stats = session.reassembler.getStats();
|
|
641
|
+
reassembly.pendingFrames += stats.pendingFrames;
|
|
642
|
+
reassembly.pendingBytes += stats.pendingBytes;
|
|
643
|
+
reassembly.pendingChunkBuffers += stats.pendingChunkBuffers;
|
|
644
|
+
reassembly.pendingBytesHighWatermark = Math.max(
|
|
645
|
+
reassembly.pendingBytesHighWatermark,
|
|
646
|
+
stats.pendingBytesHighWatermark
|
|
647
|
+
);
|
|
648
|
+
reassembly.pendingChunkBuffersHighWatermark = Math.max(
|
|
649
|
+
reassembly.pendingChunkBuffersHighWatermark,
|
|
650
|
+
stats.pendingChunkBuffersHighWatermark
|
|
651
|
+
);
|
|
652
|
+
reassembly.droppedFrames += stats.droppedFrames;
|
|
653
|
+
reassembly.completedFrames += stats.completedFrames;
|
|
654
|
+
reassembly.completedPayloadCopyCount += stats.completedPayloadCopyCount;
|
|
655
|
+
reassembly.completedPayloadCopyBytes += stats.completedPayloadCopyBytes;
|
|
656
|
+
reassembly.fullWireDuplicateCopyCount += stats.fullWireDuplicateCopyCount;
|
|
657
|
+
reassembly.binaryChunkHeaderCount += stats.binaryChunkHeaderCount;
|
|
658
|
+
reassembly.legacyChunkHeaderCount += stats.legacyChunkHeaderCount;
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
684
661
|
enabled,
|
|
685
662
|
preferred: preferP2p,
|
|
686
663
|
started,
|
|
687
664
|
bindHost,
|
|
688
665
|
port: boundPort,
|
|
689
666
|
advertiseHost,
|
|
690
|
-
rendezvousHost,
|
|
691
|
-
rendezvousPort,
|
|
692
|
-
rendezvousRefreshMs,
|
|
693
|
-
rendezvousResolvedAddressCount: rendezvousAddresses.size,
|
|
694
|
-
maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
|
|
695
|
-
sessionCount: sessions.size,
|
|
696
|
-
readySessionCount,
|
|
697
|
-
resource: {
|
|
698
|
-
...resource,
|
|
699
|
-
indexedSessionCount: sessionsBySessionId.size,
|
|
700
|
-
indexedRouteCount: sessionsByRouteKey.size,
|
|
701
|
-
...reassembly
|
|
702
|
-
}
|
|
703
|
-
};
|
|
667
|
+
rendezvousHost,
|
|
668
|
+
rendezvousPort,
|
|
669
|
+
rendezvousRefreshMs,
|
|
670
|
+
rendezvousResolvedAddressCount: rendezvousAddresses.size,
|
|
671
|
+
maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
|
|
672
|
+
sessionCount: sessions.size,
|
|
673
|
+
readySessionCount,
|
|
674
|
+
resource: {
|
|
675
|
+
...resource,
|
|
676
|
+
indexedSessionCount: sessionsBySessionId.size,
|
|
677
|
+
indexedRouteCount: sessionsByRouteKey.size,
|
|
678
|
+
...reassembly
|
|
679
|
+
}
|
|
680
|
+
};
|
|
704
681
|
}
|
|
705
682
|
}
|