@livedesk/hub 0.1.33 → 0.1.35
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 +1 -1
- package/src/remote-hub.js +54 -6
- package/src/server.js +52 -1
- package/src/transport/secure-direct-acceptor.js +113 -10
package/package.json
CHANGED
package/src/remote-hub.js
CHANGED
|
@@ -47,7 +47,7 @@ const RECENT_TASK_LIMIT = 12;
|
|
|
47
47
|
const RECENT_TASK_BATCH_LIMIT = 16;
|
|
48
48
|
const RECENT_FRAME_CACHE_TTL_MS = 4000;
|
|
49
49
|
const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
|
|
50
|
-
const RECENT_LIVE_FRAME_CACHE_LIMIT =
|
|
50
|
+
const RECENT_LIVE_FRAME_CACHE_LIMIT = 2;
|
|
51
51
|
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
|
|
52
52
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
53
53
|
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
@@ -1478,7 +1478,7 @@ function getRecentFrameCacheMaxBytes(frameKind) {
|
|
|
1478
1478
|
: RECENT_LIVE_FRAME_CACHE_MAX_BYTES;
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
-
function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
1481
|
+
export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
1482
1482
|
if (!Array.isArray(cache)) {
|
|
1483
1483
|
return;
|
|
1484
1484
|
}
|
|
@@ -1499,6 +1499,20 @@ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
|
1499
1499
|
}
|
|
1500
1500
|
}
|
|
1501
1501
|
|
|
1502
|
+
if (frameKind !== 'thumbnail' && cache.length > 1) {
|
|
1503
|
+
// Keep the newest frame plus the newest independently decodable key
|
|
1504
|
+
// frame. A browser can subscribe after capture has already started;
|
|
1505
|
+
// retaining only the latest 30 fps delta makes that browser wait for a
|
|
1506
|
+
// future GOP even though the Hub accepted a current key frame moments
|
|
1507
|
+
// earlier. Exact stream-owner matching below prevents stale replay.
|
|
1508
|
+
const latest = cache[0];
|
|
1509
|
+
const keyFrame = cache.find(entry =>
|
|
1510
|
+
entry !== latest
|
|
1511
|
+
&& (entry?.frame?.isKeyFrame === true
|
|
1512
|
+
|| safeString(entry?.frame?.chunkType, 20).toLowerCase() === 'key'));
|
|
1513
|
+
cache.splice(0, cache.length, ...[latest, keyFrame].filter(Boolean));
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1502
1516
|
for (let index = 0; index < cache.length; index += 1) {
|
|
1503
1517
|
const entry = cache[index];
|
|
1504
1518
|
const byteLength = Number(entry?.byteLength || entry?.frame?.payload?.length || 0) || 0;
|
|
@@ -1510,7 +1524,7 @@ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
|
1510
1524
|
}
|
|
1511
1525
|
}
|
|
1512
1526
|
|
|
1513
|
-
function rememberRecentFramePayload(device, frameKind, frame) {
|
|
1527
|
+
export function rememberRecentFramePayload(device, frameKind, frame) {
|
|
1514
1528
|
if (!device || !frame || !Buffer.isBuffer(frame.payload)) {
|
|
1515
1529
|
return;
|
|
1516
1530
|
}
|
|
@@ -1546,7 +1560,7 @@ function rememberRecentFramePayload(device, frameKind, frame) {
|
|
|
1546
1560
|
pruneRecentFrameCache(cache, kind, nowMs);
|
|
1547
1561
|
}
|
|
1548
1562
|
|
|
1549
|
-
function isFramePayloadRequestMatch(frame, options = {}) {
|
|
1563
|
+
export function isFramePayloadRequestMatch(frame, options = {}) {
|
|
1550
1564
|
if (!frame || !Buffer.isBuffer(frame.payload)) {
|
|
1551
1565
|
return false;
|
|
1552
1566
|
}
|
|
@@ -1565,10 +1579,37 @@ function isFramePayloadRequestMatch(frame, options = {}) {
|
|
|
1565
1579
|
return false;
|
|
1566
1580
|
}
|
|
1567
1581
|
|
|
1582
|
+
if (options.requireKeyFrame === true
|
|
1583
|
+
&& frame.isKeyFrame !== true
|
|
1584
|
+
&& safeString(frame.chunkType, 20).toLowerCase() !== 'key') {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
for (const key of ['streamId', 'sessionId', 'commandId', 'streamPurpose']) {
|
|
1589
|
+
const expected = safeString(options[key], 160);
|
|
1590
|
+
if (expected && safeString(frame[key], 160) !== expected) {
|
|
1591
|
+
return false;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const requestedCaptureGeneration = Number(options.captureGeneration);
|
|
1596
|
+
if (Number.isFinite(requestedCaptureGeneration)
|
|
1597
|
+
&& requestedCaptureGeneration > 0
|
|
1598
|
+
&& Number(frame.captureGeneration || 0) !== requestedCaptureGeneration) {
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
const requestedMonitorIndex = Number(options.monitorIndex);
|
|
1603
|
+
if (Number.isFinite(requestedMonitorIndex)
|
|
1604
|
+
&& requestedMonitorIndex >= 0
|
|
1605
|
+
&& Number(frame.monitorIndex ?? -1) !== requestedMonitorIndex) {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1568
1609
|
return true;
|
|
1569
1610
|
}
|
|
1570
1611
|
|
|
1571
|
-
function findRecentFramePayload(device, frameKind, options = {}) {
|
|
1612
|
+
export function findRecentFramePayload(device, frameKind, options = {}) {
|
|
1572
1613
|
const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
|
|
1573
1614
|
const latest = kind === 'thumbnail'
|
|
1574
1615
|
? device?.latestThumbnail
|
|
@@ -1579,7 +1620,14 @@ function findRecentFramePayload(device, frameKind, options = {}) {
|
|
|
1579
1620
|
|
|
1580
1621
|
const token = safeString(options.token, 128);
|
|
1581
1622
|
const requestedSeq = Number(options.frameSeq);
|
|
1582
|
-
|
|
1623
|
+
const ownerQuery = options.requireKeyFrame === true
|
|
1624
|
+
|| ['streamId', 'sessionId', 'commandId', 'streamPurpose']
|
|
1625
|
+
.some(key => safeString(options[key], 160))
|
|
1626
|
+
|| (Number.isFinite(Number(options.captureGeneration))
|
|
1627
|
+
&& Number(options.captureGeneration) > 0)
|
|
1628
|
+
|| (Number.isFinite(Number(options.monitorIndex))
|
|
1629
|
+
&& Number(options.monitorIndex) >= 0);
|
|
1630
|
+
if (!token && !Number.isFinite(requestedSeq) && !ownerQuery) {
|
|
1583
1631
|
return null;
|
|
1584
1632
|
}
|
|
1585
1633
|
|
package/src/server.js
CHANGED
|
@@ -2738,6 +2738,11 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
|
|
|
2738
2738
|
&& bindingIdentity === frameLaneBindingIdentity(currentExpectedBinding)) {
|
|
2739
2739
|
expectedBinding.readySent = currentExpectedBinding?.readySent === true
|
|
2740
2740
|
|| expectedBinding.readySent === true;
|
|
2741
|
+
expectedBinding.replayedKeyFrameSeq = Math.max(
|
|
2742
|
+
0,
|
|
2743
|
+
Number(currentExpectedBinding?.replayedKeyFrameSeq || 0),
|
|
2744
|
+
Number(expectedBinding.replayedKeyFrameSeq || 0)
|
|
2745
|
+
);
|
|
2741
2746
|
ws.liveDeskExpectedStreamBindingsByDeviceId.set(normalizedDeviceId, expectedBinding);
|
|
2742
2747
|
if (!lane.bindingEpochsByDeviceId.has(normalizedDeviceId)) {
|
|
2743
2748
|
lane.bindingEpochsByDeviceId.set(normalizedDeviceId, 1);
|
|
@@ -2776,6 +2781,50 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
|
|
|
2776
2781
|
return bindingIdentity;
|
|
2777
2782
|
}
|
|
2778
2783
|
|
|
2784
|
+
function replayExpectedBindingKeyFrame(ws, expectedBinding) {
|
|
2785
|
+
if (!ws || ws.readyState !== ws.OPEN || !expectedBinding) {
|
|
2786
|
+
return false;
|
|
2787
|
+
}
|
|
2788
|
+
const deviceId = String(expectedBinding.deviceId || '').trim();
|
|
2789
|
+
const currentBinding = ws.liveDeskExpectedStreamBindingsByDeviceId instanceof Map
|
|
2790
|
+
? ws.liveDeskExpectedStreamBindingsByDeviceId.get(deviceId)
|
|
2791
|
+
: null;
|
|
2792
|
+
const bindingIdentity = frameLaneBindingIdentity(expectedBinding);
|
|
2793
|
+
if (!deviceId
|
|
2794
|
+
|| !bindingIdentity
|
|
2795
|
+
|| bindingIdentity !== frameLaneBindingIdentity(currentBinding)) {
|
|
2796
|
+
return false;
|
|
2797
|
+
}
|
|
2798
|
+
const cached = remoteHub.getFramePayload(deviceId, 'live', {
|
|
2799
|
+
requireKeyFrame: true,
|
|
2800
|
+
streamId: expectedBinding.streamId,
|
|
2801
|
+
sessionId: expectedBinding.sessionId,
|
|
2802
|
+
commandId: expectedBinding.commandId,
|
|
2803
|
+
captureGeneration: expectedBinding.captureGeneration,
|
|
2804
|
+
streamPurpose: expectedBinding.streamPurpose,
|
|
2805
|
+
monitorIndex: expectedBinding.monitorIndex
|
|
2806
|
+
});
|
|
2807
|
+
const frameSeq = Number(cached?.frame?.frameSeq || 0);
|
|
2808
|
+
if (!cached
|
|
2809
|
+
|| cached.frame?.isKeyFrame !== true
|
|
2810
|
+
|| !Number.isSafeInteger(frameSeq)
|
|
2811
|
+
|| frameSeq <= 0
|
|
2812
|
+
|| Number(currentBinding?.replayedKeyFrameSeq || 0) === frameSeq) {
|
|
2813
|
+
return false;
|
|
2814
|
+
}
|
|
2815
|
+
currentBinding.replayedKeyFrameSeq = frameSeq;
|
|
2816
|
+
broadcastRemoteBinaryFrame({
|
|
2817
|
+
kind: 'live',
|
|
2818
|
+
deviceId,
|
|
2819
|
+
frame: cached.frame,
|
|
2820
|
+
payload: cached.payload,
|
|
2821
|
+
mimeType: cached.mimeType,
|
|
2822
|
+
byteLength: cached.byteLength,
|
|
2823
|
+
replayedForBinding: true
|
|
2824
|
+
});
|
|
2825
|
+
return true;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2779
2828
|
function dropQueuedFrameAtForLane(lane, dropIndex) {
|
|
2780
2829
|
const droppedBefore = lane.dropped;
|
|
2781
2830
|
const droppedItem = removeFrameLaneQueueItem(lane, dropIndex);
|
|
@@ -3269,6 +3318,7 @@ function startFrameSubscriptionLive(
|
|
|
3269
3318
|
continue;
|
|
3270
3319
|
}
|
|
3271
3320
|
cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
|
|
3321
|
+
const replayedKeyFrame = replayExpectedBindingKeyFrame(ws, expectedBinding);
|
|
3272
3322
|
started.push({
|
|
3273
3323
|
deviceId: expectedBinding.deviceId,
|
|
3274
3324
|
sessionId: expectedBinding.sessionId,
|
|
@@ -3277,7 +3327,8 @@ function startFrameSubscriptionLive(
|
|
|
3277
3327
|
commandId: expectedBinding.commandId,
|
|
3278
3328
|
captureGeneration: expectedBinding.captureGeneration,
|
|
3279
3329
|
monitorIndex: expectedBinding.monitorIndex,
|
|
3280
|
-
ready:
|
|
3330
|
+
ready: expectedBinding.readySent === true,
|
|
3331
|
+
replayedKeyFrame,
|
|
3281
3332
|
fps: result.fps,
|
|
3282
3333
|
reused: result.reused === true
|
|
3283
3334
|
});
|
|
@@ -17,10 +17,18 @@ const HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
|
17
17
|
const REPLAY_TTL_MS = 2 * 60 * 1000;
|
|
18
18
|
const MAX_REPLAY_ENTRIES = 8192;
|
|
19
19
|
const DEFAULT_MAX_PENDING_HANDSHAKES = 128;
|
|
20
|
-
|
|
20
|
+
// One Client can open control, frame, input, file, and audio channels.
|
|
21
|
+
// A 200-computer site behind one NAT can therefore legitimately create
|
|
22
|
+
// more than 1,000 authenticated handshakes during a coordinated restart.
|
|
23
|
+
// Keep a generous raw-IP burst ceiling; actual abuse blocking below is
|
|
24
|
+
// failure-scoped by device whenever a stable device id is available.
|
|
25
|
+
const DEFAULT_MAX_CONNECTIONS_PER_IP_PER_MINUTE = 2400;
|
|
21
26
|
const DEFAULT_MAX_CONSECUTIVE_FAILURES = 8;
|
|
22
27
|
const DEFAULT_IP_BLOCK_MS = 60_000;
|
|
23
28
|
const MAX_TRACKED_IPS = 4096;
|
|
29
|
+
const MAX_TRACKED_DEVICE_FAILURES = 16384;
|
|
30
|
+
const BLOCKED_IP_RECOVERY_PROBE_MS = 2_000;
|
|
31
|
+
const IP_SCOPED_FAILURES = new Set(['secure-handshake-json-invalid', 'secure-handshake-too-large']);
|
|
24
32
|
|
|
25
33
|
function directError(code) {
|
|
26
34
|
const error = new Error(code);
|
|
@@ -60,6 +68,26 @@ function safeHandshakeError(error) {
|
|
|
60
68
|
const code = clean(error?.code || error?.message, 100).toLowerCase();
|
|
61
69
|
return /^[-a-z0-9]+$/.test(code) ? code : 'secure-handshake-rejected';
|
|
62
70
|
}
|
|
71
|
+
function normalizeIpAddress(value) {
|
|
72
|
+
return clean(value, 100).replace(/^::ffff:/, '').toLowerCase() || 'unknown';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isPrivateNetworkAddress(value) {
|
|
76
|
+
const address = normalizeIpAddress(value);
|
|
77
|
+
if (address === '127.0.0.1' || address === '::1' || address === 'localhost') return true;
|
|
78
|
+
if (address.startsWith('fe80:') || address.startsWith('fc') || address.startsWith('fd')) return true;
|
|
79
|
+
const parts = address.split('.').map(part => Number(part));
|
|
80
|
+
return parts.length === 4
|
|
81
|
+
&& parts.every(part => Number.isInteger(part) && part >= 0 && part <= 255)
|
|
82
|
+
&& (parts[0] === 10
|
|
83
|
+
|| (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|
|
84
|
+
|| (parts[0] === 192 && parts[1] === 168));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function failureOwnerKey(ip, deviceId) {
|
|
88
|
+
const normalizedDeviceId = clean(deviceId, 128);
|
|
89
|
+
return normalizedDeviceId ? `${normalizeIpAddress(ip)}|${normalizedDeviceId}` : '';
|
|
90
|
+
}
|
|
63
91
|
|
|
64
92
|
export function createSecureDirectAcceptor({
|
|
65
93
|
authority,
|
|
@@ -77,6 +105,7 @@ export function createSecureDirectAcceptor({
|
|
|
77
105
|
}
|
|
78
106
|
const replayCache = new Map();
|
|
79
107
|
const ipStates = new Map();
|
|
108
|
+
const deviceFailureStates = new Map();
|
|
80
109
|
const maxPendingHandshakes = Math.max(1, Number(abuseLimits.maxPendingHandshakes)
|
|
81
110
|
|| DEFAULT_MAX_PENDING_HANDSHAKES);
|
|
82
111
|
const maxConnectionsPerIpPerMinute = Math.max(1, Number(abuseLimits.maxConnectionsPerIpPerMinute)
|
|
@@ -93,10 +122,13 @@ export function createSecureDirectAcceptor({
|
|
|
93
122
|
let capacityRejections = 0;
|
|
94
123
|
let rateLimitRejections = 0;
|
|
95
124
|
let blockedIpRejections = 0;
|
|
125
|
+
let blockedDeviceRejections = 0;
|
|
126
|
+
let compatibilityRejections = 0;
|
|
127
|
+
let recoveryProbeAdmissions = 0;
|
|
96
128
|
let probeClosures = 0;
|
|
97
129
|
|
|
98
130
|
function remoteAddress(rawSocket) {
|
|
99
|
-
return
|
|
131
|
+
return normalizeIpAddress(rawSocket?.remoteAddress);
|
|
100
132
|
}
|
|
101
133
|
|
|
102
134
|
function pruneIpStates(current) {
|
|
@@ -121,6 +153,7 @@ export function createSecureDirectAcceptor({
|
|
|
121
153
|
connectionCount: 0,
|
|
122
154
|
consecutiveFailures: 0,
|
|
123
155
|
blockedUntil: 0,
|
|
156
|
+
nextRecoveryProbeAt: 0,
|
|
124
157
|
lastSeenAt: current
|
|
125
158
|
};
|
|
126
159
|
ipStates.set(ip, state);
|
|
@@ -133,11 +166,51 @@ export function createSecureDirectAcceptor({
|
|
|
133
166
|
return state;
|
|
134
167
|
}
|
|
135
168
|
|
|
169
|
+
function pruneDeviceFailureStates(current) {
|
|
170
|
+
for (const [key, state] of deviceFailureStates) {
|
|
171
|
+
if (current - state.lastSeenAt >= Math.max(ipBlockMs * 2, 5 * 60_000)) {
|
|
172
|
+
deviceFailureStates.delete(key);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
while (deviceFailureStates.size > MAX_TRACKED_DEVICE_FAILURES) {
|
|
176
|
+
const oldest = deviceFailureStates.keys().next().value;
|
|
177
|
+
if (!oldest) break;
|
|
178
|
+
deviceFailureStates.delete(oldest);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getDeviceFailureState(ip, deviceId, current) {
|
|
183
|
+
const key = failureOwnerKey(ip, deviceId);
|
|
184
|
+
if (!key) return null;
|
|
185
|
+
pruneDeviceFailureStates(current);
|
|
186
|
+
let state = deviceFailureStates.get(key);
|
|
187
|
+
if (!state) {
|
|
188
|
+
state = { consecutiveFailures: 0, blockedUntil: 0, lastSeenAt: current };
|
|
189
|
+
deviceFailureStates.set(key, state);
|
|
190
|
+
}
|
|
191
|
+
state.lastSeenAt = current;
|
|
192
|
+
return state;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function deviceAdmissionError(ip, deviceId, current) {
|
|
196
|
+
const state = getDeviceFailureState(ip, deviceId, current);
|
|
197
|
+
if (state?.blockedUntil > current) {
|
|
198
|
+
blockedDeviceRejections += 1;
|
|
199
|
+
return 'secure-device-temporarily-blocked';
|
|
200
|
+
}
|
|
201
|
+
return '';
|
|
202
|
+
}
|
|
203
|
+
|
|
136
204
|
function admissionError(ip, current) {
|
|
137
205
|
const state = getIpState(ip, current);
|
|
138
206
|
if (state.blockedUntil > current) {
|
|
139
|
-
|
|
140
|
-
|
|
207
|
+
if (state.nextRecoveryProbeAt <= current) {
|
|
208
|
+
state.nextRecoveryProbeAt = current + BLOCKED_IP_RECOVERY_PROBE_MS;
|
|
209
|
+
recoveryProbeAdmissions += 1;
|
|
210
|
+
} else {
|
|
211
|
+
blockedIpRejections += 1;
|
|
212
|
+
return 'secure-ip-temporarily-blocked';
|
|
213
|
+
}
|
|
141
214
|
}
|
|
142
215
|
if (pendingHandshakes >= maxPendingHandshakes) {
|
|
143
216
|
capacityRejections += 1;
|
|
@@ -151,18 +224,39 @@ export function createSecureDirectAcceptor({
|
|
|
151
224
|
return '';
|
|
152
225
|
}
|
|
153
226
|
|
|
154
|
-
function recordFailure(ip, current) {
|
|
227
|
+
function recordFailure(ip, deviceId, code, current) {
|
|
228
|
+
if (code === 'secure-client-hello-required' && isPrivateNetworkAddress(ip)) {
|
|
229
|
+
compatibilityRejections += 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const deviceState = !IP_SCOPED_FAILURES.has(code)
|
|
233
|
+
? getDeviceFailureState(ip, deviceId, current)
|
|
234
|
+
: null;
|
|
235
|
+
if (deviceState) {
|
|
236
|
+
deviceState.consecutiveFailures += 1;
|
|
237
|
+
if (deviceState.consecutiveFailures >= maxConsecutiveFailures) {
|
|
238
|
+
deviceState.blockedUntil = Math.max(deviceState.blockedUntil, current + ipBlockMs);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
155
242
|
const state = getIpState(ip, current);
|
|
156
243
|
state.consecutiveFailures += 1;
|
|
157
244
|
if (state.consecutiveFailures >= maxConsecutiveFailures) {
|
|
158
245
|
state.blockedUntil = Math.max(state.blockedUntil, current + ipBlockMs);
|
|
246
|
+
state.nextRecoveryProbeAt = current + BLOCKED_IP_RECOVERY_PROBE_MS;
|
|
159
247
|
}
|
|
160
248
|
}
|
|
161
249
|
|
|
162
|
-
function recordSuccess(ip, current) {
|
|
250
|
+
function recordSuccess(ip, deviceId, current) {
|
|
163
251
|
const state = getIpState(ip, current);
|
|
164
252
|
state.consecutiveFailures = 0;
|
|
165
253
|
state.blockedUntil = 0;
|
|
254
|
+
state.nextRecoveryProbeAt = 0;
|
|
255
|
+
const deviceState = getDeviceFailureState(ip, deviceId, current);
|
|
256
|
+
if (deviceState) {
|
|
257
|
+
deviceState.consecutiveFailures = 0;
|
|
258
|
+
deviceState.blockedUntil = 0;
|
|
259
|
+
}
|
|
166
260
|
}
|
|
167
261
|
|
|
168
262
|
function rejectSocket(rawSocket, code, ip, observedDeviceId = '') {
|
|
@@ -360,12 +454,12 @@ export function createSecureDirectAcceptor({
|
|
|
360
454
|
settled = true;
|
|
361
455
|
cleanup();
|
|
362
456
|
const code = safeHandshakeError(error);
|
|
363
|
-
if (code === 'secure-handshake-closed' && bytes === 0) {
|
|
457
|
+
if ((code === 'secure-handshake-closed' || code === 'secure-handshake-timeout') && bytes === 0) {
|
|
364
458
|
probeClosures += 1;
|
|
365
459
|
try { rawSocket.destroy(); } catch {}
|
|
366
460
|
return;
|
|
367
461
|
}
|
|
368
|
-
recordFailure(ip, Math.floor(now()));
|
|
462
|
+
recordFailure(ip, observedDeviceId, code, Math.floor(now()));
|
|
369
463
|
rejectSocket(rawSocket, code, ip, observedDeviceId);
|
|
370
464
|
};
|
|
371
465
|
|
|
@@ -390,7 +484,12 @@ export function createSecureDirectAcceptor({
|
|
|
390
484
|
let hello;
|
|
391
485
|
try {
|
|
392
486
|
hello = JSON.parse(combined.subarray(0, newline).toString('utf8'));
|
|
393
|
-
observedDeviceId = clean(hello?.deviceId, 128);
|
|
487
|
+
observedDeviceId = clean(hello?.deviceId, 128); const deviceDenied = deviceAdmissionError(ip, observedDeviceId, Math.floor(now()));
|
|
488
|
+
if (deviceDenied) {
|
|
489
|
+
settled = false;
|
|
490
|
+
fail(directError(deviceDenied));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
394
493
|
} catch {
|
|
395
494
|
settled = false;
|
|
396
495
|
fail(directError('secure-handshake-json-invalid'));
|
|
@@ -398,7 +497,7 @@ export function createSecureDirectAcceptor({
|
|
|
398
497
|
}
|
|
399
498
|
try {
|
|
400
499
|
handleHello(rawSocket, hello, combined.subarray(newline + 1));
|
|
401
|
-
recordSuccess(ip, Math.floor(now()));
|
|
500
|
+
recordSuccess(ip, observedDeviceId, Math.floor(now()));
|
|
402
501
|
} catch (error) {
|
|
403
502
|
// settled is reset only for the common failure path so it can emit the
|
|
404
503
|
// bounded rejection response and release the raw socket.
|
|
@@ -424,9 +523,13 @@ export function createSecureDirectAcceptor({
|
|
|
424
523
|
replayRejections,
|
|
425
524
|
pendingHandshakes,
|
|
426
525
|
trackedIpCount: ipStates.size,
|
|
526
|
+
trackedDeviceFailureCount: deviceFailureStates.size,
|
|
427
527
|
capacityRejections,
|
|
428
528
|
rateLimitRejections,
|
|
429
529
|
blockedIpRejections,
|
|
530
|
+
blockedDeviceRejections,
|
|
531
|
+
compatibilityRejections,
|
|
532
|
+
recoveryProbeAdmissions,
|
|
430
533
|
probeClosures,
|
|
431
534
|
limits: {
|
|
432
535
|
maxPendingHandshakes,
|