node-rtc-connection 1.0.18 → 2.0.4

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 (65) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5421
  3. package/dist/index.mjs +25 -5413
  4. package/dist/types/crypto/der.d.ts +107 -0
  5. package/dist/types/crypto/x509.d.ts +56 -0
  6. package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
  7. package/dist/types/dtls/RTCCertificate.d.ts +163 -0
  8. package/dist/types/dtls/cipher.d.ts +81 -0
  9. package/dist/types/dtls/connection.d.ts +81 -0
  10. package/dist/types/dtls/prf.d.ts +29 -0
  11. package/dist/types/dtls/protocol.d.ts +127 -0
  12. package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
  13. package/dist/types/foundation/RTCError.d.ts +152 -0
  14. package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
  15. package/dist/types/ice/ice-agent.d.ts +154 -0
  16. package/dist/types/ice/stun-message.d.ts +92 -0
  17. package/dist/types/index.d.ts +29 -0
  18. package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  19. package/dist/types/sctp/association.d.ts +77 -0
  20. package/dist/types/sctp/chunks.d.ts +200 -0
  21. package/dist/types/sctp/crc32c.d.ts +24 -0
  22. package/dist/types/sctp/datachannel-manager.d.ts +51 -0
  23. package/dist/types/sctp/dcep.d.ts +56 -0
  24. package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
  25. package/dist/types/sdp/sdp-utils.d.ts +103 -0
  26. package/dist/types/stun/stun-client.d.ts +119 -0
  27. package/dist/types/transport-stack.d.ts +68 -0
  28. package/package.json +26 -21
  29. package/src/crypto/der.ts +205 -0
  30. package/src/crypto/x509.ts +146 -0
  31. package/src/datachannel/RTCDataChannel.ts +388 -0
  32. package/src/dtls/RTCCertificate.ts +396 -0
  33. package/src/dtls/cipher.ts +198 -0
  34. package/src/dtls/connection.ts +974 -0
  35. package/src/dtls/prf.ts +62 -0
  36. package/src/dtls/protocol.ts +204 -0
  37. package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
  38. package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
  39. package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
  40. package/src/ice/ice-agent.ts +609 -0
  41. package/src/ice/stun-message.ts +260 -0
  42. package/src/index.ts +72 -0
  43. package/src/peerconnection/RTCPeerConnection.ts +430 -0
  44. package/src/sctp/association.ts +523 -0
  45. package/src/sctp/chunks.ts +350 -0
  46. package/src/sctp/crc32c.ts +57 -0
  47. package/src/sctp/datachannel-manager.ts +187 -0
  48. package/src/sctp/dcep.ts +94 -0
  49. package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
  50. package/src/sdp/sdp-utils.ts +229 -0
  51. package/src/stun/stun-client.ts +936 -0
  52. package/src/transport-stack.ts +165 -0
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.mjs.map +0 -1
  55. package/src/datachannel/RTCDataChannel.js +0 -354
  56. package/src/dtls/RTCCertificate.js +0 -310
  57. package/src/dtls/RTCDtlsTransport.js +0 -247
  58. package/src/ice/RTCIceTransport.js +0 -998
  59. package/src/index.d.ts +0 -400
  60. package/src/index.js +0 -92
  61. package/src/network/network-transport.js +0 -478
  62. package/src/peerconnection/RTCPeerConnection.js +0 -851
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. package/src/sdp/sdp-utils.js +0 -224
  65. package/src/stun/stun-client.js +0 -643
@@ -0,0 +1,523 @@
1
+ /**
2
+ * @file association.ts
3
+ * @description Minimal SCTP association over a datagram channel (DTLS), scoped
4
+ * to the WebRTC data channel profile (RFC 8831).
5
+ * @module sctp/association
6
+ *
7
+ * Implements the four-way INIT/INIT-ACK/COOKIE-ECHO/COOKIE-ACK setup, DATA
8
+ * transmit/receive with TSN tracking and SACK, ordered and unordered delivery,
9
+ * and reassembly of fragmented user messages. Congestion control is
10
+ * intentionally simple (stop-and-go style with a generous rwnd) which is
11
+ * adequate for control/data-channel traffic and interoperates with usrsctp
12
+ * (the stack browsers use).
13
+ *
14
+ * The association rides on top of a reliable-ish datagram pipe provided by
15
+ * DTLS; SCTP still provides framing, ordering, multiplexing and ack'ing.
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ import * as crypto from 'crypto';
21
+ import { EventEmitter } from 'events';
22
+ import * as C from './chunks';
23
+ import { applyChecksum, verifyChecksum } from './crc32c';
24
+
25
+ const SCTP_PORT = 5000; // WebRTC uses 5000 on both ends
26
+ const DEFAULT_RWND = 1024 * 1024;
27
+ const MAX_PAYLOAD = 1200; // fragment user messages above this (fits DTLS/MTU)
28
+ const RTO_INITIAL = 500;
29
+ const RTO_MAX = 5000;
30
+
31
+ const STATE = Object.freeze({
32
+ CLOSED: 'closed',
33
+ COOKIE_WAIT: 'cookie-wait',
34
+ COOKIE_ECHOED: 'cookie-echoed',
35
+ ESTABLISHED: 'established',
36
+ });
37
+
38
+ type StateValue = (typeof STATE)[keyof typeof STATE];
39
+
40
+ /** Options for constructing an {@link SctpAssociation}. */
41
+ export interface SctpAssociationOptions {
42
+ /** the DTLS client initiates SCTP (RFC 8831) */
43
+ isClient?: boolean;
44
+ }
45
+
46
+ /** A complete user message surfaced via the 'message' event. */
47
+ export interface SctpMessage {
48
+ streamId: number;
49
+ ppid: number;
50
+ data: Buffer;
51
+ }
52
+
53
+ /** A queued DATA chunk awaiting SACK. */
54
+ interface SentEntry {
55
+ chunk: Buffer;
56
+ }
57
+
58
+ /** In-progress reassembly buffer for a fragmented user message. */
59
+ interface FragmentBuffer {
60
+ ppid: number;
61
+ parts: Buffer[];
62
+ }
63
+
64
+ /** Serial number arithmetic (RFC 1982) for 32-bit TSNs. */
65
+ function snLt(a: number, b: number): boolean {
66
+ return ((a - b) & 0xffffffff) > 0x80000000;
67
+ }
68
+ function snLte(a: number, b: number): boolean {
69
+ return a === b || snLt(a, b);
70
+ }
71
+
72
+ /**
73
+ * @class SctpAssociation
74
+ * @extends EventEmitter
75
+ *
76
+ * Events:
77
+ * - 'established' association is up
78
+ * - 'message' ({streamId, ppid, data}) a complete user message arrived
79
+ * - 'output' (Buffer) an SCTP packet to hand to DTLS
80
+ * - 'close'
81
+ */
82
+ class SctpAssociation extends EventEmitter {
83
+ isClient: boolean;
84
+ state: StateValue;
85
+
86
+ #localPort: number;
87
+ #remotePort: number;
88
+
89
+ #localTag: number;
90
+ #remoteTag: number;
91
+
92
+ // Transmit side.
93
+ #localTSN: number;
94
+ #nextSSN: Map<number, number>; // streamId -> next outbound stream sequence
95
+ #sentQueue: Map<number, SentEntry>; // tsn -> { chunk } awaiting SACK
96
+
97
+ // Receive side.
98
+ #peerCumulativeTSN: number | null; // highest contiguous TSN received
99
+ #receivedOutOfOrder: Map<number, C.ParsedDataBody>; // tsn -> dataChunk (gap storage)
100
+ #fragments: Map<string, FragmentBuffer>; // streamId -> array of partial DATA payloads
101
+
102
+ #initTimer: ReturnType<typeof setTimeout> | null;
103
+
104
+ #cookieSecret?: Buffer;
105
+
106
+ /**
107
+ * @param {Object} opts
108
+ * @param {boolean} opts.isClient - the DTLS client initiates SCTP (RFC 8831)
109
+ */
110
+ constructor(opts: SctpAssociationOptions = {}) {
111
+ super();
112
+ this.isClient = !!opts.isClient;
113
+ this.state = STATE.CLOSED;
114
+
115
+ this.#localPort = SCTP_PORT;
116
+ this.#remotePort = SCTP_PORT;
117
+
118
+ this.#localTag = crypto.randomBytes(4).readUInt32BE(0) >>> 0 || 1;
119
+ this.#remoteTag = 0;
120
+
121
+ // Transmit side.
122
+ this.#localTSN = crypto.randomBytes(4).readUInt32BE(0) >>> 0;
123
+ this.#nextSSN = new Map(); // streamId -> next outbound stream sequence
124
+ this.#sentQueue = new Map(); // tsn -> { chunk } awaiting SACK
125
+
126
+ // Receive side.
127
+ this.#peerCumulativeTSN = null; // highest contiguous TSN received
128
+ this.#receivedOutOfOrder = new Map(); // tsn -> dataChunk (gap storage)
129
+ this.#fragments = new Map(); // streamId -> array of partial DATA payloads
130
+
131
+ this.#initTimer = null;
132
+ }
133
+
134
+ /** Start the association (client sends INIT). */
135
+ start(): void {
136
+ if (this.state !== STATE.CLOSED) return;
137
+ if (this.isClient) {
138
+ this.#sendInit();
139
+ this.state = STATE.COOKIE_WAIT;
140
+ }
141
+ // Server waits for INIT.
142
+ }
143
+
144
+ // ---- packet plumbing ----------------------------------------------------
145
+
146
+ #emitPacket(verificationTag: number, chunks: Buffer[]): void {
147
+ const header = C.encodeCommonHeader(this.#localPort, this.#remotePort, verificationTag);
148
+ const packet = Buffer.concat([header, ...chunks]);
149
+ applyChecksum(packet);
150
+ this.emit('output', packet);
151
+ }
152
+
153
+ /**
154
+ * Feed an inbound SCTP packet (decrypted from DTLS).
155
+ * @param {Buffer} packet
156
+ */
157
+ receivePacket(packet: Buffer): void {
158
+ if (packet.length < 12) return;
159
+ if (!verifyChecksum(packet)) return; // drop corrupt
160
+ const header = C.parseCommonHeader(packet);
161
+ const chunks = C.parseChunks(packet);
162
+ for (const chunk of chunks) {
163
+ this.#handleChunk(chunk, header);
164
+ }
165
+ }
166
+
167
+ #handleChunk(chunk: C.ParsedChunk, _header: C.CommonHeader): void {
168
+ switch (chunk.type) {
169
+ case C.CHUNK_TYPE.INIT:
170
+ this.#handleInit(chunk);
171
+ break;
172
+ case C.CHUNK_TYPE.INIT_ACK:
173
+ this.#handleInitAck(chunk);
174
+ break;
175
+ case C.CHUNK_TYPE.COOKIE_ECHO:
176
+ this.#handleCookieEcho(chunk);
177
+ break;
178
+ case C.CHUNK_TYPE.COOKIE_ACK:
179
+ this.#handleCookieAck();
180
+ break;
181
+ case C.CHUNK_TYPE.DATA:
182
+ this.#handleData(chunk);
183
+ break;
184
+ case C.CHUNK_TYPE.SACK:
185
+ this.#handleSack(chunk);
186
+ break;
187
+ case C.CHUNK_TYPE.HEARTBEAT:
188
+ this.#handleHeartbeat(chunk);
189
+ break;
190
+ case C.CHUNK_TYPE.ABORT:
191
+ this.#abort('peer sent ABORT');
192
+ break;
193
+ case C.CHUNK_TYPE.SHUTDOWN:
194
+ this.#handleShutdown();
195
+ break;
196
+ default:
197
+ break; // ignore unknown
198
+ }
199
+ }
200
+
201
+ // ---- setup handshake ----------------------------------------------------
202
+
203
+ #supportedExtParams(): Buffer[] {
204
+ // Advertise Forward-TSN support (FORWARD_TSN_SUPPORTED) like usrsctp does;
205
+ // we don't require it but including it improves interop.
206
+ return [
207
+ C.encodeParam(C.PARAM_TYPE.FORWARD_TSN_SUPPORTED, Buffer.alloc(0)),
208
+ ];
209
+ }
210
+
211
+ #sendInit(): void {
212
+ const body = C.encodeInitBody({
213
+ initiateTag: this.#localTag,
214
+ a_rwnd: DEFAULT_RWND,
215
+ outStreams: 65535,
216
+ inStreams: 65535,
217
+ initialTSN: this.#localTSN,
218
+ });
219
+ const init = C.encodeChunk(
220
+ C.CHUNK_TYPE.INIT,
221
+ 0,
222
+ Buffer.concat([body, ...this.#supportedExtParams()])
223
+ );
224
+ // INIT must be sent with verification tag 0.
225
+ this.#emitPacket(0, [init]);
226
+ this.#armInitRetransmit([init]);
227
+ }
228
+
229
+ #armInitRetransmit(chunks: Buffer[]): void {
230
+ this.#clearInitTimer();
231
+ let rto = RTO_INITIAL;
232
+ let attempts = 0;
233
+ const fire = (): void => {
234
+ if (this.state === STATE.ESTABLISHED || this.state === STATE.CLOSED) return;
235
+ if (attempts >= 8) { this.#abort('SCTP setup timed out'); return; }
236
+ attempts++;
237
+ this.#emitPacket(this.state === STATE.COOKIE_ECHOED ? this.#remoteTag : 0, chunks);
238
+ rto = Math.min(rto * 2, RTO_MAX);
239
+ this.#initTimer = setTimeout(fire, rto);
240
+ if (this.#initTimer.unref) this.#initTimer.unref();
241
+ };
242
+ this.#initTimer = setTimeout(fire, rto);
243
+ if (this.#initTimer.unref) this.#initTimer.unref();
244
+ }
245
+
246
+ #clearInitTimer(): void {
247
+ if (this.#initTimer) { clearTimeout(this.#initTimer); this.#initTimer = null; }
248
+ }
249
+
250
+ #handleInit(chunk: C.ParsedChunk): void {
251
+ // Server side: reply with INIT_ACK carrying a state cookie.
252
+ const init = C.parseInitBody(chunk.body);
253
+ this.#remoteTag = init.initiateTag;
254
+ this.#peerCumulativeTSN = (init.initialTSN - 1) >>> 0;
255
+
256
+ // State cookie: an opaque blob the peer echoes back. We authenticate it
257
+ // with an HMAC over the parameters we need to resume.
258
+ if (!this.#cookieSecret) this.#cookieSecret = crypto.randomBytes(32);
259
+ const cookieData = Buffer.alloc(16);
260
+ cookieData.writeUInt32BE(this.#localTag, 0);
261
+ cookieData.writeUInt32BE(this.#remoteTag, 4);
262
+ cookieData.writeUInt32BE(this.#localTSN, 8);
263
+ cookieData.writeUInt32BE(init.initialTSN, 12);
264
+ const mac = crypto.createHmac('sha256', this.#cookieSecret).update(cookieData).digest();
265
+ const cookie = Buffer.concat([cookieData, mac]);
266
+
267
+ const ackBody = C.encodeInitBody({
268
+ initiateTag: this.#localTag,
269
+ a_rwnd: DEFAULT_RWND,
270
+ outStreams: 65535,
271
+ inStreams: 65535,
272
+ initialTSN: this.#localTSN,
273
+ });
274
+ const params = Buffer.concat([
275
+ C.encodeParam(C.PARAM_TYPE.STATE_COOKIE, cookie),
276
+ ...this.#supportedExtParams(),
277
+ ]);
278
+ const initAck = C.encodeChunk(C.CHUNK_TYPE.INIT_ACK, 0, Buffer.concat([ackBody, params]));
279
+ // INIT_ACK is sent with the peer's initiate tag as verification tag.
280
+ this.#emitPacket(this.#remoteTag, [initAck]);
281
+ }
282
+
283
+ #handleInitAck(chunk: C.ParsedChunk): void {
284
+ if (this.state !== STATE.COOKIE_WAIT) return;
285
+ this.#clearInitTimer();
286
+ const initAck = C.parseInitBody(chunk.body);
287
+ this.#remoteTag = initAck.initiateTag;
288
+ this.#peerCumulativeTSN = (initAck.initialTSN - 1) >>> 0;
289
+
290
+ // Find the state cookie and echo it back.
291
+ const cookieParam = initAck.params.find((p) => p.type === C.PARAM_TYPE.STATE_COOKIE);
292
+ if (!cookieParam) { this.#abort('INIT_ACK missing state cookie'); return; }
293
+
294
+ const cookieEcho = C.encodeChunk(C.CHUNK_TYPE.COOKIE_ECHO, 0, cookieParam.value);
295
+ this.state = STATE.COOKIE_ECHOED;
296
+ this.#emitPacket(this.#remoteTag, [cookieEcho]);
297
+ this.#armInitRetransmit([cookieEcho]);
298
+ }
299
+
300
+ #handleCookieEcho(chunk: C.ParsedChunk): void {
301
+ // Server side: validate cookie, establish, reply COOKIE_ACK.
302
+ const cookie = chunk.body;
303
+ if (cookie.length >= 48 && this.#cookieSecret) {
304
+ const data = cookie.slice(0, 16);
305
+ const mac = cookie.slice(16, 48);
306
+ const expected = crypto.createHmac('sha256', this.#cookieSecret).update(data).digest();
307
+ if (!crypto.timingSafeEqual(mac, expected)) return; // bad cookie
308
+ // (tags/TSNs already set from the INIT we processed)
309
+ }
310
+ const cookieAck = C.encodeChunk(C.CHUNK_TYPE.COOKIE_ACK, 0, Buffer.alloc(0));
311
+ this.#emitPacket(this.#remoteTag, [cookieAck]);
312
+ this.#establish();
313
+ }
314
+
315
+ #handleCookieAck(): void {
316
+ if (this.state !== STATE.COOKIE_ECHOED) return;
317
+ this.#clearInitTimer();
318
+ this.#establish();
319
+ }
320
+
321
+ #establish(): void {
322
+ if (this.state === STATE.ESTABLISHED) return;
323
+ this.state = STATE.ESTABLISHED;
324
+ this.emit('established');
325
+ }
326
+
327
+ // ---- data transfer ------------------------------------------------------
328
+
329
+ /**
330
+ * Send a user message on a stream.
331
+ * @param {number} streamId
332
+ * @param {number} ppid
333
+ * @param {Buffer} data
334
+ * @param {Object} [opts]
335
+ * @param {boolean} [opts.unordered=false]
336
+ */
337
+ sendData(streamId: number, ppid: number, data: Buffer, opts: { unordered?: boolean } = {}): void {
338
+ if (this.state !== STATE.ESTABLISHED) {
339
+ throw new Error('SCTP association not established');
340
+ }
341
+ const unordered = !!opts.unordered;
342
+
343
+ // Fragment into <= MAX_PAYLOAD pieces; set B/E flags accordingly.
344
+ let ssn = 0;
345
+ if (!unordered) {
346
+ ssn = this.#nextSSN.get(streamId) || 0;
347
+ this.#nextSSN.set(streamId, (ssn + 1) & 0xffff);
348
+ }
349
+
350
+ const total = data.length;
351
+ let offset = 0;
352
+ const chunks: Buffer[] = [];
353
+ // An empty message still needs one DATA chunk (use EMPTY ppid variants).
354
+ do {
355
+ const slice = data.slice(offset, offset + MAX_PAYLOAD);
356
+ const beginning = offset === 0;
357
+ const ending = offset + slice.length >= total;
358
+ const tsn = this.#localTSN;
359
+ this.#localTSN = (this.#localTSN + 1) >>> 0;
360
+ const { flags, body } = C.encodeDataBody({
361
+ tsn, streamId, streamSeq: ssn, ppid, userData: slice,
362
+ unordered, beginning, ending,
363
+ });
364
+ const chunk = C.encodeChunk(C.CHUNK_TYPE.DATA, flags, body);
365
+ this.#sentQueue.set(tsn, { chunk });
366
+ chunks.push(chunk);
367
+ offset += slice.length;
368
+ } while (offset < total);
369
+
370
+ // Send each DATA chunk (one per packet keeps it simple and MTU-safe).
371
+ for (const chunk of chunks) {
372
+ this.#emitPacket(this.#remoteTag, [chunk]);
373
+ }
374
+ }
375
+
376
+ #handleData(chunk: C.ParsedChunk): void {
377
+ const data = C.parseDataBody(chunk.flags, chunk.body);
378
+
379
+ // Always SACK what we've got (delayed-SACK simplified to immediate).
380
+ this.#deliverData(data);
381
+ this.#sendSack();
382
+ }
383
+
384
+ #deliverData(data: C.ParsedDataBody): void {
385
+ // Track cumulative TSN. Accept in-order and buffer out-of-order.
386
+ const expected = ((this.#peerCumulativeTSN as number) + 1) >>> 0;
387
+ if (snLt(data.tsn, expected)) {
388
+ return; // duplicate / already delivered
389
+ }
390
+
391
+ if (data.tsn === expected) {
392
+ this.#peerCumulativeTSN = data.tsn;
393
+ this.#consume(data);
394
+ // Drain any buffered contiguous TSNs.
395
+ let next = (this.#peerCumulativeTSN + 1) >>> 0;
396
+ while (this.#receivedOutOfOrder.has(next)) {
397
+ const buffered = this.#receivedOutOfOrder.get(next) as C.ParsedDataBody;
398
+ this.#receivedOutOfOrder.delete(next);
399
+ this.#peerCumulativeTSN = next;
400
+ this.#consume(buffered);
401
+ next = (this.#peerCumulativeTSN + 1) >>> 0;
402
+ }
403
+ } else {
404
+ // Out of order: buffer for later (gap).
405
+ if (!this.#receivedOutOfOrder.has(data.tsn)) {
406
+ this.#receivedOutOfOrder.set(data.tsn, data);
407
+ }
408
+ }
409
+ }
410
+
411
+ /** Reassemble fragments and emit complete user messages. */
412
+ #consume(data: C.ParsedDataBody): void {
413
+ const key = `${data.streamId}:${data.unordered ? 'u' : 'o'}`;
414
+ if (data.beginning && data.ending) {
415
+ this.emit('message', { streamId: data.streamId, ppid: data.ppid, data: data.userData });
416
+ return;
417
+ }
418
+ let buf = this.#fragments.get(key);
419
+ if (data.beginning) {
420
+ buf = { ppid: data.ppid, parts: [data.userData] };
421
+ this.#fragments.set(key, buf);
422
+ } else if (buf) {
423
+ buf.parts.push(data.userData);
424
+ } else {
425
+ return; // missing beginning; drop
426
+ }
427
+ if (data.ending && buf) {
428
+ this.#fragments.delete(key);
429
+ this.emit('message', {
430
+ streamId: data.streamId,
431
+ ppid: buf.ppid,
432
+ data: Buffer.concat(buf.parts),
433
+ });
434
+ }
435
+ }
436
+
437
+ #sendSack(): void {
438
+ // Build gap-ack blocks from buffered out-of-order TSNs.
439
+ const gapBlocks: Array<[number, number]> = [];
440
+ if (this.#receivedOutOfOrder.size > 0) {
441
+ const sorted = [...this.#receivedOutOfOrder.keys()].sort((a, b) => (snLt(a, b) ? -1 : 1));
442
+ const base = ((this.#peerCumulativeTSN as number) + 1) >>> 0;
443
+ let start: number | null = null;
444
+ let prev: number | null = null;
445
+ for (const tsn of sorted) {
446
+ if (start === null) { start = tsn; prev = tsn; continue; }
447
+ if (tsn === (((prev as number) + 1) >>> 0)) { prev = tsn; continue; }
448
+ gapBlocks.push([((start - base) & 0xffff) + 1, (((prev as number) - base) & 0xffff) + 1]);
449
+ start = tsn; prev = tsn;
450
+ }
451
+ if (start !== null) {
452
+ gapBlocks.push([((start - base) & 0xffff) + 1, (((prev as number) - base) & 0xffff) + 1]);
453
+ }
454
+ }
455
+
456
+ const body = C.encodeSackBody({
457
+ cumulativeTSNAck: (this.#peerCumulativeTSN as number) >>> 0,
458
+ a_rwnd: DEFAULT_RWND,
459
+ gapBlocks,
460
+ });
461
+ const sack = C.encodeChunk(C.CHUNK_TYPE.SACK, 0, body);
462
+ this.#emitPacket(this.#remoteTag, [sack]);
463
+ }
464
+
465
+ #handleSack(chunk: C.ParsedChunk): void {
466
+ const sack = C.parseSackBody(chunk.body);
467
+ // Remove acknowledged TSNs from the retransmit queue.
468
+ for (const tsn of [...this.#sentQueue.keys()]) {
469
+ if (snLte(tsn, sack.cumulativeTSNAck)) {
470
+ this.#sentQueue.delete(tsn);
471
+ }
472
+ }
473
+ // Gap-acked blocks are relative to cumAck; mark those acked too.
474
+ const base = (sack.cumulativeTSNAck + 1) >>> 0;
475
+ for (const [start, end] of sack.gapBlocks) {
476
+ for (let i = start; i <= end; i++) {
477
+ this.#sentQueue.delete((base + i - 1) >>> 0);
478
+ }
479
+ }
480
+ }
481
+
482
+ #handleHeartbeat(chunk: C.ParsedChunk): void {
483
+ // Echo the heartbeat info back as HEARTBEAT_ACK.
484
+ const ack = C.encodeChunk(C.CHUNK_TYPE.HEARTBEAT_ACK, 0, chunk.body);
485
+ this.#emitPacket(this.#remoteTag, [ack]);
486
+ }
487
+
488
+ #handleShutdown(): void {
489
+ const sdAck = C.encodeChunk(C.CHUNK_TYPE.SHUTDOWN_ACK, 0, Buffer.alloc(0));
490
+ this.#emitPacket(this.#remoteTag, [sdAck]);
491
+ this.#close();
492
+ }
493
+
494
+ #abort(reason?: string): void {
495
+ this.#clearInitTimer();
496
+ if (this.state !== STATE.CLOSED) {
497
+ this.state = STATE.CLOSED;
498
+ this.emit('error', new Error(reason || 'SCTP abort'));
499
+ this.emit('close');
500
+ }
501
+ }
502
+
503
+ /** Gracefully close the association. */
504
+ shutdown(): void {
505
+ if (this.state !== STATE.ESTABLISHED) { this.#close(); return; }
506
+ const sd = C.encodeChunk(C.CHUNK_TYPE.SHUTDOWN, 0, (() => {
507
+ const b = Buffer.alloc(4);
508
+ b.writeUInt32BE((this.#peerCumulativeTSN as number) >>> 0, 0);
509
+ return b;
510
+ })());
511
+ this.#emitPacket(this.#remoteTag, [sd]);
512
+ this.#close();
513
+ }
514
+
515
+ #close(): void {
516
+ this.#clearInitTimer();
517
+ if (this.state === STATE.CLOSED) return;
518
+ this.state = STATE.CLOSED;
519
+ this.emit('close');
520
+ }
521
+ }
522
+
523
+ export { SctpAssociation, STATE, SCTP_PORT };