/
endpoint.js
619 lines (570 loc) · 21.9 KB
/
endpoint.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
const crypto = require('crypto')
const EventEmitter = require('events').EventEmitter
const debug = require('debug')
const transport = require('./transport')
const Packet = require('./packet')
const Chunk = require('./chunk')
const Association = require('./association')
const defs = require('./defs')
debug.formatters.h = v => {
return v.toString('hex')
}
class Endpoint extends EventEmitter {
constructor (options) {
super()
options = options || {}
this.ootb = options.ootb
this.localPort = options.localPort
if (options.localAddress && options.localAddress.length > 0) {
this.localAddress = options.localAddress
this.localActiveAddress = options.localAddress[0]
}
this.udpTransport = options.udpTransport
this.debugger = {}
const label = `[${this.localPort}]`
this.debugger.warn = debug(`sctp:endpoint:### ${label}`)
this.debugger.info = debug(`sctp:endpoint:## ${label}`)
this.debugger.debug = debug(`sctp:endpoint:# ${label}`)
this.debugger.trace = debug(`sctp:endpoint: ${label}`)
this.debugger.info('creating endpoint %o', options)
this.a_rwnd = options.a_rwnd || defs.NET_SCTP.RWND
this.MIS = options.MIS || 2
this.OS = options.OS || 2
this.cookieSecretKey = crypto.randomBytes(32)
this.valid_cookie_life = defs.NET_SCTP.valid_cookie_life
this.cookie_hmac_alg = defs.NET_SCTP.cookie_hmac_alg === 'md5' ? 'md5' : 'sha1'
this.cookie_hmac_len = defs.NET_SCTP.cookie_hmac_alg === 'md5' ? 16 : 20
this._cookieInterval = setInterval(() => {
// TODO change interval when valid_cookie_life changes
this.cookieSecretKey = crypto.randomBytes(32)
}, this.valid_cookie_life * 5)
this.associations_lookup = {}
this.associations = []
this.on('icmp', this.onICMP.bind(this))
this.on('packet', this.onPacket.bind(this))
}
onICMP (packet, src, dst, code) {
const association = this._getAssociation(dst, packet.dst_port)
if (association) {
association.emit('icmp', packet, code)
}
}
onPacket (packet, src, dst) {
if (!Array.isArray(packet.chunks)) {
this.debugger.warn('< received empty packet from %s:%d', src, packet.src_port)
return
}
this.debugger.debug('< received packet from %s:%d', src, packet.src_port)
let emulateLoss
if (emulateLoss) {
this.debugger.warn('emulate loss of remote packet')
return
}
let lastDataChunk = -1
let decodedChunks = []
const errors = []
const chunkTypes = {}
let discardPacket = false
// Check if packet should be discarded because of unrecognized chunks
// Also collect errors, chunk types present, decoded chunks
packet.chunks.every((buffer, index) => {
const chunk = Chunk.fromBuffer(buffer)
if (!chunk || chunk.error) {
/*
If the receiver detects a partial chunk, it MUST drop the chunk.
*/
return true
}
if (chunk.chunkType) {
chunkTypes[chunk.chunkType] = chunk
decodedChunks.push(chunk)
chunk.buffer = buffer
if (chunk.chunkType === 'data') {
lastDataChunk = index
} else if (chunk.chunkType === 'init') {
// Ok
} else if (chunk.chunkType === 'abort') {
// Remaining chunks should be ignored
return false
}
} else {
this.debugger.warn('unrecognized chunk %s, action %s', chunk.chunkId, chunk.action)
switch (chunk.action || 0) {
case 0:
/* 00 - Stop processing this SCTP packet and discard it, do not
process any further chunks within it. */
discardPacket = true
return false
case 1:
/* 01 - Stop processing this SCTP packet and discard it, do not
process any further chunks within it, and report the
unrecognized chunk in an 'Unrecognized Chunk Type'. */
discardPacket = true
errors.push({
cause: 'UNRECONGNIZED_CHUNK_TYPE',
unrecognized_chunk: buffer
})
return false
case 2:
/* 10 - Skip this chunk and continue processing. */
break
case 3:
/* 11 - Skip this chunk and continue processing, but report in an
ERROR chunk using the 'Unrecognized Chunk Type' cause of
error. */
errors.push({
cause: 'UNRECONGNIZED_CHUNK_TYPE',
unrecognized_chunk: buffer
})
break
default:
}
}
return true
})
let association = this._getAssociation(src, packet.src_port)
if (association) {
if (errors.length > 0 && !chunkTypes.abort) {
this.debugger.warn('informing unrecognized chunks in packet', errors)
association.ERROR(errors, packet.src)
}
}
if (discardPacket) {
return
}
if (decodedChunks.length === 0) {
return
}
if (!association) {
// 8.4. Handle "Out of the Blue" Packets
this.debugger.debug('Handle "Out of the Blue" Packets')
if (chunkTypes.abort) {
// If the OOTB packet contains an ABORT chunk, the receiver MUST
// silently discard the OOTB packet and take no further action.
this.debugger.debug('OOTB ABORT, discard')
return
}
if (chunkTypes.init) {
/*
If the packet contains an INIT chunk with a Verification Tag set
to '0', process it as described in Section 5.1. If, for whatever
reason, the INIT cannot be processed normally and an ABORT has to
be sent in response, the Verification Tag of the packet
containing the ABORT chunk MUST be the Initiate Tag of the
received INIT chunk, and the T bit of the ABORT chunk has to be
set to 0, indicating that the Verification Tag is NOT reflected.
When an endpoint receives an SCTP packet with the Verification
Tag set to 0, it should verify that the packet contains only an
INIT chunk. Otherwise, the receiver MUST silently discard the
packet.
Furthermore, we require
that the receiver of an INIT chunk MUST enforce these rules by
silently discarding an arriving packet with an INIT chunk that is
bundled with other chunks or has a non-zero verification tag and
contains an INIT-chunk.
*/
if (packet.v_tag === 0 && packet.chunks.length === 1) {
this.onInit(decodedChunks[0], src, dst, packet)
} else {
// all chunks count, including bogus
this.debugger.warn('INIT rules violation, discard')
}
return
} else if (chunkTypes.cookie_echo && decodedChunks[0].chunkType === 'cookie_echo') {
association = this.onCookieEcho(decodedChunks[0], src, dst, packet)
decodedChunks.shift()
if (!association) {
this.debugger.warn('Cookie Echo failed to establish association')
return
}
} else if (chunkTypes.shutdown_ack) {
/*
If the packet contains a SHUTDOWN ACK chunk, the receiver should
respond to the sender of the OOTB packet with a SHUTDOWN
COMPLETE. When sending the SHUTDOWN COMPLETE, the receiver of
the OOTB packet must fill in the Verification Tag field of the
outbound packet with the Verification Tag received in the
SHUTDOWN ACK and set the T bit in the Chunk Flags to indicate
that the Verification Tag is reflected.
*/
const chunk = new Chunk('shutdown_complete', { flags: { T: 1 } })
this._sendPacket(src, packet.src_port, packet.v_tag, [chunk.toBuffer()])
return
} else if (chunkTypes.shutdown_complete) {
/*
If the packet contains a SHUTDOWN COMPLETE chunk, the receiver
should silently discard the packet and take no further action.
*/
this.debugger.debug('OOTB SHUTDOWN COMPLETE, discard')
return
} else if (chunkTypes.error) {
/*
If the packet contains a "Stale Cookie" ERROR or a COOKIE ACK,
the SCTP packet should be silently discarded.
*/
// TODO
this.debugger.debug('OOTB ERROR, discard')
return
} else if (chunkTypes.cookie_ack) {
this.debugger.debug('OOTB COOKIE ACK, discard')
return
} else {
/*
The receiver should respond to the sender of the OOTB packet with
an ABORT. When sending the ABORT, the receiver of the OOTB
packet MUST fill in the Verification Tag field of the outbound
packet with the value found in the Verification Tag field of the
OOTB packet and set the T bit in the Chunk Flags to indicate that
the Verification Tag is reflected. After sending this ABORT, the
receiver of the OOTB packet shall discard the OOTB packet and
take no further action.
*/
if (this.ootb) {
this.debugger.debug('OOTB packet, tolerate')
} else {
this.debugger.debug('OOTB packet, abort')
const chunk = new Chunk('abort', { flags: { T: 1 } })
this._sendPacket(src, packet.src_port, packet.v_tag, [chunk.toBuffer()])
}
return
}
}
if (!association) {
// To be sure
return
}
// all chunks count, including bogus
if (packet.chunks.length > 1 &&
(chunkTypes.init || chunkTypes.init_ack || chunkTypes.shutdown_complete)) {
this.debugger.warn('MUST NOT bundle INIT, INIT ACK, or SHUTDOWN COMPLETE.')
return
}
// 8.5.1. Exceptions in Verification Tag Rules
if (chunkTypes.abort) {
if (
(packet.v_tag === association.my_tag && !chunkTypes.abort.flags.T) ||
(packet.v_tag === association.peer_tag && chunkTypes.abort.flags.T)
) {
/*
An endpoint MUST NOT respond to any received packet
that contains an ABORT chunk (also see Section 8.4)
*/
association.mute = true
// DATA chunks MUST NOT be bundled with ABORT
// TODO. For now we just keep some types
// init_ack will be ignored, cause it needs reply
// all other control chunks are useful
decodedChunks = decodedChunks.filter(chunk =>
chunk.chunkType === 'sack' ||
chunk.chunkType === 'cookie_ack' ||
chunk.chunkType === 'abort'
)
} else {
this.debugger.warn('discard according to Rules for packet carrying ABORT %O', packet)
this.debugger.debug(
'v_tag %d, T-bit %s, my_tag %d, peer_tag %d',
packet.v_tag,
chunkTypes.abort.flags.T,
association.my_tag,
association.peer_tag
)
return
}
} else if (chunkTypes.init) {
if (packet.v_tag !== 0) {
return
}
} else if (chunkTypes.shutdown_complete) {
/*
- The receiver of a SHUTDOWN COMPLETE shall accept the packet if
the Verification Tag field of the packet matches its own tag and
the T bit is not set OR if it is set to its peer's tag and the T
bit is set in the Chunk Flags. Otherwise, the receiver MUST
silently discard the packet and take no further action. An
endpoint MUST ignore the SHUTDOWN COMPLETE if it is not in the
SHUTDOWN-ACK-SENT state.
*/
if (!((packet.v_tag === association.my_tag && !chunkTypes.shutdown_complete.flags.T) ||
(packet.v_tag === association.peer_tag && chunkTypes.shutdown_complete.flags.T))) {
return
}
} else {
// 8.5. Verification Tag
if (packet.v_tag !== association.my_tag) {
this.debugger.warn('discarding packet, v_tag %d != my_tag %d',
packet.v_tag,
association.my_tag
)
return
}
}
// TODO shutdown_ack and shutdown_complete
decodedChunks.forEach((chunk, index) => {
chunk.last_in_packet = index === lastDataChunk
this.debugger.debug('processing chunk %s from %s:%d', chunk.chunkType, src, packet.src_port)
this.debugger.debug('emit chunk %s for association', chunk.chunkType)
association.emit(chunk.chunkType, chunk, src, packet)
})
}
onInit (chunk, src, dst, header) {
this.debugger.info('< CHUNK init', chunk.initiate_tag)
// Check for errors in parameters. Note that chunk can already have parse errors.
const errors = []
if (
chunk.initiate_tag === 0 ||
chunk.a_rwnd < 1500 ||
chunk.inbound_streams === 0 ||
chunk.outbound_streams === 0
) {
/*
If the value of the Initiate Tag in a received INIT chunk is found
to be 0, the receiver MUST treat it as an error and close the
association by transmitting an ABORT.
An SCTP receiver MUST be able to receive a minimum of 1500 bytes in
one SCTP packet. This means that an SCTP endpoint MUST NOT indicate
less than 1500 bytes in its initial a_rwnd sent in the INIT or INIT
ACK.
A receiver of an INIT with the MIS value of 0 SHOULD abort
the association.
Note: A receiver of an INIT with the OS value set to 0 SHOULD
abort the association.
Invalid Mandatory Parameter: This error cause is returned to the
originator of an INIT or INIT ACK chunk when one of the mandatory
parameters is set to an invalid value.
*/
errors.push({ cause: 'INVALID_MANDATORY_PARAMETER' })
}
if (errors.length > 0) {
const abort = new Chunk('abort', { error_causes: errors })
this._sendPacket(src, header.src_port, chunk.initiate_tag, [abort.toBuffer()])
return
}
const myTag = crypto.randomBytes(4).readUInt32BE(0)
const cookie = this.createCookie(chunk, header, myTag)
const initAck = new Chunk('init_ack', {
initiate_tag: myTag,
initial_tsn: myTag,
a_rwnd: this.a_rwnd,
state_cookie: cookie,
outbound_streams: chunk.inbound_streams,
inbound_streams: this.MIS
})
if (this.localAddress) {
initAck.ipv4_address = this.localAddress
}
if (chunk.errors) {
this.debugger.warn('< CHUNK has errors (unrecognized parameters)', chunk.errors)
initAck.unrecognized_parameter = chunk.errors
}
this.debugger.trace('> sending cookie', cookie)
this._sendPacket(src, header.src_port, chunk.initiate_tag, [initAck.toBuffer()])
/*
After sending the INIT ACK with the State Cookie parameter, the
sender SHOULD delete the TCB and any other local resource related to
the new association, so as to prevent resource attacks.
*/
}
onCookieEcho (chunk, src, dst, header) {
this.debugger.info('< CHUNK cookie_echo ', chunk.cookie)
/*
If the State Cookie is valid, create an association to the sender
of the COOKIE ECHO chunk with the information in the TCB data
carried in the COOKIE ECHO and enter the ESTABLISHED state.
*/
const cookieData = this.validateCookie(chunk.cookie, header)
if (cookieData) {
this.debugger.trace('cookie is valid')
const initChunk = Chunk.fromBuffer(cookieData.initChunk)
if (initChunk.chunkType !== 'init') {
this.debugger.warn('--> this should be init chunk', initChunk)
throw new Error('bug in chunk validation function')
}
const options = {
remoteAddress: src,
my_tag: cookieData.my_tag,
remotePort: cookieData.src_port,
MIS: this.MIS,
OS: this.OS
}
const association = new Association(this, options)
this.emit('association', association)
association.acceptRemote(initChunk)
return association
}
}
_sendPacket (host, port, vTag, chunks, callback) {
this.debugger.debug('> send packet %d chunks %s -> %s:%d vTag %d',
chunks.length,
this.localActiveAddress,
host,
port,
vTag
)
const packet = new Packet(
{
src_port: this.localPort,
dst_port: port,
v_tag: vTag
},
chunks
)
// TODO multi-homing select active address
this.transport.sendPacket(this.localActiveAddress, host, packet, callback)
}
createCookie (chunk, header, myTag) {
const created = Math.floor(new Date() / 1000)
const information = Buffer.alloc(16)
information.writeUInt32BE(created, 0)
information.writeUInt32BE(this.valid_cookie_life, 4)
information.writeUInt16BE(header.src_port, 8)
information.writeUInt16BE(header.dst_port, 10)
information.writeUInt32BE(myTag, 12)
const hash = crypto.createHash(this.cookie_hmac_alg)
hash.update(information)
/*
The receiver of the PAD
parameter MUST silently discard this parameter and continue
processing the rest of the INIT chunk. This means that the size of
the generated COOKIE parameter in the INIT-ACK MUST NOT depend on the
existence of the PAD parameter in the INIT chunk. A receiver of a
PAD parameter MUST NOT include the PAD parameter within any State
Cookie parameter it generates.
Note: sctp_test doesn't follow this rule.
*/
delete chunk.pad
const strippedInit = new Chunk('init', chunk)
const initBuffer = strippedInit.toBuffer()
hash.update(initBuffer)
hash.update(this.cookieSecretKey)
const mac = hash.digest()
this.debugger.debug('created cookie mac %h %d bytes', mac, mac.length)
/*
0 1 2 3 4
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MAC | Information | INIT chunk ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MAC | time | life |spt|dpt| my tag | INIT chunk ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
return Buffer.concat([mac, information, initBuffer])
}
validateCookie (cookie, header) {
let result
// MAC 16 + Info 16 + Init chunk 20 = 52
if (cookie.length < 52) {
return
}
const receivedMAC = cookie.slice(0, this.cookie_hmac_len)
const information = cookie.slice(this.cookie_hmac_len, this.cookie_hmac_len + 16)
const initChunk = cookie.slice(this.cookie_hmac_len + 16)
/*
Compute a MAC using the TCB data carried in the State Cookie and
the secret key (note the timestamp in the State Cookie MAY be
used to determine which secret key to use).
*/
const hash = crypto.createHash(defs.NET_SCTP.cookie_hmac_alg)
hash.update(information)
hash.update(initChunk)
hash.update(this.cookieSecretKey)
const mac = hash.digest()
/*
Authenticate the State Cookie as one that it previously generated
by comparing the computed MAC against the one carried in the
State Cookie. If this comparison fails, the SCTP header,
including the COOKIE ECHO and any DATA chunks, should be silently
discarded
*/
if (mac.equals(receivedMAC)) {
result = {
created: new Date(information.readUInt32BE(0) * 1000),
cookie_lifespan: information.readUInt32BE(4),
src_port: information.readUInt16BE(8),
dst_port: information.readUInt16BE(10),
my_tag: information.readUInt32BE(12)
}
/*
Compare the port numbers and the Verification Tag contained
within the COOKIE ECHO chunk to the actual port numbers and the
Verification Tag within the SCTP common header of the received
header. If these values do not match, the packet MUST be
silently discarded.
*/
if (
header.src_port === result.src_port &&
header.dst_port === result.dst_port &&
header.v_tag === result.my_tag
) {
/*
Compare the creation timestamp in the State Cookie to the current
local time. If the elapsed time is longer than the lifespan
carried in the State Cookie, then the packet, including the
COOKIE ECHO and any attached DATA chunks, SHOULD be discarded,
and the endpoint MUST transmit an ERROR chunk with a "Stale
Cookie" error cause to the peer endpoint.
*/
if (new Date() - result.created < result.cookie_lifespan) {
result.initChunk = initChunk
return result
}
} else {
this.debugger.warn('port verification error', header, result)
}
} else {
this.debugger.warn('mac verification error %h != %h', receivedMAC, mac)
}
}
close () {
this.emit('close')
this.associations.forEach(association => {
association.emit('COMMUNICATION LOST')
association._destroy()
})
this._destroy()
}
_destroy () {
clearInterval(this._cookieInterval)
this.transport.unallocate(this.localPort)
}
_getAssociation (host, port) {
const key = host + ':' + port
this.debugger.trace('trying to find association for %s', key)
return this.associations_lookup[key]
}
ASSOCIATE (options) {
/*
Format: ASSOCIATE(local SCTP instance name,
destination transport addr, outbound stream count)
-> association id [,destination transport addr list]
[,outbound stream count]
*/
this.debugger.info('API ASSOCIATE', options)
options = options || {}
if (!options.remotePort) {
throw new Error('port is required')
}
options.OS = options.OS || this.OS
options.MIS = options.MIS || this.MIS
const association = new Association(this, options)
association.init()
return association
}
DESTROY () {
/*
Format: DESTROY(local SCTP instance name)
*/
this.debugger.trace('API DESTROY')
this._destroy()
}
static INITIALIZE (options, transportOptions, callback) {
const endpoint = new Endpoint(options)
// TODO register is synchronous for now, but could be async
const port = transport.register(endpoint, transportOptions)
if (port) {
callback(null, endpoint)
} else {
callback(new Error('bind EADDRINUSE 0.0.0.0:' + options.localPort))
}
}
}
module.exports = Endpoint