@robota-sdk/agent-transport-webrtc 3.0.0-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +694 -0
  2. package/LICENSE +661 -0
  3. package/README.md +111 -0
  4. package/dist/node/index.cjs +1 -0
  5. package/dist/node/index.d.cts +1650 -0
  6. package/dist/node/index.d.cts.map +1 -0
  7. package/dist/node/index.d.ts +1650 -0
  8. package/dist/node/index.d.ts.map +1 -0
  9. package/dist/node/index.js +2 -0
  10. package/dist/node/index.js.map +1 -0
  11. package/package.json +93 -0
  12. package/src/__tests__/admission-secure-by-default.test.ts +63 -0
  13. package/src/__tests__/admission-steps.test.ts +46 -0
  14. package/src/__tests__/device-mesh-relay.test.ts +441 -0
  15. package/src/__tests__/device-mesh.test.ts +682 -0
  16. package/src/__tests__/discovering-mesh-relay.test.ts +234 -0
  17. package/src/__tests__/enrollment-link.test.ts +103 -0
  18. package/src/__tests__/fake-datachannel.ts +83 -0
  19. package/src/__tests__/handoff-grant-gate.test.ts +403 -0
  20. package/src/__tests__/local-peer-proof-gate.test.ts +215 -0
  21. package/src/__tests__/mesh-admission-contract.test.ts +38 -0
  22. package/src/__tests__/mesh-discovery.test.ts +820 -0
  23. package/src/__tests__/mesh-fixtures.ts +148 -0
  24. package/src/__tests__/mesh-internet.test.ts +942 -0
  25. package/src/__tests__/mesh-turn-relay.test.ts +161 -0
  26. package/src/__tests__/negotiated-certificate.test.ts +76 -0
  27. package/src/__tests__/nostr-source-condition.test.ts +27 -0
  28. package/src/__tests__/operator-approval-gate.test.ts +308 -0
  29. package/src/__tests__/outbound-delivery-carrier.test.ts +155 -0
  30. package/src/__tests__/pairing-e2e.test.ts +237 -0
  31. package/src/__tests__/pairing-gate-e3.test.ts +194 -0
  32. package/src/__tests__/pairing-gate-e4.test.ts +110 -0
  33. package/src/__tests__/pairing-gate.test.ts +296 -0
  34. package/src/__tests__/pairing-relay-in-the-middle.test.ts +225 -0
  35. package/src/__tests__/rtc-channel-close.test.ts +52 -0
  36. package/src/__tests__/rtc-peer-candidates.test.ts +61 -0
  37. package/src/__tests__/session-attachment-app-view.test.ts +66 -0
  38. package/src/__tests__/session-attachment-budget.test.ts +136 -0
  39. package/src/__tests__/turn-server-bind.test.ts +59 -0
  40. package/src/__tests__/turn-server.test.ts +335 -0
  41. package/src/__tests__/turn-test-client.ts +256 -0
  42. package/src/__tests__/webrtc-implementation.test.ts +83 -0
  43. package/src/__tests__/webrtc-transport.test.ts +349 -0
  44. package/src/__tests__/ws-mesh-relay-client.test.ts +95 -0
  45. package/src/__tests__/ws-signaling-client.test.ts +108 -0
  46. package/src/admission-steps.ts +55 -0
  47. package/src/channel-delivery.ts +44 -0
  48. package/src/datachannel-loader.ts +86 -0
  49. package/src/device-mesh-node.ts +1019 -0
  50. package/src/discovering-mesh-relay.ts +675 -0
  51. package/src/enrollment-link.ts +465 -0
  52. package/src/handoff-grant-gate.ts +186 -0
  53. package/src/index.ts +139 -0
  54. package/src/local-peer-proof.ts +135 -0
  55. package/src/mainline-dht-store.ts +174 -0
  56. package/src/mesh-dht.ts +446 -0
  57. package/src/mesh-discovery.ts +87 -0
  58. package/src/mesh-item-store.ts +287 -0
  59. package/src/mesh-lan-listener.ts +193 -0
  60. package/src/mesh-mdns.ts +406 -0
  61. package/src/mesh-peer-link.ts +600 -0
  62. package/src/mesh-public-relays.ts +25 -0
  63. package/src/mesh-records.ts +357 -0
  64. package/src/mesh-relay.ts +98 -0
  65. package/src/mesh-signal.ts +116 -0
  66. package/src/mesh-turn-relay.ts +238 -0
  67. package/src/negotiated-certificate.ts +72 -0
  68. package/src/nostr-mesh-relay.ts +391 -0
  69. package/src/pairing-channel-lifecycle.ts +31 -0
  70. package/src/pairing-controllers.ts +81 -0
  71. package/src/pairing-frames.ts +34 -0
  72. package/src/pairing-gate-options.ts +133 -0
  73. package/src/pairing-gate.ts +397 -0
  74. package/src/rtc-peer.ts +429 -0
  75. package/src/session-attachment.ts +96 -0
  76. package/src/signaling.ts +62 -0
  77. package/src/stun-message.ts +341 -0
  78. package/src/transport-lifecycle-error.ts +11 -0
  79. package/src/turn-server.ts +1028 -0
  80. package/src/webrtc-delivery-lifecycle.ts +54 -0
  81. package/src/webrtc-transport-options.ts +87 -0
  82. package/src/webrtc-transport.ts +368 -0
  83. package/src/ws-mesh-relay-client.ts +117 -0
  84. package/src/ws-signaling-client.ts +139 -0
@@ -0,0 +1,1028 @@
1
+ /**
2
+ * A TURN server (RFC 8656) over UDP: the subset a WebRTC client needs to reach a peer through a
3
+ * relayed address — Allocate, Refresh, CreatePermission, ChannelBind, Send and Data indications,
4
+ * ChannelData — with the long-term credential mechanism (RFC 8489 §9.2).
5
+ *
6
+ * It relays datagrams and nothing else: what crosses it is the peers' own DTLS, which it cannot
7
+ * open. Who may allocate is the caller's answer ({@link ITurnServerOptions.authorize}), asked once
8
+ * per allocation; the key it yields then authenticates every later request on that allocation, so
9
+ * an allocation outlives its credential's expiry but not its owner's removal ({@link TurnServer.retain}).
10
+ * Quotas bound what one owner can take: allocations, relayed bytes per second, and how long an
11
+ * allocation may live.
12
+ */
13
+ import { createHmac, randomBytes, randomInt, timingSafeEqual } from 'node:crypto';
14
+ import { createSocket, type RemoteInfo, type Socket } from 'node:dgram';
15
+ import { isIPv4 } from 'node:net';
16
+ import { networkInterfaces } from 'node:os';
17
+
18
+ import {
19
+ StunAttr,
20
+ StunClass,
21
+ StunMethod,
22
+ attribute,
23
+ attributes,
24
+ decodeChannelData,
25
+ decodeStun,
26
+ decodeXorAddress,
27
+ encodeChannelData,
28
+ encodeStun,
29
+ encodeXorAddress,
30
+ errorCode,
31
+ integrityProtected,
32
+ isChannelData,
33
+ longTermKey,
34
+ verifyIntegrity,
35
+ uint32,
36
+ MAX_CHANNEL,
37
+ MIN_CHANNEL,
38
+ type IStunMessage,
39
+ type ITransportAddress,
40
+ type TStunClass,
41
+ } from './stun-message.js';
42
+
43
+ /** Who a username belongs to, and the password it was issued with. */
44
+ export interface ITurnAuthorization {
45
+ /** The owner quotas are counted against and {@link TurnServer.retain} names. */
46
+ readonly owner: string;
47
+ readonly password: string;
48
+ }
49
+
50
+ export interface ITurnQuotas {
51
+ /** Allocations one owner may hold at once. */
52
+ readonly allocationsPerOwner: number;
53
+ /** Allocations the server holds at once. */
54
+ readonly totalAllocations: number;
55
+ /** Bytes relayed per second for one owner, both directions together; more is dropped. */
56
+ readonly bytesPerSecondPerOwner: number;
57
+ /** The longest lifetime one Allocate or Refresh grants. */
58
+ readonly maxLifetimeSeconds: number;
59
+ /** How long an allocation may live however often it is refreshed. */
60
+ readonly maxAllocationAgeMs: number;
61
+ readonly permissionsPerAllocation: number;
62
+ readonly channelsPerAllocation: number;
63
+ }
64
+
65
+ export const DEFAULT_TURN_QUOTAS: ITurnQuotas = {
66
+ allocationsPerOwner: 8,
67
+ totalAllocations: 64,
68
+ bytesPerSecondPerOwner: 1024 * 1024,
69
+ maxLifetimeSeconds: 3600,
70
+ maxAllocationAgeMs: 12 * 60 * 60 * 1000,
71
+ permissionsPerAllocation: 32,
72
+ channelsPerAllocation: 32,
73
+ };
74
+
75
+ /**
76
+ * Answers to requests nobody has authenticated, per second. Such a request may carry a forged
77
+ * source address, so what it is answered with must not be worth sending it for.
78
+ */
79
+ export interface IUnauthenticatedLimits {
80
+ readonly perSourcePerSecond: number;
81
+ readonly totalPerSecond: number;
82
+ }
83
+
84
+ export const DEFAULT_UNAUTHENTICATED_LIMITS: IUnauthenticatedLimits = {
85
+ perSourcePerSecond: 10,
86
+ // High: the per-source limit and the size rule already take away any gain from a forged source,
87
+ // and a low total would let anyone crowd out genuine clients' challenges.
88
+ totalPerSecond: 2_000,
89
+ };
90
+
91
+ export interface ITurnServerOptions {
92
+ /** The address the server and its relayed sockets bind (default every IPv4 interface). */
93
+ readonly host?: string;
94
+ /** The server's UDP port (default 3478; 0 picks a free one). */
95
+ readonly port?: number;
96
+ /**
97
+ * The ports relayed sockets take, inclusive (default: any free port). A relay behind a NAT needs
98
+ * this range forwarded to it as well as its own port.
99
+ */
100
+ readonly relayPorts?: { readonly min: number; readonly max: number };
101
+ /** The address relayed transport addresses carry (default `host`, or this machine's first external IPv4). */
102
+ readonly relayAddress?: string;
103
+ /**
104
+ * Default `r`: a realm travels in the clear, so it names nothing. At most 4 bytes: a challenge is
105
+ * never larger than the request it answers, and a longer realm would not fit the first request of
106
+ * the mesh's WebRTC stack.
107
+ */
108
+ readonly realm?: string;
109
+ /** Who `username` belongs to and its password; `undefined` refuses it. Asked once per allocation. */
110
+ readonly authorize: (username: string) => Promise<ITurnAuthorization | undefined>;
111
+ readonly quotas?: Partial<ITurnQuotas>;
112
+ /** How many requests nobody authenticated are answered, per source address and in all. */
113
+ readonly unauthenticatedLimits?: Partial<IUnauthenticatedLimits>;
114
+ /**
115
+ * Whether the default peer rule lets data reach private, link-local and shared (carrier-grade
116
+ * NAT) ranges: the relay host's own network (default true — a relayed connection to a device on
117
+ * that network needs it).
118
+ */
119
+ readonly allowPrivatePeers?: boolean;
120
+ /**
121
+ * Whether data may be relayed to or from `address`. Default: not unspecified, multicast or
122
+ * broadcast addresses, not loopback unless the relay itself is on loopback, and not private
123
+ * ranges when `allowPrivatePeers` is false.
124
+ */
125
+ readonly allowPeer?: (address: string) => boolean;
126
+ readonly now?: () => number;
127
+ /** A socket failed; the server keeps running. */
128
+ readonly onError?: (error: Error) => void;
129
+ }
130
+
131
+ const DEFAULT_LIFETIME_SECONDS = 600;
132
+ const PERMISSION_LIFETIME_MS = 300_000;
133
+ const CHANNEL_LIFETIME_MS = 600_000;
134
+ const NONCE_LIFETIME_MS = 600_000;
135
+ const SWEEP_INTERVAL_MS = 5_000;
136
+ const UDP = 17;
137
+ const MAX_REALM_BYTES = 4;
138
+ /** The smallest unauthenticated answer: a header and an ERROR-CODE with no reason phrase. */
139
+ const MIN_ANSWER_BYTES = 28;
140
+ /** Random ports of the relayed range tried before the range is walked in order. */
141
+ const RELAY_PORT_TRIES = 8;
142
+
143
+ /** Attributes a request may carry that this server understands or may ignore by RFC. */
144
+ const UNDERSTOOD = new Set<number>([
145
+ StunAttr.MappedAddress,
146
+ StunAttr.Username,
147
+ StunAttr.MessageIntegrity,
148
+ StunAttr.ErrorCode,
149
+ StunAttr.ChannelNumber,
150
+ StunAttr.Lifetime,
151
+ StunAttr.XorPeerAddress,
152
+ StunAttr.Data,
153
+ StunAttr.Realm,
154
+ StunAttr.Nonce,
155
+ StunAttr.RequestedAddressFamily,
156
+ StunAttr.RequestedTransport,
157
+ StunAttr.MessageIntegritySha256,
158
+ StunAttr.PasswordAlgorithm,
159
+ StunAttr.XorMappedAddress,
160
+ StunAttr.Priority,
161
+ StunAttr.UseCandidate,
162
+ ]);
163
+
164
+ interface IChannel {
165
+ readonly number: number;
166
+ readonly peer: ITransportAddress;
167
+ expiresAt: number;
168
+ }
169
+
170
+ interface IAllocation {
171
+ readonly key: string;
172
+ readonly client: ITransportAddress;
173
+ readonly owner: string;
174
+ readonly username: string;
175
+ readonly integrityKey: Buffer;
176
+ readonly transactionId: Buffer;
177
+ readonly socket: Socket;
178
+ readonly relayed: ITransportAddress;
179
+ readonly createdAt: number;
180
+ expiresAt: number;
181
+ /** Peer IP → expiry. */
182
+ readonly permissions: Map<string, number>;
183
+ readonly channelsByNumber: Map<number, IChannel>;
184
+ readonly channelsByPeer: Map<string, IChannel>;
185
+ response: Buffer;
186
+ }
187
+
188
+ interface IBucket {
189
+ tokens: number;
190
+ at: number;
191
+ }
192
+
193
+ function clientKey(rinfo: RemoteInfo | ITransportAddress): string {
194
+ return `${rinfo.address}:${rinfo.port}`;
195
+ }
196
+
197
+ function firstExternalIpv4(): string {
198
+ for (const list of Object.values(networkInterfaces())) {
199
+ for (const entry of list ?? []) {
200
+ if (entry.family === 'IPv4' && !entry.internal) return entry.address;
201
+ }
202
+ }
203
+ return '127.0.0.1';
204
+ }
205
+
206
+ function isLoopback(address: string): boolean {
207
+ return address.startsWith('127.') || address === '::1';
208
+ }
209
+
210
+ /** 10/8, 172.16/12, 192.168/16, 169.254/16 (link-local) and 100.64/10 (shared, carrier-grade NAT). */
211
+ function isPrivateIpv4(address: string): boolean {
212
+ const [a = 0, b = 0] = address.split('.').map(Number);
213
+ return (
214
+ a === 10 ||
215
+ (a === 172 && b >= 16 && b <= 31) ||
216
+ (a === 192 && b === 168) ||
217
+ (a === 169 && b === 254) ||
218
+ (a === 100 && b >= 64 && b <= 127)
219
+ );
220
+ }
221
+
222
+ function defaultAllowPeer(
223
+ relayOnLoopback: boolean,
224
+ allowPrivate: boolean,
225
+ ): (address: string) => boolean {
226
+ return (address) => {
227
+ if (!isIPv4(address)) return false;
228
+ const first = Number(address.split('.')[0]);
229
+ if (first === 0 || first >= 224) return false; // unspecified, multicast, reserved, broadcast
230
+ if (first === 127) return relayOnLoopback;
231
+ return allowPrivate || !isPrivateIpv4(address);
232
+ };
233
+ }
234
+
235
+ /** A UDP socket bound to `port` on `host`; rejects with the bind error. */
236
+ async function bindUdp(port: number, host: string): Promise<Socket> {
237
+ const socket = createSocket('udp4');
238
+ try {
239
+ await new Promise<void>((resolve, reject) => {
240
+ socket.once('error', reject);
241
+ socket.bind(port, host, () => {
242
+ socket.off('error', reject);
243
+ resolve();
244
+ });
245
+ });
246
+ return socket;
247
+ } catch (error) {
248
+ try {
249
+ socket.close();
250
+ } catch {
251
+ /* never bound */
252
+ }
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ /** A UDP socket bound to `port` on `host`; `undefined` when it cannot be. */
258
+ async function tryBindUdp(port: number, host: string): Promise<Socket | undefined> {
259
+ try {
260
+ return await bindUdp(port, host);
261
+ } catch {
262
+ // allow-fallback: a relayed port that cannot be bound; the caller tries another or refuses the request
263
+ return undefined;
264
+ }
265
+ }
266
+
267
+ /** Why the server's own socket could not be bound, in words that say what to change. */
268
+ function bindFailure(error: unknown, host: string, port: number): Error {
269
+ const code = error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined;
270
+ const why =
271
+ code === 'EADDRINUSE'
272
+ ? 'the port is taken'
273
+ : code === 'EADDRNOTAVAIL'
274
+ ? `${host} is not an address of this machine`
275
+ : code === 'EACCES'
276
+ ? `port ${port} needs privileges this process does not have`
277
+ : asError(error).message;
278
+ return new Error(`the TURN relay cannot listen on UDP ${host}:${port}: ${why}`, {
279
+ cause: error,
280
+ });
281
+ }
282
+
283
+ function localAddresses(): Set<string> {
284
+ const out = new Set<string>();
285
+ for (const list of Object.values(networkInterfaces())) {
286
+ for (const entry of list ?? []) out.add(entry.address);
287
+ }
288
+ return out;
289
+ }
290
+
291
+ /**
292
+ * Refill `bucket` at `rate` per second up to one second's worth (at least `need`), and say whether it
293
+ * now holds `need`. Time that runs backwards (a clock step) refills nothing and takes nothing, so a
294
+ * step cannot drain a budget for as long as the step.
295
+ */
296
+ function refill(bucket: IBucket, rate: number, now: number, need = 1): boolean {
297
+ const elapsed = Math.max(0, now - bucket.at);
298
+ bucket.tokens = Math.min(Math.max(rate, need), bucket.tokens + (elapsed / 1000) * rate);
299
+ bucket.at = now;
300
+ return bucket.tokens >= need;
301
+ }
302
+
303
+ function asError(error: unknown): Error {
304
+ return error instanceof Error ? error : new Error(String(error));
305
+ }
306
+
307
+ export class TurnServer {
308
+ private readonly allocations = new Map<string, IAllocation>();
309
+ private readonly buckets = new Map<string, IBucket>();
310
+ private readonly quotas: ITurnQuotas;
311
+ private readonly realm: string;
312
+ private readonly limits: IUnauthenticatedLimits;
313
+ /** Answers to requests nobody has authenticated yet, per source address and in all. */
314
+ private readonly answerBuckets = new Map<string, IBucket>();
315
+ private answerBudget?: IBucket;
316
+ private readonly nonceSecret = randomBytes(32);
317
+ private readonly ownAddresses = localAddresses();
318
+ private readonly allowPeer: (address: string) => boolean;
319
+ private readonly sweeper: ReturnType<typeof setInterval>;
320
+ /** Once {@link TurnServer.retain} has named them: the only owners that may allocate. */
321
+ private retained?: ReadonlySet<string>;
322
+ private closed = false;
323
+
324
+ private constructor(
325
+ private readonly socket: Socket,
326
+ private readonly options: ITurnServerOptions,
327
+ private readonly bindHost: string,
328
+ private readonly relayAddress: string,
329
+ ) {
330
+ this.quotas = { ...DEFAULT_TURN_QUOTAS, ...options.quotas };
331
+ this.realm = options.realm ?? 'r';
332
+ this.limits = { ...DEFAULT_UNAUTHENTICATED_LIMITS, ...options.unauthenticatedLimits };
333
+ this.allowPeer =
334
+ options.allowPeer ??
335
+ defaultAllowPeer(isLoopback(relayAddress), options.allowPrivatePeers ?? true);
336
+ socket.on('message', (data, rinfo) => this.receive(data, rinfo));
337
+ socket.on('error', (error) => options.onError?.(error));
338
+ this.sweeper = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
339
+ this.sweeper.unref?.();
340
+ }
341
+
342
+ /** Bind the server's socket and start serving. */
343
+ public static async start(options: ITurnServerOptions): Promise<TurnServer> {
344
+ const realmBytes = Buffer.byteLength(options.realm ?? 'r');
345
+ if (realmBytes === 0 || realmBytes > MAX_REALM_BYTES) {
346
+ throw new Error(`TURN realm must be 1 to ${MAX_REALM_BYTES} bytes`);
347
+ }
348
+ const host = options.host ?? '0.0.0.0';
349
+ const port = options.port ?? 3478;
350
+ let socket: Socket;
351
+ try {
352
+ socket = await bindUdp(port, host);
353
+ } catch (error) {
354
+ throw bindFailure(error, host, port);
355
+ }
356
+ const relayAddress = options.relayAddress ?? (host === '0.0.0.0' ? firstExternalIpv4() : host);
357
+ return new TurnServer(socket, options, host, relayAddress);
358
+ }
359
+
360
+ /** Where clients reach the server. */
361
+ public get address(): ITransportAddress {
362
+ const bound = this.socket.address();
363
+ return {
364
+ address: this.bindHost === '0.0.0.0' ? this.relayAddress : bound.address,
365
+ port: bound.port,
366
+ };
367
+ }
368
+
369
+ /** Allocations held, by one owner or in all. */
370
+ public allocationCount(owner?: string): number {
371
+ if (owner === undefined) return this.allocations.size;
372
+ let count = 0;
373
+ for (const allocation of this.allocations.values()) if (allocation.owner === owner) count += 1;
374
+ return count;
375
+ }
376
+
377
+ /**
378
+ * Close every allocation whose owner is not one of `owners`, and allocate for these owners only
379
+ * from now on — also for a request whose authorization was answered before this call.
380
+ */
381
+ public retain(owners: ReadonlySet<string>): void {
382
+ this.retained = new Set(owners);
383
+ for (const allocation of [...this.allocations.values()]) {
384
+ if (!owners.has(allocation.owner)) this.remove(allocation);
385
+ }
386
+ for (const owner of [...this.buckets.keys()])
387
+ if (!owners.has(owner)) this.buckets.delete(owner);
388
+ }
389
+
390
+ public close(): Promise<void> {
391
+ if (this.closed) return Promise.resolve();
392
+ this.closed = true;
393
+ clearInterval(this.sweeper);
394
+ for (const allocation of [...this.allocations.values()]) this.remove(allocation);
395
+ return new Promise((resolve) => this.socket.close(() => resolve()));
396
+ }
397
+
398
+ private now(): number {
399
+ return (this.options.now ?? Date.now)();
400
+ }
401
+
402
+ private receive(data: Buffer, rinfo: RemoteInfo): void {
403
+ if (this.closed) return;
404
+ if (isChannelData(data)) {
405
+ this.fromClientChannel(data, rinfo);
406
+ return;
407
+ }
408
+ const message = decodeStun(data);
409
+ if (message === undefined) return;
410
+ if (message.cls === StunClass.Indication) {
411
+ if (message.method === StunMethod.Send) this.fromClientSend(message, rinfo);
412
+ return;
413
+ }
414
+ if (message.cls !== StunClass.Request) return;
415
+ this.request(message, rinfo).catch((error: unknown) => this.options.onError?.(asError(error)));
416
+ }
417
+
418
+ private send(data: Buffer, to: ITransportAddress): void {
419
+ if (this.closed) return;
420
+ this.socket.send(data, to.port, to.address, (error) => {
421
+ if (error) this.options.onError?.(error);
422
+ });
423
+ }
424
+
425
+ private reply(
426
+ request: IStunMessage,
427
+ to: ITransportAddress,
428
+ attrs: { type: number; value: Buffer }[],
429
+ key?: Buffer,
430
+ ): Buffer {
431
+ const response = encodeStun(request.method, StunClass.Success, request.transactionId, attrs, {
432
+ ...(key !== undefined ? { integrityKey: key } : {}),
433
+ });
434
+ this.send(response, to);
435
+ return response;
436
+ }
437
+
438
+ private fail(
439
+ request: IStunMessage,
440
+ to: ITransportAddress,
441
+ code: number,
442
+ reason: string,
443
+ extra: { type: number; value: Buffer }[] = [],
444
+ key?: Buffer,
445
+ ): void {
446
+ const response = encodeStun(
447
+ request.method,
448
+ StunClass.Error,
449
+ request.transactionId,
450
+ [{ type: StunAttr.ErrorCode, value: errorCode(code, reason) }, ...extra],
451
+ { ...(key !== undefined ? { integrityKey: key } : {}) },
452
+ );
453
+ this.send(response, to);
454
+ }
455
+
456
+ /** A nonce: when it was issued and a MAC binding that to the client's address, 16 characters. */
457
+ private nonce(client: ITransportAddress): string {
458
+ const issued = Buffer.alloc(4);
459
+ issued.writeUInt32BE(Math.floor(this.now() / 1000) >>> 0, 0);
460
+ return Buffer.concat([issued, this.nonceMac(issued, client)]).toString('base64url');
461
+ }
462
+
463
+ private nonceMac(issued: Buffer, client: ITransportAddress): Buffer {
464
+ return createHmac('sha256', this.nonceSecret)
465
+ .update(issued)
466
+ .update(clientKey(client))
467
+ .digest()
468
+ .subarray(0, 8);
469
+ }
470
+
471
+ private nonceValid(nonce: string, client: ITransportAddress): boolean {
472
+ if (!/^[A-Za-z0-9_-]{16}$/.test(nonce)) return false;
473
+ const bytes = Buffer.from(nonce, 'base64url');
474
+ const issued = bytes.subarray(0, 4);
475
+ if (!timingSafeEqual(bytes.subarray(4), this.nonceMac(issued, client))) return false;
476
+ const age = this.now() - issued.readUInt32BE(0) * 1000;
477
+ return age >= -60_000 && age <= NONCE_LIFETIME_MS;
478
+ }
479
+
480
+ /**
481
+ * Answer a request nobody has authenticated. Its source may be forged, so the answer is never
482
+ * larger than the request (no reflection gain), and answers are limited per source address and in
483
+ * all; what is over is dropped unanswered.
484
+ */
485
+ private answerUnauthenticated(
486
+ request: IStunMessage,
487
+ to: ITransportAddress,
488
+ cls: TStunClass,
489
+ attrs: () => { type: number; value: Buffer }[],
490
+ ): void {
491
+ const now = this.now();
492
+ const total = (this.answerBudget ??= { tokens: this.limits.totalPerSecond, at: now });
493
+ const source = this.answerBuckets.get(to.address) ?? {
494
+ tokens: this.limits.perSourcePerSecond,
495
+ at: now,
496
+ };
497
+ this.answerBuckets.set(to.address, source);
498
+ // No answer is smaller than this: a request below it is dropped before any work.
499
+ if (request.raw.length < MIN_ANSWER_BYTES) return;
500
+ // Both budgets are checked before anything is computed, and charged only for an answer sent.
501
+ if (!refill(source, this.limits.perSourcePerSecond, now)) return;
502
+ if (!refill(total, this.limits.totalPerSecond, now)) return;
503
+ const response = encodeStun(request.method, cls, request.transactionId, attrs(), {
504
+ fingerprint: false,
505
+ });
506
+ if (response.length > request.raw.length) return;
507
+ source.tokens -= 1;
508
+ total.tokens -= 1;
509
+ this.send(response, to);
510
+ }
511
+
512
+ /** An error answer to an unauthenticated request; the reason phrase is left empty to keep it small. */
513
+ private refuseUnauthenticated(
514
+ request: IStunMessage,
515
+ to: ITransportAddress,
516
+ code: number,
517
+ extra: () => { type: number; value: Buffer }[] = () => [],
518
+ ): void {
519
+ this.answerUnauthenticated(request, to, StunClass.Error, () => [
520
+ { type: StunAttr.ErrorCode, value: errorCode(code, '') },
521
+ ...extra(),
522
+ ]);
523
+ }
524
+
525
+ private challenge(request: IStunMessage, to: ITransportAddress, code: 401 | 438): void {
526
+ this.refuseUnauthenticated(request, to, code, () => [
527
+ { type: StunAttr.Realm, value: Buffer.from(this.realm) },
528
+ { type: StunAttr.Nonce, value: Buffer.from(this.nonce(to)) },
529
+ ]);
530
+ }
531
+
532
+ /**
533
+ * Authenticate a request. On an existing allocation only its own username and key count;
534
+ * otherwise the caller's authorization is asked. `undefined`: already answered with an error.
535
+ */
536
+ private async authenticate(
537
+ request: IStunMessage,
538
+ client: ITransportAddress,
539
+ allocation: IAllocation | undefined,
540
+ ): Promise<
541
+ | { readonly auth: ITurnAuthorization; readonly key: Buffer; readonly username: string }
542
+ | undefined
543
+ > {
544
+ const integrity = attribute(request, StunAttr.MessageIntegrity);
545
+ if (integrity === undefined) {
546
+ this.challenge(request, client, 401);
547
+ return undefined;
548
+ }
549
+ const username = attribute(request, StunAttr.Username)?.toString('utf8');
550
+ const realm = attribute(request, StunAttr.Realm)?.toString('utf8');
551
+ const nonce = attribute(request, StunAttr.Nonce)?.toString('utf8');
552
+ if (username === undefined || realm === undefined || nonce === undefined) {
553
+ this.refuseUnauthenticated(request, client, 400);
554
+ return undefined;
555
+ }
556
+ if (realm !== this.realm) {
557
+ this.challenge(request, client, 401);
558
+ return undefined;
559
+ }
560
+ if (!this.nonceValid(nonce, client)) {
561
+ this.challenge(request, client, 438);
562
+ return undefined;
563
+ }
564
+ if (allocation !== undefined) {
565
+ if (username !== allocation.username) {
566
+ this.refuseUnauthenticated(request, client, 441);
567
+ return undefined;
568
+ }
569
+ if (!verifyIntegrity(request, allocation.integrityKey)) {
570
+ this.challenge(request, client, 401);
571
+ return undefined;
572
+ }
573
+ return {
574
+ auth: { owner: allocation.owner, password: '' },
575
+ key: allocation.integrityKey,
576
+ username,
577
+ };
578
+ }
579
+ let auth: ITurnAuthorization | undefined;
580
+ try {
581
+ auth = await this.options.authorize(username);
582
+ } catch {
583
+ // allow-fallback: an authorization that cannot be answered refuses, like an unknown username
584
+ auth = undefined;
585
+ }
586
+ if (this.closed) return undefined;
587
+ const key = auth === undefined ? undefined : longTermKey(username, this.realm, auth.password);
588
+ if (auth === undefined || key === undefined || !verifyIntegrity(request, key)) {
589
+ this.challenge(request, client, 401);
590
+ return undefined;
591
+ }
592
+ return { auth, key, username };
593
+ }
594
+
595
+ private unknownAttributes(request: IStunMessage): number[] {
596
+ return request.attributes
597
+ .map((a) => a.type)
598
+ .filter((type) => type < 0x8000 && !UNDERSTOOD.has(type));
599
+ }
600
+
601
+ private async request(request: IStunMessage, rinfo: RemoteInfo): Promise<void> {
602
+ const client: ITransportAddress = { address: rinfo.address, port: rinfo.port };
603
+ if (request.method === StunMethod.Binding) {
604
+ this.answerUnauthenticated(request, client, StunClass.Success, () => [
605
+ { type: StunAttr.XorMappedAddress, value: encodeXorAddress(client, request.transactionId) },
606
+ ]);
607
+ return;
608
+ }
609
+ const known: number[] = [
610
+ StunMethod.Allocate,
611
+ StunMethod.Refresh,
612
+ StunMethod.CreatePermission,
613
+ StunMethod.ChannelBind,
614
+ ];
615
+ if (!known.includes(request.method)) {
616
+ this.refuseUnauthenticated(request, client, 400);
617
+ return;
618
+ }
619
+ const held = this.allocations.get(clientKey(client));
620
+ const existing = this.live(held) ? held : undefined;
621
+ if (
622
+ request.method === StunMethod.Allocate &&
623
+ existing !== undefined &&
624
+ existing.transactionId.equals(request.transactionId)
625
+ ) {
626
+ // A retransmission of the request that made the allocation; like any unauthenticated answer,
627
+ // never larger than what asked for it.
628
+ if (existing.response.length <= request.raw.length) this.send(existing.response, client);
629
+ return;
630
+ }
631
+ const authenticated = await this.authenticate(request, client, existing);
632
+ if (authenticated === undefined) return;
633
+ // Only what MESSAGE-INTEGRITY covers counts: anyone on the path can append after it.
634
+ await this.authenticatedRequest(integrityProtected(request), client, authenticated);
635
+ }
636
+
637
+ private async authenticatedRequest(
638
+ request: IStunMessage,
639
+ client: ITransportAddress,
640
+ authenticated: {
641
+ readonly auth: ITurnAuthorization;
642
+ readonly key: Buffer;
643
+ readonly username: string;
644
+ },
645
+ ): Promise<void> {
646
+ const { key } = authenticated;
647
+ const unknown = this.unknownAttributes(request);
648
+ if (unknown.length > 0) {
649
+ const list = Buffer.alloc(unknown.length * 2);
650
+ unknown.forEach((type, i) => list.writeUInt16BE(type, i * 2));
651
+ this.fail(
652
+ request,
653
+ client,
654
+ 420,
655
+ 'Unknown Attribute',
656
+ [{ type: StunAttr.UnknownAttributes, value: list }],
657
+ key,
658
+ );
659
+ return;
660
+ }
661
+ const found = this.allocations.get(clientKey(client));
662
+ const current = this.live(found) ? found : undefined;
663
+ if (request.method === StunMethod.Allocate) {
664
+ await this.allocate(request, client, authenticated, current);
665
+ return;
666
+ }
667
+ if (current === undefined || current.owner !== authenticated.auth.owner) {
668
+ this.fail(request, client, 437, 'Allocation Mismatch', [], key);
669
+ return;
670
+ }
671
+ if (request.method === StunMethod.Refresh) this.refresh(request, client, current);
672
+ else if (request.method === StunMethod.CreatePermission) this.permit(request, client, current);
673
+ else this.bindChannel(request, client, current);
674
+ }
675
+
676
+ private async allocate(
677
+ request: IStunMessage,
678
+ client: ITransportAddress,
679
+ authenticated: {
680
+ readonly auth: ITurnAuthorization;
681
+ readonly key: Buffer;
682
+ readonly username: string;
683
+ },
684
+ existing: IAllocation | undefined,
685
+ ): Promise<void> {
686
+ const { auth, key, username } = authenticated;
687
+ if (existing !== undefined) {
688
+ this.fail(request, client, 437, 'Allocation Mismatch', [], key);
689
+ return;
690
+ }
691
+ const transport = attribute(request, StunAttr.RequestedTransport);
692
+ if (transport === undefined || transport.length !== 4) {
693
+ this.fail(request, client, 400, 'Bad Request', [], key);
694
+ return;
695
+ }
696
+ if (transport[0] !== UDP) {
697
+ this.fail(request, client, 442, 'Unsupported Transport Protocol', [], key);
698
+ return;
699
+ }
700
+ const family = attribute(request, StunAttr.RequestedAddressFamily);
701
+ if (family !== undefined && family[0] !== 0x01) {
702
+ this.fail(request, client, 440, 'Address Family not Supported', [], key);
703
+ return;
704
+ }
705
+ if (this.retained !== undefined && !this.retained.has(auth.owner)) {
706
+ this.fail(request, client, 403, 'Forbidden', [], key);
707
+ return;
708
+ }
709
+ if (this.allocations.size >= this.quotas.totalAllocations) {
710
+ this.fail(request, client, 508, 'Insufficient Capacity', [], key);
711
+ return;
712
+ }
713
+ if (this.allocationCount(auth.owner) >= this.quotas.allocationsPerOwner) {
714
+ this.fail(request, client, 486, 'Allocation Quota Reached', [], key);
715
+ return;
716
+ }
717
+ const socket = await this.relayedSocket();
718
+ if (socket === undefined) {
719
+ this.fail(request, client, 508, 'Insufficient Capacity', [], key);
720
+ return;
721
+ }
722
+ // Checked again: other allocations may have been made, or the owner removed, while the socket bound.
723
+ if (!this.closed && this.retained !== undefined && !this.retained.has(auth.owner)) {
724
+ socket.close();
725
+ this.fail(request, client, 403, 'Forbidden', [], key);
726
+ return;
727
+ }
728
+ if (
729
+ this.closed ||
730
+ this.allocations.has(clientKey(client)) ||
731
+ this.allocationCount(auth.owner) >= this.quotas.allocationsPerOwner ||
732
+ this.allocations.size >= this.quotas.totalAllocations
733
+ ) {
734
+ socket.close();
735
+ if (!this.closed) this.fail(request, client, 486, 'Allocation Quota Reached', [], key);
736
+ return;
737
+ }
738
+ const now = this.now();
739
+ const lifetime = this.grantedLifetime(request, now, now);
740
+ const allocation: IAllocation = {
741
+ key: clientKey(client),
742
+ client,
743
+ owner: auth.owner,
744
+ username,
745
+ integrityKey: key,
746
+ transactionId: Buffer.from(request.transactionId),
747
+ socket,
748
+ relayed: { address: this.relayAddress, port: socket.address().port },
749
+ createdAt: now,
750
+ expiresAt: now + lifetime * 1000,
751
+ permissions: new Map(),
752
+ channelsByNumber: new Map(),
753
+ channelsByPeer: new Map(),
754
+ response: Buffer.alloc(0),
755
+ };
756
+ socket.on('message', (data, rinfo) => this.fromPeer(allocation, data, rinfo));
757
+ socket.on('error', (error) => this.options.onError?.(error));
758
+ this.allocations.set(allocation.key, allocation);
759
+ allocation.response = this.reply(
760
+ request,
761
+ client,
762
+ [
763
+ {
764
+ type: StunAttr.XorRelayedAddress,
765
+ value: encodeXorAddress(allocation.relayed, request.transactionId),
766
+ },
767
+ { type: StunAttr.Lifetime, value: uint32(lifetime) },
768
+ { type: StunAttr.XorMappedAddress, value: encodeXorAddress(client, request.transactionId) },
769
+ ],
770
+ key,
771
+ );
772
+ }
773
+
774
+ /** A socket for a relayed address: any free port, or a free one of the configured range. */
775
+ private async relayedSocket(): Promise<Socket | undefined> {
776
+ const range = this.options.relayPorts;
777
+ if (range === undefined) return tryBindUdp(0, this.bindHost);
778
+ const size = range.max - range.min + 1;
779
+ for (let i = 0; i < Math.min(size, RELAY_PORT_TRIES); i += 1) {
780
+ const socket = await tryBindUdp(randomInt(range.min, range.max + 1), this.bindHost);
781
+ if (socket !== undefined) return socket;
782
+ }
783
+ // Random picks can miss the last free ports of a small range: take them in order.
784
+ const taken = new Set(this.allocationPorts());
785
+ for (let port = range.min; port <= range.max; port += 1) {
786
+ if (taken.has(port)) continue;
787
+ const socket = await tryBindUdp(port, this.bindHost);
788
+ if (socket !== undefined) return socket;
789
+ }
790
+ return undefined;
791
+ }
792
+
793
+ private allocationPorts(): number[] {
794
+ return [...this.allocations.values()].map((allocation) => allocation.relayed.port);
795
+ }
796
+
797
+ /** Whether `peer` is this server's own listening socket: relaying there would loop TURN into itself. */
798
+ private isServerItself(peer: ITransportAddress): boolean {
799
+ return (
800
+ peer.port === this.socket.address().port &&
801
+ (isLoopback(peer.address) ||
802
+ peer.address === this.relayAddress ||
803
+ peer.address === this.bindHost ||
804
+ (this.bindHost === '0.0.0.0' && this.ownAddresses.has(peer.address)))
805
+ );
806
+ }
807
+
808
+ /** The lifetime granted: what was asked, within the quota and the allocation's remaining age. */
809
+ private grantedLifetime(request: IStunMessage, createdAt: number, now: number): number {
810
+ const asked = attribute(request, StunAttr.Lifetime);
811
+ const requested =
812
+ asked !== undefined && asked.length === 4 ? asked.readUInt32BE(0) : DEFAULT_LIFETIME_SECONDS;
813
+ const remaining = Math.floor((createdAt + this.quotas.maxAllocationAgeMs - now) / 1000);
814
+ return Math.max(0, Math.min(requested, this.quotas.maxLifetimeSeconds, remaining));
815
+ }
816
+
817
+ private refresh(request: IStunMessage, client: ITransportAddress, allocation: IAllocation): void {
818
+ const now = this.now();
819
+ const lifetime = this.grantedLifetime(request, allocation.createdAt, now);
820
+ if (lifetime === 0) {
821
+ this.remove(allocation);
822
+ this.reply(
823
+ request,
824
+ client,
825
+ [{ type: StunAttr.Lifetime, value: uint32(0) }],
826
+ allocation.integrityKey,
827
+ );
828
+ return;
829
+ }
830
+ allocation.expiresAt = now + lifetime * 1000;
831
+ this.reply(
832
+ request,
833
+ client,
834
+ [{ type: StunAttr.Lifetime, value: uint32(lifetime) }],
835
+ allocation.integrityKey,
836
+ );
837
+ }
838
+
839
+ private peerAddresses(request: IStunMessage): ITransportAddress[] | undefined {
840
+ const out: ITransportAddress[] = [];
841
+ for (const value of attributes(request, StunAttr.XorPeerAddress)) {
842
+ const peer = decodeXorAddress(value, request.transactionId);
843
+ if (peer === undefined) return undefined;
844
+ out.push(peer);
845
+ }
846
+ return out.length > 0 ? out : undefined;
847
+ }
848
+
849
+ private permit(request: IStunMessage, client: ITransportAddress, allocation: IAllocation): void {
850
+ const key = allocation.integrityKey;
851
+ const peers = this.peerAddresses(request);
852
+ if (peers === undefined) {
853
+ this.fail(request, client, 400, 'Bad Request', [], key);
854
+ return;
855
+ }
856
+ if (peers.some((peer) => !isIPv4(peer.address))) {
857
+ this.fail(request, client, 443, 'Peer Address Family Mismatch', [], key);
858
+ return;
859
+ }
860
+ if (peers.some((peer) => !this.allowPeer(peer.address))) {
861
+ this.fail(request, client, 403, 'Forbidden', [], key);
862
+ return;
863
+ }
864
+ const added = new Set(
865
+ peers.map((p) => p.address).filter((a) => !allocation.permissions.has(a)),
866
+ );
867
+ if (allocation.permissions.size + added.size > this.quotas.permissionsPerAllocation) {
868
+ this.fail(request, client, 508, 'Insufficient Capacity', [], key);
869
+ return;
870
+ }
871
+ const expiresAt = this.now() + PERMISSION_LIFETIME_MS;
872
+ for (const peer of peers) allocation.permissions.set(peer.address, expiresAt);
873
+ this.reply(request, client, [], key);
874
+ }
875
+
876
+ private bindChannel(
877
+ request: IStunMessage,
878
+ client: ITransportAddress,
879
+ allocation: IAllocation,
880
+ ): void {
881
+ const key = allocation.integrityKey;
882
+ const numberValue = attribute(request, StunAttr.ChannelNumber);
883
+ const peers = this.peerAddresses(request);
884
+ const number =
885
+ numberValue !== undefined && numberValue.length === 4 ? numberValue.readUInt16BE(0) : -1;
886
+ if (number < MIN_CHANNEL || number > MAX_CHANNEL || peers === undefined || peers.length !== 1) {
887
+ this.fail(request, client, 400, 'Bad Request', [], key);
888
+ return;
889
+ }
890
+ const peer = peers[0]!;
891
+ if (!isIPv4(peer.address)) {
892
+ this.fail(request, client, 443, 'Peer Address Family Mismatch', [], key);
893
+ return;
894
+ }
895
+ if (!this.allowPeer(peer.address)) {
896
+ this.fail(request, client, 403, 'Forbidden', [], key);
897
+ return;
898
+ }
899
+ const byNumber = allocation.channelsByNumber.get(number);
900
+ const byPeer = allocation.channelsByPeer.get(clientKey(peer));
901
+ if (
902
+ (byNumber !== undefined && clientKey(byNumber.peer) !== clientKey(peer)) ||
903
+ (byPeer !== undefined && byPeer.number !== number)
904
+ ) {
905
+ this.fail(request, client, 400, 'Bad Request', [], key);
906
+ return;
907
+ }
908
+ const isNew = byNumber === undefined;
909
+ if (
910
+ (isNew && allocation.channelsByNumber.size >= this.quotas.channelsPerAllocation) ||
911
+ (!allocation.permissions.has(peer.address) &&
912
+ allocation.permissions.size >= this.quotas.permissionsPerAllocation)
913
+ ) {
914
+ this.fail(request, client, 508, 'Insufficient Capacity', [], key);
915
+ return;
916
+ }
917
+ const now = this.now();
918
+ const channel: IChannel = byNumber ?? { number, peer, expiresAt: 0 };
919
+ channel.expiresAt = now + CHANNEL_LIFETIME_MS;
920
+ allocation.channelsByNumber.set(number, channel);
921
+ allocation.channelsByPeer.set(clientKey(peer), channel);
922
+ allocation.permissions.set(peer.address, now + PERMISSION_LIFETIME_MS);
923
+ this.reply(request, client, [], key);
924
+ }
925
+
926
+ private permitted(allocation: IAllocation, address: string): boolean {
927
+ const expiresAt = allocation.permissions.get(address);
928
+ return expiresAt !== undefined && expiresAt > this.now();
929
+ }
930
+
931
+ /** Take `bytes` from the owner's budget for this second; false when it is spent. */
932
+ private spend(owner: string, bytes: number): boolean {
933
+ const now = this.now();
934
+ const rate = this.quotas.bytesPerSecondPerOwner;
935
+ const bucket = this.buckets.get(owner) ?? { tokens: rate, at: now };
936
+ this.buckets.set(owner, bucket);
937
+ if (!refill(bucket, rate, now, bytes)) return false;
938
+ bucket.tokens -= bytes;
939
+ return true;
940
+ }
941
+
942
+ private live(allocation: IAllocation | undefined): allocation is IAllocation {
943
+ if (allocation === undefined) return false;
944
+ if (allocation.expiresAt > this.now()) return true;
945
+ this.remove(allocation);
946
+ return false;
947
+ }
948
+
949
+ private fromClientSend(message: IStunMessage, rinfo: RemoteInfo): void {
950
+ const allocation = this.allocations.get(clientKey(rinfo));
951
+ if (!this.live(allocation)) return;
952
+ const peerValue = attribute(message, StunAttr.XorPeerAddress);
953
+ const data = attribute(message, StunAttr.Data);
954
+ if (peerValue === undefined || data === undefined) return;
955
+ const peer = decodeXorAddress(peerValue, message.transactionId);
956
+ if (peer === undefined || !this.permitted(allocation, peer.address)) return;
957
+ if (this.isServerItself(peer)) return;
958
+ if (!this.spend(allocation.owner, data.length)) return;
959
+ allocation.socket.send(data, peer.port, peer.address);
960
+ }
961
+
962
+ private fromClientChannel(data: Buffer, rinfo: RemoteInfo): void {
963
+ const allocation = this.allocations.get(clientKey(rinfo));
964
+ if (!this.live(allocation)) return;
965
+ const frame = decodeChannelData(data);
966
+ if (frame === undefined) return;
967
+ const channel = allocation.channelsByNumber.get(frame.channel);
968
+ if (channel === undefined || channel.expiresAt <= this.now()) return;
969
+ if (!this.permitted(allocation, channel.peer.address)) return;
970
+ if (this.isServerItself(channel.peer)) return;
971
+ if (!this.spend(allocation.owner, frame.payload.length)) return;
972
+ allocation.socket.send(frame.payload, channel.peer.port, channel.peer.address);
973
+ }
974
+
975
+ private fromPeer(allocation: IAllocation, data: Buffer, rinfo: RemoteInfo): void {
976
+ if (this.allocations.get(allocation.key) !== allocation || !this.live(allocation)) return;
977
+ if (!this.permitted(allocation, rinfo.address)) return;
978
+ if (!this.spend(allocation.owner, data.length)) return;
979
+ const channel = allocation.channelsByPeer.get(clientKey(rinfo));
980
+ if (channel !== undefined && channel.expiresAt > this.now()) {
981
+ this.send(encodeChannelData(channel.number, data), allocation.client);
982
+ return;
983
+ }
984
+ const transactionId = randomBytes(12);
985
+ this.send(
986
+ encodeStun(StunMethod.Data, StunClass.Indication, transactionId, [
987
+ {
988
+ type: StunAttr.XorPeerAddress,
989
+ value: encodeXorAddress({ address: rinfo.address, port: rinfo.port }, transactionId),
990
+ },
991
+ { type: StunAttr.Data, value: data },
992
+ ]),
993
+ allocation.client,
994
+ );
995
+ }
996
+
997
+ private remove(allocation: IAllocation): void {
998
+ if (this.allocations.get(allocation.key) === allocation)
999
+ this.allocations.delete(allocation.key);
1000
+ try {
1001
+ allocation.socket.close();
1002
+ } catch {
1003
+ /* already closed */
1004
+ }
1005
+ }
1006
+
1007
+ private sweep(): void {
1008
+ const now = this.now();
1009
+ // A source whose budget has refilled is forgotten, so the table holds recent senders only.
1010
+ for (const [address, bucket] of this.answerBuckets) {
1011
+ if (Math.abs(now - bucket.at) >= 1000) this.answerBuckets.delete(address);
1012
+ }
1013
+ for (const allocation of [...this.allocations.values()]) {
1014
+ if (allocation.expiresAt <= now) {
1015
+ this.remove(allocation);
1016
+ continue;
1017
+ }
1018
+ for (const [address, expiresAt] of allocation.permissions) {
1019
+ if (expiresAt <= now) allocation.permissions.delete(address);
1020
+ }
1021
+ for (const [number, channel] of allocation.channelsByNumber) {
1022
+ if (channel.expiresAt > now) continue;
1023
+ allocation.channelsByNumber.delete(number);
1024
+ allocation.channelsByPeer.delete(clientKey(channel.peer));
1025
+ }
1026
+ }
1027
+ }
1028
+ }