├── .gitignore ├── test ├── .eslintrc └── test.js ├── .editorconfig ├── examples ├── listen.js ├── self.js ├── socket.js ├── loop.js └── server.js ├── .eslintrc ├── LICENSE ├── lib ├── index.js ├── packet.js ├── serial.js ├── chunk.js ├── reassembly.js ├── transport.js ├── sockets.js ├── defs.js ├── endpoint.js └── association.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "max-len": [ 4 | "error", 5 | { 6 | "code": 300 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /examples/listen.js: -------------------------------------------------------------------------------- 1 | const sctp = require('../lib/') 2 | 3 | const sock1 = sctp.connect({ 4 | passive: true, 5 | localPort: 3565, 6 | host: '127.0.0.1', 7 | port: 3566 8 | }) 9 | 10 | sock1.on('connect', () => { 11 | console.log('remote connected') 12 | }) 13 | 14 | sock1.on('end', () => { 15 | console.log('remote end') 16 | }) 17 | 18 | const sock2 = sctp.connect({ 19 | protocol: sctp.PPID.M2PA, 20 | host: '127.0.0.1', 21 | localPort: 3566, 22 | port: 3565 23 | }) 24 | 25 | sock2.on('connect', () => { 26 | console.log('socket connected') 27 | sock2.write(Buffer.from('01000b020000001400ffffff00ffffff00000009', 'hex')) 28 | sock2.end() 29 | }) 30 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ], 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | "jest": true 9 | }, 10 | "rules": { 11 | "no-undef": "error", 12 | "no-unused-vars": "error", 13 | "no-warning-comments": "off", 14 | "no-mixed-operators": "off", 15 | "guard-for-in": "off", 16 | "max-params": [ 17 | "warn", 18 | { 19 | "max": 5 20 | } 21 | ], 22 | "new-cap": "off", 23 | "max-len": [ 24 | "error", 25 | { 26 | "code": 100 27 | } 28 | ], 29 | "camelcase": [ 30 | "error", 31 | { 32 | "properties": "never" 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/self.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const sctp = require('../lib') 4 | 5 | const server = sctp.createServer() 6 | 7 | server.on('connection', socket => { 8 | console.log('remote socket connected from', socket.remoteAddress, socket.remotePort) 9 | socket.on('data', data => { 10 | console.log('server socket received data', data) 11 | socket.write(Buffer.from('010003040000001000110008000003ea', 'hex')) 12 | }) 13 | }) 14 | 15 | server.listen({port: 2905}, () => { 16 | console.log('server listening') 17 | }) 18 | 19 | const socket = sctp.connect({host: '127.0.0.1', port: 2905}, () => { 20 | console.log('socket connected') 21 | socket.write(Buffer.from('010003010000001000110008000003ea', 'hex')) 22 | }) 23 | 24 | socket.on('data', buffer => { 25 | console.log('socket received data from server', buffer) 26 | socket.end() 27 | server.close() 28 | process.exit() 29 | }) 30 | -------------------------------------------------------------------------------- /examples/socket.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | const fs = require('fs') 3 | const util = require('util') 4 | 5 | const sctp = require('../lib') 6 | 7 | const [node, script, host, port] = process.argv 8 | console.log(node, script, host, port) 9 | 10 | let start 11 | 12 | const socket = sctp.connect({host, port}, () => { 13 | console.log('socket connected') 14 | start = Date.now() 15 | fs.createReadStream(node).pipe(socket) 16 | }) 17 | 18 | socket.on('error', error => { 19 | console.error(error.message) 20 | }) 21 | 22 | const size = fs.statSync(node).size 23 | socket.on('end', () => { 24 | const duration = Date.now() - start 25 | const rateIn = Math.floor(socket.bytesRead / duration / 1024 / 1024 * 100000) / 100 26 | const rateOut = Math.floor(socket.bytesWritten / duration / 1024 / 1024 * 100000) / 100 27 | console.log( 28 | util.format( 29 | 'file size %d, %d bytes read (rate %d MB/s), %d bytes sent (rate %d MB/s)', 30 | size, socket.bytesRead, rateIn, socket.bytesWritten, rateOut 31 | ) 32 | ) 33 | // Close 34 | // socket.end() 35 | process.exit() 36 | }) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dmitriy Tsvettsikh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | RFC 4960 "Stream Control Transmission Protocol" 4 | https://tools.ietf.org/html/rfc4960 5 | 6 | */ 7 | 8 | const defs = require('./defs') 9 | const sockets = require('./sockets') 10 | const Reassembly = require('./reassembly') 11 | const Packet = require('./packet') 12 | const Chunk = require('./chunk') 13 | 14 | const Socket = sockets.Socket 15 | const Server = sockets.Server 16 | 17 | function createServer(options, connectionListener) { 18 | return new Server(options, connectionListener) 19 | } 20 | 21 | function connect(options, connectListener) { 22 | const socket = new Socket(options) 23 | setImmediate(() => { 24 | socket.connect(options, connectListener) 25 | }) 26 | return socket 27 | } 28 | 29 | function defaults(params) { 30 | params = params || {} 31 | for (const param in defs.NET_SCTP) { 32 | if (param in params) { 33 | // Todo validate all 34 | defs.NET_SCTP[param] = params[param] 35 | } 36 | } 37 | if (defs.NET_SCTP.sack_timeout > 500) { 38 | defs.NET_SCTP.sack_timeout = 500 39 | } 40 | if (defs.NET_SCTP.RWND < 1500) { 41 | defs.NET_SCTP.RWND = 1500 42 | } 43 | return defs.NET_SCTP 44 | } 45 | 46 | module.exports = { 47 | createServer, 48 | connect, 49 | createConnection: connect, 50 | Socket, 51 | Server, 52 | Reassembly, 53 | Packet, 54 | Chunk, 55 | PPID: defs.PPID, 56 | defaults 57 | } 58 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const Packet = require('../lib/packet') 3 | const Chunk = require('../lib/chunk') 4 | 5 | describe('Packet', () => { 6 | describe('functions', () => { 7 | const chunk = new Chunk('init', { 8 | message: 'init', 9 | initiate_tag: 2925606774, 10 | a_rwnd: 62464, 11 | outbound_streams: 10, 12 | inbound_streams: 65535, 13 | initial_tsn: 1553697926, 14 | ipv4_address: ['10.211.55.18', '10.211.55.19', '10.211.55.20'], 15 | supported_address_type: 5, 16 | ecn: true, 17 | forward_tsn_supported: true 18 | }) 19 | const packet = new Packet( 20 | { 21 | src_port: 10000, 22 | dst_port: 10000, 23 | v_tag: 483748 24 | }, 25 | [chunk.toBuffer()] 26 | ) 27 | 28 | it('chunk creation', () => { 29 | expect(chunk.toBuffer().toString('hex')).to.equal( 30 | '0100003cae6137760000f400000affff5c9b8c86000500080ad33712000500080ad33713000500080ad33714000c00060005000080000004c0000004' 31 | ) 32 | }) 33 | it('packet creation', () => { 34 | expect(packet.toBuffer().toString('hex')).to.equal( 35 | '27102710000761a4725ad0b70100003cae6137760000f400000affff5c9b8c86000500080ad33712000500080ad33713000500080ad33714000c00060005000080000004c0000004' 36 | ) 37 | }) 38 | it('packet roundtrip translation', () => { 39 | expect(Packet.fromBuffer(packet.toBuffer())).to.deep.equal(packet) 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodertc/sctp", 3 | "version": "0.1.0", 4 | "author": "Dmitry Tsvettsikh ", 5 | "contributors": [ 6 | { 7 | "name": "Vladimir Latyshev", 8 | "email": "latysheff@gmail.com" 9 | } 10 | ], 11 | "engines": { 12 | "node": ">=6.0.0" 13 | }, 14 | "dependencies": { 15 | "debug": "^3.1.0", 16 | "ip": "^1.1.5", 17 | "turbo-crc32": "^1.0.0" 18 | }, 19 | "devDependencies": { 20 | "chai": "^4.1.2", 21 | "eslint": "^4.16.0", 22 | "mocha": "^5.0.0", 23 | "supports-color": "^5.1.0", 24 | "xo": "^0.18.2" 25 | }, 26 | "description": "SCTP network protocol (RFC4960) in plain js", 27 | "keywords": [ 28 | "RFC4960", 29 | "SCTP", 30 | "Sigtran", 31 | "SS7", 32 | "RFC8261", 33 | "DTLS", 34 | "WebRTC" 35 | ], 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/nodertc/sctp/issues" 39 | }, 40 | "homepage": "https://github.com/nodertc/sctp#readme", 41 | "main": "lib/index.js", 42 | "scripts": { 43 | "test": "xo && mocha" 44 | }, 45 | "xo": { 46 | "space": true, 47 | "semicolon": false, 48 | "extends": ".eslintrc", 49 | "overrides": [ 50 | { 51 | "files": "test/*.js", 52 | "rules": { 53 | "max-len": [ 54 | "error", 55 | { 56 | "code": 300 57 | } 58 | ] 59 | } 60 | } 61 | ] 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "git+https://github.com/nodertc/sctp.git" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/loop.js: -------------------------------------------------------------------------------- 1 | const sctp = require('../lib/') 2 | 3 | sctp.defaults({ 4 | sack_timeout: 200, 5 | rto_initial: 500, 6 | rto_min: 500, 7 | rto_max: 1000 8 | }) 9 | 10 | const server = sctp.createServer({logger: null}) 11 | 12 | server.on('connection', socket => { 13 | console.log( 14 | 'remote socket connected from', 15 | socket.remoteAddress, 16 | socket.remotePort 17 | ) 18 | // Socket.end(); 19 | socket.on('data', data => { 20 | console.log('server socket received data', data) 21 | // Socket.write(Buffer.from('010003040000001000110008000003ea', 'hex')) 22 | }) 23 | socket.on('error', () => { 24 | // ignore 25 | }) 26 | }) 27 | 28 | server.listen({ 29 | port: 3000 30 | }) 31 | 32 | let count = 1 33 | const maxcount = 1000 34 | const start = new Date() 35 | 36 | const interval = setInterval(() => { 37 | if (count > maxcount) { 38 | clearInterval(interval) 39 | console.log( 40 | 'average socket creation time, ms', 41 | (new Date() - start) / maxcount 42 | ) 43 | return 44 | } 45 | newsocket() 46 | }, 1) 47 | 48 | function newsocket() { 49 | count++ 50 | const sctpSocket = sctp.connect( 51 | { 52 | protocol: sctp.M3UA, 53 | host: '127.0.0.1', 54 | // Host: '10.192.169.102', 55 | port: 3000 56 | }, 57 | () => { 58 | // Console.log('sctp socket connected',i) 59 | } 60 | ) 61 | sctpSocket.on('connect', () => { 62 | // Console.log('socket connected', i) 63 | // sctpSocket.write(Buffer.from('010003010000001000110008000003ea', 'hex')) 64 | let packet = 0 65 | const interv = setInterval(() => { 66 | sctpSocket.write(Buffer.from('010003010000001000110008000003ea', 'hex')) 67 | if (packet++ === 100) { 68 | // Console.log('finish socket' + count) 69 | clearInterval(interv) 70 | sctpSocket.end() 71 | } 72 | }, 10) 73 | // SctpSocket.end() 74 | }) 75 | sctpSocket.on('error', () => { 76 | // ignore 77 | }) 78 | } 79 | 80 | newsocket() 81 | -------------------------------------------------------------------------------- /lib/packet.js: -------------------------------------------------------------------------------- 1 | const crc32c = require('turbo-crc32/crc32c') 2 | 3 | class Packet { 4 | constructor(headers, chunks) { 5 | if (Buffer.isBuffer(headers)) { 6 | this.fromBuffer(headers) 7 | return 8 | } 9 | headers = headers || {} 10 | this.src_port = headers.src_port 11 | this.dst_port = headers.dst_port 12 | this.v_tag = headers.v_tag 13 | this.checksum = 0x00000000 14 | this.chunks = chunks 15 | } 16 | 17 | fromBuffer(buffer) { 18 | // Todo failsafe 19 | this.src_port = buffer.readUInt16BE(0) 20 | this.dst_port = buffer.readUInt16BE(2) 21 | this.v_tag = buffer.readUInt32BE(4) 22 | if (buffer.length === 8) { 23 | return 24 | } 25 | this.checksum = buffer.readUInt32LE(8) 26 | buffer.writeUInt32LE(0x00000000, 8) 27 | const checksum = crc32c(buffer) 28 | buffer.writeUInt32LE(this.checksum, 8) 29 | if (this.checksum !== checksum) { 30 | this.checksum_error = true 31 | this.checksum_expected = checksum 32 | // Return 33 | } 34 | let offset = 12 35 | this.chunks = [] 36 | while (offset + 4 <= buffer.length) { 37 | const length = buffer.readUInt16BE(offset + 2) 38 | if (!length) { 39 | return 40 | } 41 | if (offset + length > buffer.length) { 42 | this.length_error = true 43 | return 44 | } 45 | const chunk = buffer.slice(offset, offset + length) 46 | this.chunks.push(chunk) 47 | offset += length 48 | const padding = length % 4 49 | if (padding) { 50 | offset += 4 - padding 51 | } 52 | } 53 | } 54 | 55 | static fromBuffer(buffer) { 56 | if (buffer.length < 8) { 57 | return false 58 | } 59 | return new Packet(buffer) 60 | } 61 | 62 | toBuffer() { 63 | if (!Array.isArray(this.chunks)) { 64 | this.chunks = [] 65 | } 66 | const headers = Buffer.alloc(12) 67 | headers.writeUInt16BE(this.src_port, 0) 68 | headers.writeUInt16BE(this.dst_port, 2) 69 | headers.writeUInt32BE(this.v_tag, 4) 70 | headers.writeUInt32LE(0x00000000, 8) 71 | const buffer = Buffer.concat([headers, ...this.chunks]) 72 | this.checksum = crc32c(buffer, 0) 73 | buffer.writeUInt32LE(this.checksum, 8) 74 | return buffer 75 | } 76 | } 77 | 78 | module.exports = Packet 79 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const util = require('util') 4 | const fs = require('fs') 5 | 6 | const ip = require('ip') 7 | const sctp = require('../lib') 8 | 9 | const port = 3000 10 | 11 | sctp.defaults({ 12 | rto_initial: 500, 13 | rto_min: 300, 14 | rto_max: 1000, 15 | sack_timeout: 100, 16 | sack_freq: 2 17 | }) 18 | const fileName = '' 19 | 20 | let count = 0 21 | const server = sctp.createServer(socket => { 22 | const start = Date.now() 23 | count = 0 24 | console.log( 25 | 'remote socket connected from', 26 | socket.remoteAddress, 27 | socket.remotePort 28 | ) 29 | if (fileName) { 30 | socket.pipe(fs.createWriteStream(fileName)) 31 | } 32 | 33 | const streamOut = socket.createStream(2) 34 | 35 | streamOut.on('error', error => { 36 | console.log(error.message) 37 | }) 38 | 39 | socket.on('stream', (streamIn, id) => { 40 | console.log('< new sctp stream', id) 41 | // Uncomment to receive data 42 | streamIn.on('data', data => { 43 | // Incoming data 44 | // console.log('< received data on stream', data.length, 'bytes') 45 | streamOut.write(data) 46 | }) 47 | }) 48 | 49 | socket.on('data', () => { 50 | count++ 51 | // Io impacts performance 52 | // console.log('< server received', data.length, 'bytes') 53 | // Send data back 54 | // if (!socket.destroyed) socket.write(data) 55 | }) 56 | 57 | socket.on('error', error => { 58 | console.log(error.message) 59 | }) 60 | 61 | socket.on('end', () => { 62 | const duration = Date.now() - start 63 | const rate = Math.floor(socket.bytesRead / duration / 1024 / 1024 * 100000) / 100 64 | const ratePackets = ~~(count / duration * 1000) 65 | console.log( 66 | util.format( 67 | '%d packets, %d bytes read, %d bytes sent, rate %d MB/s, %d packets/sec', 68 | count, socket.bytesRead, socket.bytesWritten, rate, ratePackets 69 | ) 70 | ) 71 | if (fileName) { 72 | console.log('Contents of piped file (first 100 bytes):') 73 | console.log(fs.readFileSync(fileName).slice(0, 100).toString()) 74 | } 75 | }) 76 | }) 77 | 78 | server.on('error', error => { 79 | console.error(error.message) 80 | process.exit() 81 | }) 82 | 83 | server.listen({OS: 10, MIS: 10, port}, () => { 84 | console.log('server started on port %d', port) 85 | console.log('now run test, for example:') 86 | console.log( 87 | util.format('sctp_test -H -h <%s or other local ip> -p %d -s -P -x 10000 -d0 -c 1', 88 | ip.address(), port) 89 | ) 90 | }) 91 | 92 | process.on('SIGINT', () => { 93 | console.log('SIGINT') 94 | // Todo close socket 95 | setTimeout(() => { 96 | console.log('exiting') 97 | process.exit() 98 | }, 100) 99 | }) 100 | -------------------------------------------------------------------------------- /lib/serial.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | RFC 1982 Serial Number Arithmetic 4 | 5 | Taken from serial-arithmetic module and modified 6 | 7 | */ 8 | 9 | // todo valueOf() 10 | 11 | class SerialNumber { 12 | constructor(value, size) { 13 | value = value || 0 14 | size = size || 32 15 | this.serialBits = size 16 | this.serialBytes = size / 8 17 | this._value = value 18 | this._max = Math.pow(2, this.serialBits) 19 | this._half = Math.pow(2, this.serialBits - 1) 20 | this._maxAdd = this._half - 1 21 | this.number = this._value % this._max 22 | } 23 | 24 | toString() { 25 | return `` 26 | // Return '' + this.number 27 | } 28 | 29 | getNumber(options) { 30 | options = options || {} 31 | options.radix = options.radix || 10 32 | options.string = options.string || true 33 | 34 | let number = this.number.toString(options.radix) 35 | 36 | if (options.encoding === 'BE') { 37 | const buf = Buffer.from(this.serialBytes) 38 | buf.writeUIntLE(this.number, 0, this.serialBytes) 39 | number = buf.readUIntBE(0, this.serialBytes).toString(options.radix) 40 | } 41 | 42 | if (options.string) { 43 | return number 44 | } 45 | console.log(number) 46 | return parseInt(number, options.radix) 47 | } 48 | 49 | eq(that) { 50 | return this.number === that.number 51 | } 52 | 53 | ne(that) { 54 | return this.number !== that.number 55 | } 56 | 57 | lt(that) { 58 | return ( 59 | (this.number < that.number && that.number - this.number < this._half) || 60 | (this.number > that.number && this.number - that.number > this._half) 61 | ) 62 | } 63 | 64 | gt(that) { 65 | return ( 66 | (this.number < that.number && that.number - this.number > this._half) || 67 | (this.number > that.number && this.number - that.number < this._half) 68 | ) 69 | } 70 | 71 | le(that) { 72 | return this.eq(that) || this.lt(that) 73 | } 74 | 75 | ge(that) { 76 | return this.eq(that) || this.gt(that) 77 | } 78 | 79 | delta(that) { 80 | let result = this.number - that.number 81 | if (result < 0 && result < -this._half) { 82 | result += this._max 83 | } 84 | return result 85 | } 86 | 87 | inc(delta = 1) { 88 | this.number = (this.number + delta) % this._max 89 | return this 90 | } 91 | 92 | copy() { 93 | return new SerialNumber(this.number, this.serialBits) 94 | } 95 | 96 | prev() { 97 | return new SerialNumber( 98 | this.number === 0 ? this._max : this.number - 1, 99 | this.serialBits 100 | ) 101 | } 102 | 103 | next() { 104 | return new SerialNumber( 105 | this.number === this._max ? 0 : this.number + 1, 106 | this.serialBits 107 | ) 108 | } 109 | } 110 | 111 | module.exports = SerialNumber 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @nodertc/sctp 2 | 3 | [![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg)](https://github.com/emersion/stability-badges#experimental) 4 | [![Build Status](https://travis-ci.org/nodertc/sctp.svg?branch=master)](https://travis-ci.org/nodertc/sctp) 5 | [![npm](https://img.shields.io/npm/v/@nodertc/sctp.svg)](https://npmjs.org/package/@nodertc/sctp) 6 | [![node](https://img.shields.io/node/v/@nodertc/sctp.svg)](https://npmjs.org/package/@nodertc/sctp) 7 | [![license](https://img.shields.io/npm/l/@nodertc/sctp.svg)](https://npmjs.org/package/@nodertc/sctp) 8 | [![downloads](https://img.shields.io/npm/dm/@nodertc/sctp.svg)](https://npmjs.org/package/@nodertc/sctp) 9 | 10 | SCTP network protocol [RFC4960](https://tools.ietf.org/html/rfc4960) in plain js 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm i @nodertc/sctp 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | const securesocket = dtls.connect(/*...*/); 22 | 23 | const socket = sctp.connect({ 24 | localPort: 5000, 25 | port: 5000, 26 | transport: securesocket, 27 | }); 28 | 29 | socket.on('connect', socket => { 30 | console.log('socket connected') 31 | socket.write(Buffer.from('010003010000001000110008000003ea', 'hex')) 32 | }) 33 | 34 | socket.on('data', buffer => { 35 | console.log('socket received data from server', buffer.toString()) 36 | socket.end() 37 | }) 38 | ``` 39 | 40 | In UDP mode host and localAddress will be ignored, 41 | because addressing is provided by underlying transport. 42 | 43 | Also note that in most cases "passive" connect is a better alternative to creating server. 44 | 45 | **passive** option disables active connect to remote peer. 46 | Socket waits for remote connection, 47 | allowing it only from indicated remote port. 48 | This unusual option doesn't exist in TCP API. 49 | 50 | ### new net.Socket([options]) 51 | * options [Object] 52 | 53 | For SCTP socketss, available options are: 54 | 55 | * ppid [number] Payload protocol id (see below) 56 | * stream_id [number] SCTP stream id. Default: 0 57 | * unordered [boolean] Indicate unordered mode. Default: false 58 | * no_bundle [boolean] Disable chunk bundling. Default: false 59 | 60 | Note: SCTP does not support a half-open state (like TCP) 61 | wherein one side may continue sending data while the other end is closed. 62 | 63 | ### socket.connect(options[, connectListener]) 64 | * options [Object] 65 | * connectListener [Function] Common parameter of socket.connect() methods. 66 | Will be added as a listener for the 'connect' event once. 67 | 68 | For SCTP connections, available options are: 69 | 70 | * port [number] Required. Port the socket should connect to. 71 | * host [string] Host the socket should connect to. Default: 'localhost' 72 | * localAddress [string] Local address the socket should connect from. 73 | * localPort [number] Local port the socket should connect from. 74 | * MIS [number] Maximum inbound streams. Default: 2 75 | * OS [number] Requested outbound streams. Default: 2 76 | * passive [boolean] Indicates passive mode. Socket will not connect, 77 | but allow connection of remote socket from host:port. Default: false 78 | * transport [stream.Duplex] Any valid Duplex stream. 79 | 80 | ### socket.createStream(id) 81 | Creates SCTP stream with stream id. Those are SCTP socket sub-streams. 82 | 83 | > After the association is initialized, the valid outbound stream 84 | identifier range for either endpoint shall be 0 to min(local OS, remote MIS)-1. 85 | 86 | You can check this negotiated value by referring to `socket.OS` 87 | after 'connect' event. id should be less the socket.OS. 88 | 89 | Result is stream.Writable. 90 | 91 | ``` 92 | const stream = socket.createStream(1) 93 | stream.write('some data') 94 | ``` 95 | 96 | ### Socket events 97 | See [Net] module documentation. 98 | 99 | For SCTP additional event 'stream' is defined. 100 | It signals that incoming data chunk were noticed with new SCTP stream id. 101 | 102 | ``` 103 | socket.on('stream', (stream, id) => { 104 | stream.on('data', data => { 105 | // Incoming data 106 | }) 107 | }) 108 | ``` 109 | 110 | ### sctp.defaults(options) 111 | Function sets default module parameters. Names follow net.sctp conventions. Returns current default parameters. 112 | 113 | See `sysctl -a | grep sctp` 114 | 115 | Example: 116 | 117 | ``` 118 | sctp.defaults({ 119 | rto_initial: 500, 120 | rto_min: 300, 121 | rto_max: 1000, 122 | sack_timeout: 150, 123 | sack_freq: 2, 124 | }) 125 | ``` 126 | 127 | ### sctp.PPID 128 | sctp.PPID is an object with [SCTP Payload Protocol Identifiers][ppid] 129 | 130 | ``` 131 | { 132 | SCTP: 0, 133 | IUA: 1, 134 | M2UA: 2, 135 | M3UA: 3, 136 | SUA: 4, 137 | M2PA: 5, 138 | V5UA: 6, 139 | H248: 7, 140 | BICC: 8, 141 | ... 142 | } 143 | ``` 144 | 145 | ## RFC to implement 146 | * [3758 Partial Reliability Extension][RFC3758] 147 | * [4820 Padding Chunk and Parameter][RFC4820] 148 | * [4895 Authenticated Chunks][RFC4895] 149 | * [5061 Dynamic Address Reconfiguration][RFC5061] 150 | * [5062 Security Attacks Found Against SCTP and Current Countermeasures][RFC5062] 151 | * [6525 Stream Reconfiguration][RFC6525] 152 | * [7053 SACK-IMMEDIATELY Extension (I-bit)][RFC7053] 153 | * [7496 Additional Policies for the Partially Reliable Extension][RFC7496] 154 | * [7829 SCTP-PF: A Quick Failover Algorithm][RFC7829] 155 | * [8260 Stream Schedulers and User Message Interleaving (I-DATA Chunk)][RFC8260] 156 | * [Draft: ECN for Stream Control Transmission Protocol][ECN] 157 | 158 | ## License 159 | 160 | - MIT, 2017-2018 © Vladimir Latyshev 161 | - MIT, 2018 © Dmitriy Tsvettsikh 162 | 163 | [raw-socket]: https://www.npmjs.com/package/raw-socket 164 | [Net]: https://nodejs.org/api/net.html 165 | [UDP]: https://nodejs.org/api/dgram.html 166 | [RTCDataChannel]: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel 167 | [RFC4960]: https://tools.ietf.org/html/rfc4960 168 | [RFC6458]: https://tools.ietf.org/html/rfc6458 169 | [RFC8261]: https://tools.ietf.org/html/rfc8261 170 | [smpp]: https://www.npmjs.com/package/smpp 171 | [ppid]: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 172 | [RFC3758]: https://tools.ietf.org/html/rfc3758 173 | [RFC4820]: https://tools.ietf.org/html/rfc4820 174 | [RFC4895]: https://tools.ietf.org/html/rfc4895 175 | [RFC5061]: https://tools.ietf.org/html/rfc5061 176 | [RFC5062]: https://tools.ietf.org/html/rfc5062 177 | [RFC6525]: https://tools.ietf.org/html/rfc6525 178 | [RFC7053]: https://tools.ietf.org/html/rfc7053 179 | [RFC7496]: https://tools.ietf.org/html/rfc7496 180 | [RFC7829]: https://tools.ietf.org/html/rfc7829 181 | [RFC8260]: https://tools.ietf.org/html/rfc8260 182 | [ECN]: https://tools.ietf.org/html/draft-stewart-tsvwg-sctpecn-05 183 | [sctptests]: https://github.com/nplab/sctp-tests 184 | -------------------------------------------------------------------------------- /lib/chunk.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('sctp:chunk') 2 | 3 | const defs = require('./defs') 4 | 5 | const chunkdefs = defs.chunkdefs 6 | const tlvs = defs.tlvs 7 | 8 | class Chunk { 9 | constructor(chunkType, options) { 10 | if (Buffer.isBuffer(chunkType)) { 11 | this.fromBuffer(chunkType) 12 | return 13 | } 14 | if (!chunkdefs[chunkType]) { 15 | return 16 | } 17 | this.chunkType = chunkType 18 | options = options || {} 19 | this.flags = options.flags 20 | const chunkParams = chunkdefs[chunkType].params || {} 21 | for (const param in chunkParams) { 22 | if (param in options) { 23 | this[param] = options[param] 24 | } else if ('default' in chunkParams[param]) { 25 | this[param] = chunkParams[param].default 26 | } else { 27 | this[param] = chunkParams[param].type.default 28 | } 29 | } 30 | for (const param in options) { 31 | if (param in tlvs) { 32 | this[param] = options[param] 33 | } 34 | } 35 | debug('new chunk %O', this) 36 | } 37 | 38 | fromBuffer(buffer) { 39 | let offset = 0 40 | let chunkParams 41 | const chunkId = buffer.readUInt8(offset) 42 | this.chunkId = chunkId 43 | const flags = buffer.readUInt8(offset + 1) 44 | this.length = buffer.readUInt16BE(offset + 2) 45 | if (this.length < buffer.length - 3 || this.length > buffer.length) { 46 | this.error = true 47 | return 48 | } 49 | offset += 4 50 | if (chunkdefs[chunkId]) { 51 | this.chunkType = chunkdefs[chunkId].chunkType 52 | chunkParams = chunkdefs[this.chunkType].params || {} 53 | } else { 54 | this.action = chunkId >> 6 55 | debug('unrecognized chunk', chunkId) 56 | return 57 | } 58 | const minSize = chunkdefs[chunkId].size || 4 59 | debug('decoding chunk %O', this, buffer) 60 | if (this.length < minSize) { 61 | this.error = true 62 | return 63 | } 64 | if (chunkdefs[this.chunkType].flags_filter) { 65 | // Todo memoize, too often to decode 66 | this.flags = chunkdefs[this.chunkType].flags_filter.decode.call(this, flags) 67 | } 68 | for (const key in chunkParams) { 69 | // Too verbose 70 | // debug('key %s offset %d, chunk length %d', key, offset, this.length, buffer) 71 | this[key] = chunkParams[key].type.read(buffer, offset, this.length - offset) 72 | offset += chunkParams[key].type.size(this[key]) 73 | } 74 | let padding 75 | while (offset + 4 <= this.length) { 76 | const tlvId = buffer.readUInt16BE(offset) 77 | const length = buffer.readUInt16BE(offset + 2) 78 | padding = length % 4 79 | if (padding) { 80 | padding = 4 - padding 81 | } 82 | const tlv = tlvs[tlvId] 83 | if (!tlv) { 84 | let action = tlvId >> 14 85 | debug('unrecognized parameter %s, action %s', tlvId, action) 86 | debug(buffer.slice(offset)) 87 | if (tlvId & 0x4000) { 88 | if (!this.errors) { 89 | this.errors = [] 90 | } 91 | let param = buffer.slice(offset, offset + length + padding) 92 | if (param.length % 4) { 93 | // last param can be not padded, let's pad it 94 | param = Buffer.concat([param, Buffer.alloc(4 - param.length % 4)]) 95 | } 96 | this.errors.push(param) 97 | } 98 | // offset += length 99 | if (tlvId & 0x8000) { 100 | // continue 101 | } else { 102 | break 103 | } 104 | } else { 105 | const tag = tlv.tag 106 | if (tlv.multiple) { 107 | if (!this[tag]) { 108 | this[tag] = [] 109 | } 110 | this[tag].push(tlv.type.read(buffer, offset + 4, length - 4)) 111 | } else { 112 | this[tag] = tlv.type.read(buffer, offset + 4, length - 4) 113 | } 114 | } 115 | offset += length + padding 116 | debug('length %d, padding %d', length, padding) 117 | } 118 | this._filter('decode') 119 | } 120 | 121 | static fromBuffer(buffer) { 122 | if (buffer.length < 4) { 123 | return false 124 | } 125 | const chunk = new Chunk(buffer) 126 | debug('decoded chunk %O', chunk) 127 | return chunk 128 | } 129 | 130 | toBuffer() { 131 | if (this.buffer) { 132 | return this.buffer 133 | } 134 | const chunkId = chunkdefs[this.chunkType].id 135 | this.message_length = 4 136 | let offset = this.message_length 137 | let flags = 0 138 | if (chunkdefs[this.chunkType].flags_filter) { 139 | flags = chunkdefs[this.chunkType].flags_filter.encode.call(this, this.flags) 140 | } 141 | this._filter('encode') 142 | const chunkParams = chunkdefs[this.chunkType].params || {} 143 | let length 144 | let padding 145 | for (const param in this) { 146 | const value = this[param] 147 | if (chunkParams[param]) { 148 | length = chunkParams[param].type.size(value) 149 | this.message_length += length 150 | } else if (tlvs[param]) { 151 | const values = tlvs[param].multiple ? value : [value] 152 | values.forEach(value => { 153 | if (value === undefined || value === false) { 154 | return 155 | } // Todo comment 156 | length = tlvs[param].type.size(value) + 4 157 | this.message_length += length 158 | // Variable-length parameter padding 159 | padding = length % 4 160 | if (padding) { 161 | padding = 4 - padding 162 | this.message_length += padding 163 | debug('encode tlv to buff, add padding %d, length %d', padding, length) 164 | } 165 | }) 166 | } 167 | } 168 | 169 | if (padding) { 170 | // Padding of the final parameter should be the padding of the chunk 171 | // discount it from message length 172 | this.message_length -= padding 173 | } 174 | 175 | let bufferLength = this.message_length 176 | const chunkPadding = this.message_length % 4 177 | if (chunkPadding > 0) { 178 | debug('chunk padding %d, length %d', chunkPadding, length) 179 | bufferLength += 4 - chunkPadding 180 | } 181 | 182 | const buffer = Buffer.alloc(bufferLength) 183 | buffer.writeUInt8(chunkId, 0) 184 | buffer.writeUInt8(flags, 1) 185 | buffer.writeUInt16BE(this.message_length, 2) 186 | 187 | // Write mandatory params 188 | for (const param in chunkParams) { 189 | chunkParams[param].type.write(this[param], buffer, offset) 190 | offset += chunkParams[param].type.size(this[param]) 191 | } 192 | 193 | // Write optional variable-length params 194 | for (const param in this) { 195 | const value = this[param] 196 | if (tlvs[param]) { 197 | const values = tlvs[param].multiple ? value : [value] 198 | values.forEach(value => { 199 | if (value === undefined || value === false) { 200 | return 201 | } 202 | buffer.writeUInt16BE(tlvs[param].id, offset) 203 | const length = tlvs[param].type.size(value) 204 | padding = length % 4 205 | if (padding) { 206 | padding = 4 - padding 207 | } 208 | buffer.writeUInt16BE(length + 4, offset + 2) 209 | // offset += 4 210 | tlvs[param].type.write(value, buffer, offset + 4) 211 | offset += 4 + length + padding 212 | }) 213 | } 214 | } 215 | return buffer 216 | } 217 | 218 | _filter(func) { 219 | const chunkParams = chunkdefs[this.chunkType].params || {} 220 | for (const param in this) { 221 | if (chunkParams[param] && chunkParams[param].filter) { 222 | this[param] = chunkParams[param].filter[func].call(this, this[param]) 223 | } else if (tlvs[param] && tlvs[param].filter) { 224 | if (tlvs[param].multiple) { 225 | if (!Array.isArray(this[param])) { 226 | throw new TypeError('parameter can be multiple, but is not an array: ' + param) 227 | } 228 | this[param].forEach( 229 | (value, i) => { 230 | this[param][i] = tlvs[param].filter[func].call(this, value) 231 | } 232 | ) 233 | } else { 234 | this[param] = tlvs[param].filter[func].call(this, this[param]) 235 | } 236 | } 237 | } 238 | } 239 | } 240 | 241 | module.exports = Chunk 242 | -------------------------------------------------------------------------------- /lib/reassembly.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter 2 | const debug = require('debug')('sctp:reasm') 3 | const SN = require('./serial') 4 | 5 | const MAX_DUPLICATES_LENGTH = 50 6 | 7 | class Reassembly extends EventEmitter { 8 | constructor(options) { 9 | super() 10 | options = options || {} 11 | this.rwnd = options.rwnd 12 | this.mapping_array = [] 13 | this.duplicates = [] 14 | this.peer_ssn = [] 15 | } 16 | 17 | init(options) { 18 | options = options || {} 19 | this.initial_tsn = options.initial_tsn 20 | this.peer_c_tsn = new SN(this.initial_tsn).prev() 21 | this.peer_max_tsn = this.peer_c_tsn.copy() 22 | this.peer_min_tsn = this.peer_c_tsn.copy() 23 | } 24 | 25 | process(chunk) { 26 | if (chunk.chunkType !== 'data') { 27 | throw new Error('This is DATA chunk processing, not ' + chunk.chunkType) 28 | } 29 | debug('< process DATA chunk %d/%d stream [%d] %d bytes', chunk.tsn, chunk.ssn, chunk.stream_id, 30 | chunk.user_data.length 31 | ) 32 | const TSN = new SN(chunk.tsn) 33 | const index = TSN.delta(this.peer_min_tsn) - 1 34 | if (index < 0 || this.mapping_array[index]) { 35 | debug('duplicate tsn %d, peer_min_tsn %d', chunk.tsn, this.peer_min_tsn.number) 36 | if (this.duplicates.length < MAX_DUPLICATES_LENGTH) { 37 | this.duplicates.push(chunk.tsn) 38 | } 39 | return false 40 | } 41 | 42 | if (this.rwnd <= 0) { 43 | /* 44 | When the receiver's advertised window is 0, the receiver MUST drop 45 | any new incoming DATA chunk with a TSN larger than the largest TSN 46 | received so far. If the new incoming DATA chunk holds a TSN value 47 | less than the largest TSN received so far, then the receiver SHOULD 48 | drop the largest TSN held for reordering and accept the new incoming 49 | DATA chunk. In either case, if such a DATA chunk is dropped, the 50 | receiver MUST immediately send back a SACK with the current receive 51 | window showing only DATA chunks received and accepted so far. The 52 | dropped DATA chunk(s) MUST NOT be included in the SACK, as they were 53 | not accepted. The receiver MUST also have an algorithm for 54 | advertising its receive window to avoid receiver silly window 55 | syndrome (SWS), as described in [RFC0813]. The algorithm can be 56 | similar to the one described in Section 4.2.3.3 of [RFC1122]. 57 | */ 58 | debug('rwnd is %d, drop chunk %d', this.rwnd, chunk.tsn) 59 | if (TSN.gt(this.peer_max_tsn)) { 60 | // MUST drop any new incoming DATA chunk 61 | return false 62 | } 63 | // SHOULD drop the largest TSN held for reordering and accept the new incoming DATA chunk 64 | let dropIndex 65 | for (let i = this.mapping_array.length - 1; i >= 0; i--) { 66 | if (typeof this.mapping_array[i] === 'object') { 67 | dropIndex = i 68 | } 69 | } 70 | this.rwnd += this.mapping_array[dropIndex].user_data.length 71 | this.mapping_array[dropIndex] = false 72 | // If the largest TSN held for reordering is the largest TSN received so far 73 | // then decrement peer_max_tsn 74 | // else largest TSN received so far is already delivered to the ULP 75 | if (dropIndex === this.mapping_array.length - 1) { 76 | this.peer_max_tsn-- 77 | } 78 | } 79 | this.accept(chunk) 80 | return true 81 | } 82 | 83 | accept(chunk) { 84 | const TSN = new SN(chunk.tsn) 85 | // Adjust peer_max_tsn 86 | if (TSN.gt(this.peer_max_tsn)) { 87 | this.peer_max_tsn = TSN.copy() 88 | } 89 | const index = TSN.delta(this.peer_min_tsn) - 1 90 | // Todo before inserting check if it was empty => no scan 91 | this.mapping_array[index] = chunk 92 | this.rwnd -= chunk.user_data.length 93 | debug('reduce rwnd by %d to %d (%d/%d) %o', chunk.user_data.length, this.rwnd, 94 | chunk.tsn, chunk.ssn, chunk.flags) 95 | 96 | this.reassemble(chunk, TSN) 97 | this.cumulative(TSN) 98 | 99 | this.have_gaps = this.peer_c_tsn.lt(this.peer_max_tsn) 100 | } 101 | 102 | cumulative(TSN) { 103 | // Update cumulative TSN 104 | if (TSN.gt(this.peer_c_tsn)) { 105 | const max = this.peer_max_tsn.delta(this.peer_min_tsn) 106 | const offset = this.peer_c_tsn.delta(this.peer_min_tsn) 107 | let index = offset > 0 ? offset : 0 108 | while (this.mapping_array[index] && index <= max) { 109 | index++ 110 | } 111 | const delta = index - offset 112 | if (delta > 0) { 113 | this.peer_c_tsn.inc(delta) 114 | debug('update peer_c_tsn +%d to %d', delta, this.peer_c_tsn.number) 115 | } 116 | } 117 | } 118 | 119 | reassemble(newChunk, TSN) { 120 | const streamId = newChunk.stream_id 121 | let ssn = newChunk.ssn 122 | if (this.peer_ssn[streamId] === undefined) { 123 | // Accept any start ssn here, but enforce 0 somewhere 124 | this.peer_ssn[streamId] = ssn 125 | } 126 | const ordered = !newChunk.flags.U 127 | if (ordered && (newChunk.ssn !== this.peer_ssn[streamId])) { 128 | debug('out-of-sequence ssn %d (wait %d), ignore', newChunk.ssn, this.peer_ssn[streamId]) 129 | return 130 | } 131 | if (newChunk.tsn === this.peer_max_tsn.number && !newChunk.flags.E) { 132 | // Should wait for final fragment 133 | debug('%d/%d %o, wait for final fragment', 134 | newChunk.tsn, 135 | newChunk.ssn, 136 | newChunk.flags 137 | ) 138 | return 139 | } 140 | const size = this.peer_max_tsn.delta(this.peer_min_tsn) 141 | const start = newChunk.flags.B ? TSN.delta(this.peer_min_tsn) - 1 : 0 142 | // Only for unordered we can short-cut scanning to new chunk's tsn 143 | const finish = (newChunk.flags.E && !ordered) ? TSN.delta(this.peer_min_tsn) : size 144 | let candidate = null 145 | debug('--> begin scan %d/%d stream [%d]: %d to %d, [%d - %d], in array of %d', 146 | newChunk.tsn, 147 | ssn, 148 | streamId, 149 | start, 150 | finish, 151 | this.peer_min_tsn.number, 152 | this.peer_max_tsn.number, 153 | this.mapping_array.length 154 | ) 155 | let index 156 | for (index = start; index < finish; index++) { 157 | const chunk = this.mapping_array[index] 158 | if (typeof chunk === 'object' && chunk.stream_id === streamId && chunk.ssn === ssn) { 159 | debug('chunk %d/%d, chunk flags %o', 160 | chunk.tsn, chunk.ssn, chunk.flags) 161 | if (chunk.flags.B) { 162 | // Probable candidate for reassembly 163 | debug('flag B - begin reassemble ssn %d on stream %d', 164 | chunk.ssn, 165 | chunk.stream_id 166 | ) 167 | candidate = { 168 | data: [chunk.user_data], 169 | idx: [index] 170 | } 171 | debug('candidate %o', candidate) 172 | } 173 | if (candidate) { 174 | if (!chunk.flags.B) { 175 | // Add data if not first fragment 176 | candidate.data.push(chunk.user_data) 177 | candidate.idx.push(index) 178 | } 179 | if (chunk.flags.E) { 180 | debug('got full data chunk') 181 | if (ordered) { 182 | this.peer_ssn[streamId]++ 183 | debug('new stream sequence number %d', this.peer_ssn[streamId]) 184 | // Serial arithmetic 16 bit 185 | if (this.peer_ssn[streamId] > 0xFFFF) { 186 | this.peer_ssn[streamId] = 0 187 | } 188 | } 189 | 190 | debug('deliver chunks %o from mapping array on stream %d', candidate.idx, streamId) 191 | const data = Buffer.concat(candidate.data) 192 | this.emit('data', data, streamId, chunk.ppid) 193 | this.rwnd += data.length 194 | debug('new rwnd is %d', this.rwnd) 195 | 196 | candidate.idx.forEach(index => { 197 | this.mapping_array[index] = true 198 | }) 199 | if (ordered) { 200 | // Other chunks can also be ready for reassembly 201 | // reset candidate, shift expected ssn and scan for those possible chunks 202 | candidate = null 203 | ssn++ 204 | ssn %= 0x10000 205 | debug('ordered delivery - continue to ssn %d', ssn) 206 | } else { 207 | debug('unordered delivery - finish scan') 208 | break 209 | } 210 | } 211 | } 212 | } else if (candidate) { 213 | // If there is a gap in ordered chunk, we should exit, can not continue to next chunk 214 | // but if unordered, there can be another B fragment, we don't know 215 | if (ordered) { 216 | debug('have candidate but found the gap in ordered delivery - exit scan') 217 | break 218 | } else { 219 | debug('unordered sequence broken, scan for another one') 220 | candidate = null 221 | } 222 | } 223 | } 224 | // Shrink mapping array 225 | let offset 226 | for (let index = 0; index < size; index++) { 227 | if (this.mapping_array[index] === true) { 228 | offset = index + 1 229 | } else { 230 | break 231 | } 232 | } 233 | if (offset) { 234 | this.peer_min_tsn.inc(offset) 235 | this.mapping_array.splice(0, offset) 236 | debug('shift mapping array %d chunks, [%d - %d]', offset, 237 | this.peer_min_tsn.number, 238 | this.peer_max_tsn.number 239 | ) 240 | } 241 | debug('--> end scan %d/->%d stream [%d]: %d to %d, ->[%d - %d] (%d)', 242 | newChunk.tsn, 243 | ssn, 244 | streamId, 245 | start, 246 | index, 247 | this.peer_min_tsn.number, 248 | this.peer_max_tsn.number, 249 | this.mapping_array.length 250 | ) 251 | } 252 | 253 | sackInfo() { 254 | const gapBlocks = [] 255 | const max = this.peer_max_tsn.delta(this.peer_c_tsn) 256 | if (max > 0xFFFF) { 257 | throw 'bug? gap interval too big' 258 | } 259 | const offset = this.peer_c_tsn.delta(this.peer_min_tsn) 260 | let start 261 | let gap 262 | debug('scan mapping for gaps, offset %d, max %d', offset, max) 263 | for (let index = 0; index <= max; index++) { 264 | const chunk = this.mapping_array[index + offset] 265 | if (chunk) { 266 | if (gap && !start) { 267 | start = index 268 | } 269 | } else { 270 | gap = true 271 | if (start) { 272 | gapBlocks.push({ 273 | start: start + 1, 274 | finish: index 275 | }) 276 | start = null // Todo ? 277 | } 278 | } 279 | } 280 | const sackOptions = { 281 | a_rwnd: this.rwnd > 0 ? this.rwnd : 0, 282 | c_tsn_ack: this.peer_c_tsn.number 283 | } 284 | if (gapBlocks.length > 0 || this.duplicates.length > 0) { 285 | sackOptions.sack_info = { 286 | gap_blocks: gapBlocks, 287 | duplicate_tsn: this.duplicates 288 | } 289 | } 290 | if (gapBlocks.length > 0) { 291 | debug('< packet loss %d gap blocks %o,%O', gapBlocks.length, gapBlocks, sackOptions) 292 | } 293 | this.duplicates = [] 294 | return sackOptions 295 | } 296 | } 297 | 298 | module.exports = Reassembly 299 | -------------------------------------------------------------------------------- /lib/transport.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug') 2 | const ip = require('ip') 3 | const Packet = require('./packet') 4 | 5 | const IP_TTL = 64 6 | // https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 7 | const SCTP_PROTO = 132 8 | const SO_RCVBUF = 1024 * 256 9 | const SO_SNDBUF = SO_RCVBUF 10 | const BUFFER_SIZE = 1024 * 4 11 | 12 | const IP_HEADER_TEMPLATE = Buffer.from([ 13 | 0x45, // Version and header length 14 | 0x00, // Dfs 15 | 0x00, // Packet length 16 | 0x00, 17 | 0x00, // Id 18 | 0x00, 19 | 0x00, // Flags 20 | 0x00, // Offset 21 | IP_TTL, 22 | SCTP_PROTO, 23 | 0x00, // Checksum 24 | 0x00, 25 | 0x00, // Source address 26 | 0x00, 27 | 0x00, 28 | 0x00, 29 | 0x00, // Destination address 30 | 0x00, 31 | 0x00, 32 | 0x00 33 | ]) 34 | 35 | let raw = null 36 | let rawtransport = null 37 | 38 | const checkLength = 39 | process.platform === 'darwin' ? 40 | (buffer, headerLen, packetLen) => buffer.length === headerLen + packetLen : 41 | (buffer, headerLen, packetLen) => buffer.length === packetLen 42 | 43 | const readLength = 44 | process.platform === 'darwin' ? 45 | buffer => buffer.readUInt16LE(2) : 46 | buffer => buffer.readUInt16BE(2) 47 | 48 | const writeLength = 49 | process.platform === 'darwin' ? 50 | (buffer, value) => buffer.writeUInt16LE(value, 2) : 51 | (buffer, value) => buffer.writeUInt16BE(value, 2) 52 | 53 | const transports = new WeakMap() 54 | 55 | class Transport { 56 | constructor() { 57 | /* 58 | Port numbers are divided into three ranges. The Well Known Ports are 59 | those from 0 through 1023, the Registered Ports are those from 1024 60 | through 49151, and the Dynamic and/or Private Ports are those from 61 | 49152 through 65535. 62 | */ 63 | this.pool_start = 0xC000 64 | this.pool_finish = 0xFFFF 65 | this.pool_size = this.pool_finish - this.pool_start 66 | this.pool = {} 67 | this.pointer = this.pool_start 68 | this.countRcv = 0 69 | } 70 | 71 | register(endpoint) { 72 | endpoint.localPort = this.allocate(endpoint.localPort) 73 | if (endpoint.localPort) { 74 | this.pool[endpoint.localPort] = endpoint 75 | this.debug('endpoint registered on port %d', endpoint.localPort) 76 | return endpoint 77 | } 78 | } 79 | 80 | allocate(desired) { 81 | if (desired > 0 && desired < 0xFFFF) { 82 | if (desired in this.pool) { 83 | return null 84 | } 85 | return desired 86 | } 87 | let attempt = 0 88 | while (this.pointer in this.pool) { 89 | this.debug('attempt #%d to allocate port %d', attempt, this.pointer) 90 | attempt++ 91 | if (attempt > this.pool_size) { 92 | return null 93 | } 94 | this.pointer++ 95 | if (this.pointer > this.pool_finish) { 96 | this.pointer = this.pool_start 97 | } 98 | } 99 | return this.pointer 100 | } 101 | 102 | unallocate(port) { 103 | delete this.pool[port] 104 | this.debug('unallocate port %d', port) 105 | } 106 | 107 | receivePacket(packet, src, dst) { 108 | if (packet && packet.chunks) { 109 | this.debug( 110 | '< packet %d chunks %s:%d <- %s:%d', 111 | packet.chunks.length, 112 | dst, 113 | packet.dst_port, 114 | src, 115 | packet.src_port 116 | ) 117 | const endpoint = this.pool[packet.dst_port] 118 | if (endpoint) { 119 | endpoint.emit('packet', packet, src, dst) 120 | } else { 121 | this.debug('OOTB message', packet) 122 | } 123 | } else { 124 | this.debug('SCTP packet decode error') 125 | } 126 | } 127 | } 128 | 129 | class RawTransport extends Transport { 130 | constructor() { 131 | super() 132 | 133 | this.debug = debug('sctp:transport:raw') 134 | this.debug('opening raw socket') 135 | 136 | if (!raw) { 137 | raw = require('raw-socket') 138 | } 139 | 140 | const rawsocket = raw.createSocket({ 141 | addressFamily: raw.AddressFamily.IPv4, 142 | protocol: SCTP_PROTO, 143 | bufferSize: BUFFER_SIZE 144 | }) 145 | 146 | rawsocket.setOption( 147 | raw.SocketLevel.IPPROTO_IP, 148 | raw.SocketOption.IP_TTL, 149 | IP_TTL 150 | ) 151 | rawsocket.setOption( 152 | raw.SocketLevel.SOL_SOCKET, 153 | raw.SocketOption.SO_RCVBUF, 154 | SO_RCVBUF 155 | ) 156 | rawsocket.setOption( 157 | raw.SocketLevel.SOL_SOCKET, 158 | raw.SocketOption.SO_SNDBUF, 159 | SO_SNDBUF 160 | ) 161 | 162 | // Workaround to start listening on win32 // todo 163 | if (process.platform === 'win32') { 164 | rawsocket.send(Buffer.alloc(20), 0, 0, '127.0.0.1', null, () => { 165 | }) 166 | } 167 | this.debug('raw socket opened on %s', process.platform) 168 | 169 | rawsocket.on('message', this.onMessage.bind(this)) 170 | this.rawsocket = rawsocket 171 | } 172 | 173 | onMessage(buffer, src) { 174 | this.countRcv++ 175 | this.debug('< message %d bytes from %s', buffer.length, src) 176 | if (buffer.length < 36) { 177 | return 178 | } // Less than ip header + sctp header 179 | 180 | const headerLength = (buffer[0] & 0x0F) << 2 181 | // Const protocol = buffer[9] 182 | const dst = ip.toString(buffer, 16, 4) 183 | const packetLength = readLength(buffer) 184 | if (!checkLength(buffer, headerLength, packetLength)) { 185 | return 186 | } 187 | this.debug('< ip packet ok %s <- %s', dst, src) 188 | const payload = buffer.slice(headerLength) 189 | 190 | const packet = Packet.fromBuffer(payload) 191 | this.receivePacket(packet, src, dst) 192 | } 193 | 194 | sendPacket(src, dst, packet, callback) { 195 | const payload = packet.toBuffer() 196 | this.debug( 197 | '> send %d bytes %d chunks %s:%d -> %s:%d', 198 | payload.length, 199 | packet.chunks.length, 200 | src, 201 | packet.src_port, 202 | dst, 203 | packet.dst_port 204 | ) 205 | let buffer 206 | const cb = (error, bytes) => { 207 | if (error) { 208 | this.debug('raw socket send error', error) 209 | } else { 210 | this.debug('raw socket sent %d bytes', bytes) 211 | } 212 | if (typeof callback === 'function') { 213 | callback(error) 214 | } 215 | } 216 | 217 | let beforeSend 218 | if (src) { 219 | beforeSend = () => 220 | this.rawsocket.setOption( 221 | raw.SocketLevel.IPPROTO_IP, 222 | raw.SocketOption.IP_HDRINCL, 223 | 1 224 | ) 225 | const headerBuffer = createHeader({src, dst, payload}) 226 | this.debug('headerBuffer', headerBuffer) 227 | const checksum = raw.createChecksum(headerBuffer) 228 | raw.writeChecksum(headerBuffer, 10, checksum) 229 | buffer = Buffer.concat([headerBuffer, payload]) 230 | } else { 231 | beforeSend = () => 232 | this.rawsocket.setOption( 233 | raw.SocketLevel.IPPROTO_IP, 234 | raw.SocketOption.IP_HDRINCL, 235 | 0 236 | ) 237 | buffer = payload 238 | } 239 | this.rawsocket.send(buffer, 0, buffer.length, dst, beforeSend, cb) 240 | return true 241 | } 242 | 243 | enableDiscardService() { 244 | /* 245 | Discard 9/sctp Discard # IETF TSVWG 246 | # Randall Stewart 247 | # [RFC4960] 248 | 249 | The discard service, which accepts SCTP connections on port 250 | 9, discards all incoming application data and sends no data 251 | in response. Thus, SCTP's discard port is analogous to 252 | TCP's discard port, and might be used to check the health 253 | of an SCTP stack. 254 | */ 255 | (new (require('./sockets').Server)({ppid: 0})).listen({OS: 1, MIS: 100, port: 9}) 256 | } 257 | 258 | enableICMP() { 259 | /* 260 | Appendix C. ICMP Handling 261 | */ 262 | this.debug('start ICMP RAW socket on %s', process.platform) 263 | 264 | this.icmpsocket = raw.createSocket({ 265 | addressFamily: raw.AddressFamily.IPv4, 266 | protocol: raw.Protocol.ICMP 267 | }) 268 | this.icmpsocket.setOption( 269 | raw.SocketLevel.IPPROTO_IP, 270 | raw.SocketOption.IP_TTL, 271 | IP_TTL 272 | ) 273 | 274 | if (process.platform === 'win32') { 275 | const buffer = Buffer.alloc(24) 276 | this.icmpsocket.send( 277 | buffer, 278 | 0, 279 | buffer.length, 280 | '127.0.0.1', 281 | null, 282 | (error, bytes) => { 283 | this.debug('> ICMP ping', error, bytes) 284 | } 285 | ) 286 | } 287 | 288 | this.debug('ICMP socket opened on %s', process.platform) 289 | 290 | this.icmpsocket.on('message', (buffer, src) => { 291 | if (src !== '127.0.0.1') { 292 | this.processICMPPacket(src, buffer) 293 | } 294 | }) 295 | } 296 | 297 | processICMPPacket(src, buffer) { 298 | if (buffer.length < 42) { 299 | // IP header + ICMP header + part of SCTP header = 20 + 16 + 8 = 42 300 | return 301 | } 302 | const headerLength = (buffer[0] & 0x0F) << 2 303 | const packetLength = readLength(buffer) 304 | if (!checkLength(buffer, headerLength, packetLength)) { 305 | return 306 | } 307 | const icmpBuffer = buffer.slice(headerLength) 308 | 309 | /* 310 | 311 | https://tools.ietf.org/html/rfc792 312 | https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml 313 | 314 | 0 1 2 3 315 | 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 316 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 317 | | Type | Code | Checksum | 318 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 319 | | unused | 320 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 321 | | Internet Header + 64 bits of Original Data Datagram | 322 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 323 | 324 | */ 325 | 326 | const type = icmpBuffer[0] 327 | if (type !== 3) { 328 | // An implementation MAY ignore all ICMPv4 messages 329 | // where the type field is not set to "Destination Unreachable" 330 | // this.debug('< type field is not set to "Destination Unreachable", ignore it') 331 | return 332 | } 333 | 334 | const code = icmpBuffer[1] 335 | const payload = icmpBuffer.slice(8) 336 | // Mute debug 337 | // this.debug('< ICMP from %s type %d code %d, %d bytes', src, type, code, payload.length) 338 | this.processICMPPayload(payload, type, code) 339 | } 340 | 341 | processICMPPayload(buffer, type, code) { 342 | const headerLength = (buffer[0] & 0x0F) << 2 343 | const protocol = buffer[9] 344 | if (protocol !== SCTP_PROTO) { 345 | return 346 | } 347 | const dst = ip.toString(buffer, 16, 4) 348 | const src = ip.toString(buffer, 12, 4) 349 | 350 | const sctpBuffer = buffer.slice(headerLength) 351 | const packet = Packet.fromBuffer(sctpBuffer) 352 | 353 | /* 354 | https://tools.ietf.org/html/rfc792 355 | https://tools.ietf.org/html/rfc1122 356 | */ 357 | const ICMP_CODES = [ 358 | 'net unreachable', 359 | 'host unreachable', 360 | 'protocol unreachable', 361 | 'port unreachable', 362 | 'fragmentation needed and DF set', 363 | 'source route failed', 364 | 'destination network unknown', 365 | 'destination host unknown', 366 | 'source host isolated', 367 | 'communication with destination network administratively prohibited', 368 | 'communication with destination host administratively prohibited', 369 | 'network unreachable for type of service', 370 | 'host unreachable for type of service' 371 | ] 372 | this.debug('< ICMP for %s:%d -> %s:%d %s', 373 | src, packet.src_port, dst, packet.dst_port, ICMP_CODES[code]) 374 | 375 | if (packet) { 376 | const endpoint = this.pool[packet.src_port] 377 | if (endpoint) { 378 | endpoint.emit('icmp', packet, src, dst, code) 379 | } else { 380 | // If the association cannot be found, 381 | // an implementation SHOULD ignore the ICMP message. 382 | } 383 | } 384 | } 385 | } 386 | 387 | class UDPTransport extends Transport { 388 | constructor(stream) { 389 | super() 390 | 391 | this.debug = debug('sctp:transport:udp') 392 | this.socket = stream 393 | 394 | this.socket.on('close', () => { 395 | this.debug('error: transport was closed') 396 | for (const port in this.pool) { 397 | const endpoint = this.pool[port] 398 | endpoint.close() 399 | } 400 | delete this.socket 401 | delete transports[this.socket] 402 | }) 403 | 404 | this.socket.on('data', buffer => { 405 | this.countRcv++ 406 | this.debug('< message %d bytes', buffer.length) 407 | if (buffer.length < 12) { 408 | return 409 | } // Less than SCTP header 410 | const packet = Packet.fromBuffer(buffer) 411 | this.receivePacket(packet) 412 | }) 413 | } 414 | 415 | sendPacket(src, dst, packet, callback) { 416 | const payload = packet.toBuffer() 417 | this.debug( 418 | '> send %d bytes %d chunks %d -> %d', 419 | payload.length, 420 | packet.chunks.length, 421 | packet.src_port, 422 | packet.dst_port 423 | ) 424 | const buffer = payload 425 | this.socket.write(buffer, callback) 426 | return true 427 | } 428 | } 429 | 430 | function createHeader(packet) { 431 | const buffer = Buffer.from(IP_HEADER_TEMPLATE) 432 | writeLength(buffer, buffer.length + packet.payload.length) 433 | if (packet.ttl > 0 && packet.ttl < 0xFF) { 434 | buffer.writeUInt8(packet.ttl, 8) 435 | } 436 | ip.toBuffer(packet.src, buffer, 12) 437 | ip.toBuffer(packet.dst, buffer, 16) 438 | return buffer 439 | } 440 | 441 | function register(endpoint) { 442 | if (endpoint.udpTransport) { 443 | if (transports.has(endpoint.udpTransport)) { 444 | endpoint.transport = transports.get(endpoint.udpTransport) 445 | } else { 446 | endpoint.transport = new UDPTransport(endpoint.udpTransport) 447 | transports.set(endpoint.udpTransport, endpoint.transport) 448 | } 449 | } else { 450 | if (!rawtransport) { 451 | rawtransport = new RawTransport() 452 | rawtransport.enableICMP() 453 | rawtransport.enableDiscardService() 454 | } 455 | endpoint.transport = rawtransport 456 | } 457 | return endpoint.transport.register(endpoint) 458 | } 459 | 460 | module.exports = { 461 | register 462 | } 463 | -------------------------------------------------------------------------------- /lib/sockets.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | RFC 6458 4 | Sockets API Extensions for the Stream Control Transmission Protocol (SCTP) 5 | 6 | */ 7 | 8 | const Duplex = require('stream').Duplex 9 | const Readable = require('stream').Readable 10 | const Writable = require('stream').Writable 11 | const EventEmitter = require('events').EventEmitter 12 | const debug = require('debug') 13 | const ip = require('ip') 14 | const Endpoint = require('./endpoint') 15 | 16 | class SCTPStreamReadable extends Readable { 17 | // Constructor is useless 18 | constructor(socket, streamId) { 19 | super() 20 | this.socket = socket 21 | this.stream_id = streamId 22 | this.debugger = this.socket.debugger 23 | } 24 | 25 | _read() { 26 | this.debugger.debug('_read stream') 27 | } 28 | } 29 | 30 | class SCTPStreamWritable extends Writable { 31 | constructor(socket, streamId) { 32 | super() 33 | this.socket = socket 34 | this.debugger = this.socket.debugger 35 | this.stream_id = streamId 36 | this.bytesWritten = 0 37 | } 38 | 39 | _write(chunk, encoding, callback) { 40 | this.debugger.debug('> write stream %d, %d bytes', this.stream_id, chunk.length) 41 | if (!this.socket.association) { 42 | return callback(new Error('no association established')) 43 | } 44 | const options = {} 45 | options.stream_id = this.stream_id 46 | this.bytesWritten += chunk.length 47 | this.socket.bytesWritten += chunk.length 48 | return this.socket.association.SEND(chunk, options, callback) 49 | } 50 | } 51 | 52 | class Socket extends Duplex { 53 | constructor(options) { 54 | super(options) 55 | options = options || {} 56 | this.ootb = options.ootb 57 | 58 | this.debugger = {} 59 | this.debugger.info = debug('sctp:sockets:##') 60 | this.debugger.info('starting socket %o', options) 61 | 62 | this.writeCount = 0 63 | this.bytesRead = 0 64 | this.bytesWritten = 0 65 | 66 | /* 67 | Todo? 68 | this.bufferSize = 0 // getter of this.writeBuffer.length? 69 | this.destroyed = false 70 | this.connecting = false 71 | this._highWaterMark = 8 * 1024 72 | this.writeBuffer = [] 73 | */ 74 | 75 | this.streamsReadable = [] 76 | this.streamsWritable = [] 77 | 78 | this.stream_id = options.stream_id || false 79 | this.unordered = options.unordered || false 80 | this.no_bundle = options.no_bundle || false 81 | this.ppid = options.ppid || 0 82 | } 83 | 84 | _read() { 85 | this.debugger.debug('_read') 86 | // This function means that socket wants to get more data 87 | // should exist even if empty 88 | } 89 | 90 | createStream(streamId) { 91 | if (streamId < 0 || streamId >= this.OS) { 92 | /* 93 | After the association is initialized, the valid outbound stream 94 | identifier range for either endpoint shall be 0 to min(local OS, remote MIS)-1. 95 | */ 96 | this.debugger.warn('wrong stream %d, OS: %d, MIS: %d', streamId, this.OS, this.MIS) 97 | throw new Error('wrong stream id, check local OS and peer MIS') 98 | } 99 | 100 | this.debugger.warn('createStream %d, OS: %d, MIS: %d', streamId, this.OS, this.MIS) 101 | 102 | if (this.streamsWritable[streamId]) { 103 | return this.streamsWritable[streamId] 104 | } 105 | const stream = new SCTPStreamWritable(this, streamId) 106 | this.streamsWritable[streamId] = stream 107 | return stream 108 | } 109 | 110 | _write(chunk, encoding, callback) { 111 | const writeCount = this.writeCount++ 112 | this.debugger.info('> write socket #%d %d bytes', writeCount, chunk.length) 113 | if (!this.association) { 114 | return callback(new Error('no association established')) 115 | } 116 | const options = {} 117 | options.stream_id = this.stream_id 118 | this.bytesWritten += chunk.length 119 | // While a stream is not draining, calls to write() will buffer chunk, and return false. 120 | // internal _send 121 | let drain = this.association.SEND(chunk, options, (error) => { 122 | setImmediate(callback) 123 | this.debugger.debug('> write socket #%d complete', writeCount) 124 | if (error) { 125 | this.debugger.warn('> write socket error', error) 126 | } 127 | }) 128 | this.debugger.trace('draining', drain) 129 | return drain 130 | } 131 | 132 | _final(callback) { 133 | /* 134 | This optional function will be called before the stream closes, 135 | delaying the 'finish' event until callback is called. 136 | This is useful to close resources or write buffered data 137 | before a stream ends. 138 | */ 139 | // called by end() 140 | // todo! 141 | this.debugger.info('_final') 142 | if (this.association) { 143 | this.association.SHUTDOWN(callback) 144 | } 145 | } 146 | 147 | address() { 148 | return { 149 | port: this.localPort, 150 | address: this.localAddress, 151 | family: 'IPv4' 152 | } 153 | } 154 | 155 | connect(options, connectListener) { 156 | /* 157 | Port: Port the client should connect to (Required). 158 | host: Host the client should connect to. Defaults to 'localhost'. 159 | localAddress: Local interface to bind to for network connections. 160 | localPort: Local port to bind to for network connections. 161 | family : Version of IP stack. Defaults to 4. 162 | hints: dns.lookup() hints. Defaults to 0. 163 | lookup : Custom lookup function. Defaults to dns.lookup. 164 | */ 165 | 166 | if (this.outbound) { 167 | return 168 | } 169 | this.outbound = true 170 | 171 | if (typeof options !== 'object') { 172 | options = {port: options} 173 | } 174 | 175 | this.passive = Boolean(options.passive) 176 | 177 | options.port = ~~options.port 178 | if (isPort(options.port)) { 179 | this.remotePort = options.port 180 | } else { 181 | throw new Error('Invalid remote port') 182 | } 183 | 184 | this.remoteAddress = options.host || null 185 | // Do not set default host in passive mode, let user decide who may connect 186 | if (!this.remoteAddress && !this.passive) { 187 | this.remoteAddress = 'localhost' 188 | } 189 | 190 | this.localPort = ~~options.localPort 191 | 192 | if (isPort(options.localPort)) { 193 | this.localPort = options.localPort 194 | } else { 195 | throw new Error('Invalid source port') 196 | } 197 | 198 | this.localAddress = toarray(options.localAddress) 199 | 200 | this.udpTransport = options.transport 201 | 202 | if (this.udpTransport) { 203 | this.localAddress = undefined 204 | this.remoteAddress = undefined 205 | } 206 | 207 | this.debugger.info( 208 | 'connect(%d -> %s:%d)', 209 | this.localPort, 210 | this.remoteAddress, 211 | this.remotePort 212 | ) 213 | 214 | if (typeof connectListener === 'function') { 215 | this.once('connect', connectListener) 216 | } 217 | 218 | const assocOptions = { 219 | streams: 1, // Todo 220 | remoteAddress: this.remoteAddress, 221 | remotePort: this.remotePort 222 | } 223 | 224 | const initOptions = { 225 | localAddress: this.localAddress, 226 | localPort: this.localPort, 227 | MIS: options.MIS, 228 | OS: options.OS, 229 | udpTransport: this.udpTransport, 230 | ootb: this.ootb 231 | } 232 | 233 | Endpoint.INITIALIZE(initOptions, (error, endpoint) => { 234 | if (error) { 235 | this.emit('error', error) 236 | } else if (this.passive) { 237 | endpoint.on('association', association => { 238 | this.debugger.info('associated with %s:%d', 239 | association.remoteAddress, association.remotePort) 240 | if ( 241 | association.remotePort === this.remotePort && 242 | association.remoteAddress === this.remoteAddress 243 | ) { 244 | this.establish(endpoint, association) 245 | } else { 246 | // Todo abort immediately or even ignore 247 | this.debugger.info('denied connect from %d', association.remotePort) 248 | association.ABORT() 249 | } 250 | }) 251 | } else { 252 | const association = endpoint.ASSOCIATE(assocOptions) 253 | this.establish(endpoint, association) 254 | } 255 | }) 256 | } 257 | 258 | establish(endpoint, association) { 259 | this.endpoint = endpoint 260 | this.localPort = endpoint.localPort 261 | this.localAddress = endpoint.localAddress 262 | 263 | this.association = association 264 | this.remoteAddress = association.remoteAddress 265 | this.remotePort = association.remotePort 266 | 267 | // Update to min(local OS, remote MIS) 268 | this.MIS = association.MIS 269 | this.OS = association.OS 270 | this.remoteFamily = 'IPv4' 271 | 272 | const label = `${this.localPort}/${this.remoteAddress}:${this.remotePort}` 273 | this.debugger.warn = debug(`sctp:sockets:### ${label}`) 274 | this.debugger.info = debug(`sctp:sockets:## ${label}`) 275 | this.debugger.debug = debug(`sctp:sockets:# ${label}`) 276 | this.debugger.trace = debug(`sctp:sockets: ${label}`) 277 | 278 | // A) 279 | association.on('DATA ARRIVE', streamId => { 280 | const buffer = association.RECEIVE(streamId) 281 | if (!buffer) { 282 | return 283 | } 284 | 285 | this.debugger.debug('< DATA ARRIVE %d bytes on stream %d', buffer.length, streamId) 286 | 287 | if (this.listenerCount('stream') > 0) { 288 | if (!this.streamsReadable[streamId]) { 289 | this.streamsReadable[streamId] = new SCTPStreamReadable(this, streamId) 290 | this.emit('stream', this.streamsReadable[streamId], streamId) 291 | } 292 | this.streamsReadable[streamId].push(buffer) 293 | } 294 | 295 | this.bytesRead += buffer.length 296 | this.push(buffer) 297 | }) 298 | 299 | // B) todo ? 300 | association.on('SEND FAILURE', info => { 301 | this.debugger.warn('send falure', info) 302 | }) 303 | 304 | // C) todo ? 305 | association.on('NETWORK STATUS CHANGE', info => { 306 | this.debugger.warn('status change', info) 307 | }) 308 | 309 | association.once('COMMUNICATION UP', () => { 310 | this.debugger.info('socket connected') 311 | this.emit('connect') 312 | }) 313 | 314 | association.once('COMMUNICATION LOST', (event, reason) => { 315 | this.debugger.info('COMMUNICATION LOST', event, reason) 316 | if (this.outbound) { 317 | endpoint.DESTROY() 318 | } 319 | this.debugger.info('emit end') 320 | this.emit('end') 321 | }) 322 | 323 | association.on('COMMUNICATION ERROR', () => { 324 | this.emit('error') 325 | }) 326 | 327 | association.on('RESTART', () => { 328 | this.emit('restart') 329 | }) 330 | 331 | association.on('SHUTDOWN COMPLETE', () => { 332 | this.debugger.debug('socket ended') 333 | if (this.outbound) { 334 | endpoint.DESTROY() 335 | } 336 | this.emit('end') 337 | }) 338 | } 339 | 340 | SCTP_ASSOCINFO(options) { 341 | const params = ['valid_cookie_life'] 342 | const endpoint = this.endpoint 343 | if (endpoint && typeof options === 'object') { 344 | params.forEach(key => { 345 | if (key in options) { 346 | endpoint[key] = options[key] 347 | } 348 | }) 349 | } 350 | } 351 | 352 | /** 353 | * Destroy() internal implementation 354 | * @param {Error} err 355 | * @param {function} callback 356 | * @returns {Socket} 357 | * @private 358 | */ 359 | _destroy(err, callback) { 360 | this.debugger.info('destroy()') 361 | // SetTimeout(() => { 362 | // todo 363 | this.association.ABORT() 364 | if (this.outbound) { 365 | this.endpoint.DESTROY() 366 | } 367 | // }, 100) 368 | callback(err) 369 | return this 370 | } 371 | } 372 | 373 | class Server extends EventEmitter { 374 | constructor(options, connectionListener) { 375 | super() 376 | if (typeof options === 'function') { 377 | connectionListener = options 378 | options = {} 379 | } else { 380 | options = options || {} 381 | } 382 | 383 | this.debugger = {} 384 | this.debugger.info = debug('sctp:server:##') 385 | this.debugger.info('server start %o', options) 386 | 387 | if (typeof connectionListener === 'function') { 388 | this.on('connection', connectionListener) 389 | } 390 | 391 | this.listening = false 392 | this.ppid = options.ppid 393 | this.udpTransport = options.transport 394 | } 395 | 396 | address() { 397 | return { 398 | port: this.localPort, 399 | address: this.localAddress, 400 | family: 'IPv4' 401 | } 402 | } 403 | 404 | close(callback) { 405 | if (!this.listening) { 406 | return 407 | } 408 | this.listening = false 409 | // Todo close connections? 410 | this.emit('close') 411 | if (typeof callback === 'function') { 412 | callback() 413 | } 414 | } 415 | 416 | listen(port, host, backlog, callback) { 417 | /* 418 | The server.listen() method can be called again if and only if there was an error 419 | during the first server.listen() call or server.close() has been called. 420 | Otherwise, an ERR_SERVER_ALREADY_LISTEN error will be thrown. 421 | */ 422 | 423 | if (typeof port === 'object') { 424 | const options = port 425 | callback = host 426 | this._listen(options, callback) 427 | } else { 428 | const options = {port, host, backlog} 429 | this._listen(options, callback) 430 | } 431 | } 432 | 433 | _listen(options, callback) { 434 | options = options || {} 435 | this.debugger.info('server try listen %o', options) 436 | 437 | if (typeof callback === 'function') { 438 | this.once('listening', callback) 439 | } 440 | 441 | const initOptions = { 442 | localPort: options.port, 443 | localAddress: toarray(options.host), 444 | MIS: options.MIS || this.maxConnections, 445 | OS: options.OS, 446 | udpTransport: this.udpTransport, 447 | } 448 | 449 | Endpoint.INITIALIZE(initOptions, (error, endpoint) => { 450 | if (error) { 451 | this.emit('error', error) 452 | } else { 453 | this.localPort = endpoint.localPort 454 | this.endpoint = endpoint 455 | 456 | const label = `[${endpoint.localPort}]` 457 | this.debugger.warn = debug(`sctp:server:### ${label}`) 458 | this.debugger.info = debug(`sctp:server:## ${label}`) 459 | this.debugger.debug = debug(`sctp:server:# ${label}`) 460 | this.debugger.trace = debug(`sctp:server: ${label}`) 461 | this.debugger.info('bound') 462 | 463 | endpoint.on('association', association => { 464 | // Todo other params 465 | const socket = new Socket({ppid: this.ppid}) 466 | socket.establish(endpoint, association) 467 | this.emit('connection', socket) 468 | this.debugger.debug('connect <- %s:%s', association.remoteAddress, association.remotePort) 469 | }) 470 | this.listening = true 471 | this.emit('listening') 472 | } 473 | }) 474 | } 475 | } 476 | 477 | function toarray(address) { 478 | if (!address) { 479 | return 480 | } 481 | let addresses = Array.isArray(address) ? address : [address] 482 | addresses = addresses.filter(address => ip.isV4Format(address)) 483 | return addresses 484 | } 485 | 486 | function isPort(port) { 487 | return Number.isInteger(port) && port > 0 && port < 0xFFFF; 488 | } 489 | 490 | module.exports = { 491 | Socket, 492 | Server 493 | } 494 | -------------------------------------------------------------------------------- /lib/defs.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml 4 | 5 | */ 6 | 7 | const debug = require('debug')('sctp:defs') 8 | 9 | const ip = require('ip') 10 | 11 | const NET_SCTP = { 12 | G: 50, // Granularity 13 | RWND: 1024 * 100, 14 | rto_initial: 3000, 15 | rto_min: 1000, 16 | rto_max: 60000, 17 | rto_alpha_exp_divisor: 3, 18 | rto_beta_exp_divisor: 2, 19 | valid_cookie_life: 60000, 20 | max_burst: 4, 21 | association_max_retrans: 10, // Todo 22 | cookie_hmac_alg: 'sha1', 23 | max_init_retransmits: 8, 24 | hb_interval: 30000, 25 | sack_timeout: 180, 26 | sack_freq: 2 27 | } 28 | 29 | const CAUSE_CODES = { 30 | INVALID_STREAM_IDENTIFIER: 0x0001, 31 | MISSING_MANDATORY_PARAMETER: 0x0002, 32 | STALE_COOKIE_ERROR: 0x0003, 33 | OUT_OF_RESOURCE: 0x0004, 34 | UNRESOLVABLE_ADDRESS: 0x0005, 35 | UNRECONGNIZED_CHUNK_TYPE: 0x0006, 36 | INVALID_MANDATORY_PARAMETER: 0x0007, 37 | UNRECONGNIZED_PARAMETERS: 0x0008, 38 | NO_USER_DATA: 0x0009, 39 | COOKIE_RECEIVED_WHILE_SHUTTING_DOWN: 0x000A, 40 | RESTART_WITH_NEW_ADDRESSES: 0x000B, 41 | USER_INITIATED_ABORT: 0x000C, 42 | PROTOCOL_VIOLATION: 0x000D, 43 | UNSUPPORTED_HMAC_IDENTIFIER: 0x0105 44 | } 45 | 46 | revert(CAUSE_CODES) 47 | 48 | /* 49 | 50 | Todo 51 | sysctl -a | grep sctp 52 | 53 | net.sctp.addip_enable = 0 54 | net.sctp.addip_noauth_enable = 0 55 | net.sctp.addr_scope_policy = 1 56 | net.sctp.association_max_retrans = 10 57 | net.sctp.auth_enable = 0 58 | net.sctp.cookie_hmac_alg = sha1 59 | net.sctp.cookie_preserve_enable = 1 60 | net.sctp.default_auto_asconf = 0 61 | net.sctp.hb_interval = 30000 62 | net.sctp.max_autoclose = 2147483 63 | net.sctp.max_burst = 4 64 | net.sctp.max_init_retransmits = 8 65 | net.sctp.path_max_retrans = 5 66 | net.sctp.pf_retrans = 0 67 | net.sctp.prsctp_enable = 1 68 | net.sctp.rcvbuf_policy = 0 69 | net.sctp.rto_alpha_exp_divisor = 3 70 | net.sctp.rto_beta_exp_divisor = 2 71 | net.sctp.rto_initial = 3000 72 | net.sctp.rto_max = 60000 73 | net.sctp.rto_min = 1000 74 | net.sctp.rwnd_update_shift = 4 75 | net.sctp.sack_timeout = 200 76 | net.sctp.sctp_mem = 42486 56648 84972 77 | net.sctp.sctp_rmem = 4096 865500 1812736 78 | net.sctp.sctp_wmem = 4096 16384 1812736 79 | net.sctp.sndbuf_policy = 0 80 | net.sctp.valid_cookie_life = 60000 81 | 82 | */ 83 | 84 | function revert(hash, key1, key2) { 85 | for (const key in hash) { 86 | const value = hash[key] 87 | if (key1 && key2) { 88 | hash[value[key1]] = value 89 | value[key2] = key 90 | } else { 91 | hash[value] = key 92 | } 93 | } 94 | } 95 | 96 | const types = { 97 | int8: { 98 | read(buffer, offset) { 99 | return buffer.readUInt8(offset) 100 | }, 101 | write(value, buffer, offset) { 102 | value = value || 0 103 | buffer.writeUInt8(value, offset) 104 | }, 105 | size() { 106 | return 1 107 | }, 108 | default: 0 109 | }, 110 | int16: { 111 | read(buffer, offset) { 112 | return buffer.readUInt16BE(offset) 113 | }, 114 | write(value, buffer, offset) { 115 | value = value || 0 116 | buffer.writeUInt16BE(value, offset) 117 | }, 118 | size() { 119 | return 2 120 | }, 121 | default: 0 122 | }, 123 | int32: { 124 | read(buffer, offset) { 125 | return buffer.readUInt32BE(offset) 126 | }, 127 | write(value, buffer, offset) { 128 | value = value || 0 129 | buffer.writeUInt32BE(value, offset) 130 | }, 131 | size() { 132 | return 4 133 | }, 134 | default: 0 135 | }, 136 | buffer: { 137 | read(buffer, offset, length) { 138 | return buffer.slice(offset, offset + length) 139 | // Return Buffer.from(buffer.slice(offset, offset + length)) 140 | }, 141 | write(value, buffer, offset) { 142 | if (typeof value === 'string') { 143 | value = Buffer.from(value, 'ascii') 144 | } 145 | value.copy(buffer, offset) 146 | }, 147 | size(value) { 148 | return value ? value.length : 0 149 | }, 150 | default: Buffer.alloc(0) 151 | }, 152 | empty: { 153 | read() { 154 | return true 155 | }, 156 | write() { 157 | }, 158 | size() { 159 | return 0 160 | }, 161 | default: false 162 | }, 163 | string: { 164 | read(buffer, offset, length) { 165 | return buffer.slice(offset, offset + length).toString('ascii') 166 | }, 167 | write(value, buffer, offset) { 168 | Buffer.from(value, 'ascii').copy(buffer, offset) 169 | }, 170 | size(value) { 171 | return value ? value.length : 0 172 | }, 173 | default: '' 174 | } 175 | } 176 | 177 | const filters = {} 178 | 179 | filters.data_flags = { 180 | encode(value) { 181 | let result 182 | if (typeof value === 'object') { 183 | result = 184 | (value.E ? 0x01 : 0x00) | 185 | (value.B ? 0x02 : 0x00) | 186 | (value.U ? 0x04 : 0x00) | 187 | (value.I ? 0x08 : 0x00) 188 | } else { 189 | result = value 190 | } 191 | return result 192 | }, 193 | decode(value) { 194 | const result = { 195 | B: (value >> 1) & 0x01, 196 | E: value & 0x01, 197 | U: (value >> 2) & 0x01, 198 | I: (value >> 3) & 0x01 199 | } 200 | return result 201 | } 202 | } 203 | 204 | filters.reflect_flag = { 205 | encode(value) { 206 | let result 207 | if (typeof value === 'object') { 208 | result = value.T ? 0x01 : 0x00 209 | } else { 210 | result = value 211 | } 212 | return result 213 | }, 214 | decode(value) { 215 | const result = { 216 | T: value & 0x01 217 | } 218 | return result 219 | } 220 | } 221 | 222 | filters.ip = { 223 | encode(value) { 224 | if (Buffer.isBuffer(value)) { 225 | return value 226 | } 227 | return ip.toBuffer(value) 228 | }, 229 | decode(value) { 230 | if (!Buffer.isBuffer(value)) { 231 | return value 232 | } 233 | return ip.toString(value) 234 | } 235 | } 236 | 237 | filters.sack_info = { 238 | encode(value) { 239 | let result = Buffer.alloc(0) 240 | if (!value) { 241 | return result 242 | } 243 | // If (typeof value === 'object') { 244 | let offset = 0 245 | if (Array.isArray(value.gap_blocks) && value.gap_blocks.length > 0) { 246 | this.gap_blocks_number = value.gap_blocks.length 247 | offset = 0 248 | const gapBlocksBuffer = Buffer.alloc(value.gap_blocks.length * 4) 249 | value.gap_blocks.forEach(gapBlock => { 250 | if (offset <= gapBlocksBuffer.length - 4) { 251 | debug('gapBlock.start %d, gapBlock.finish %d', gapBlock.start, gapBlock.finish) 252 | gapBlocksBuffer.writeUInt16BE(gapBlock.start, offset) 253 | gapBlocksBuffer.writeUInt16BE(gapBlock.finish, offset + 2) 254 | offset += 4 255 | } else { 256 | // Todo tmp to catch bug if any 257 | throw new Error('incorrect buffer length for gap blocks') 258 | } 259 | }) 260 | result = gapBlocksBuffer 261 | } 262 | if (Array.isArray(value.duplicate_tsn) && value.duplicate_tsn.length > 0) { 263 | this.duplicate_tsn_number = value.duplicate_tsn.length 264 | offset = 0 265 | const duplicateTsnBuffer = Buffer.alloc(value.duplicate_tsn.length * 4) 266 | value.duplicate_tsn.forEach(tsn => { 267 | duplicateTsnBuffer.writeUInt32BE(tsn, offset) 268 | offset += 4 269 | }) 270 | result = Buffer.concat([result, duplicateTsnBuffer]) 271 | } 272 | // } 273 | return result 274 | }, 275 | decode(buffer) { 276 | const result = { 277 | gap_blocks: [], 278 | duplicate_tsn: [] 279 | } 280 | let offset = 0 281 | let gapBlock 282 | for (let n = 1; n <= this.gap_blocks_number; n++) { 283 | if (offset > buffer.length - 4) { 284 | break 285 | } 286 | gapBlock = { 287 | start: buffer.readUInt16BE(offset), 288 | finish: buffer.readUInt16BE(offset + 2) 289 | } 290 | result.gap_blocks.push(gapBlock) 291 | offset += 4 292 | } 293 | for (let x = 1; x <= this.duplicate_tsn_number; x++) { 294 | if (offset > buffer.length - 4) { 295 | break 296 | } 297 | result.duplicate_tsn.push(buffer.readUInt32BE(offset)) 298 | offset += 4 299 | } 300 | return result 301 | } 302 | } 303 | 304 | filters.error_causes = { 305 | encode(value) { 306 | if (!Array.isArray(value) || value.length === 0) { 307 | return Buffer.alloc(0) 308 | } 309 | const buffers = [] 310 | let header 311 | let body 312 | value.forEach(error => { 313 | header = Buffer.alloc(4) 314 | if (error.cause) { 315 | error.cause_code = CAUSE_CODES[error.cause] 316 | } 317 | header.writeUInt16BE(error.cause_code, 0) 318 | switch (error.cause_code) { 319 | case CAUSE_CODES.INVALID_STREAM_IDENTIFIER: 320 | body = Buffer.alloc(4) 321 | body.writeUInt16BE(error.stream_id, 0) 322 | break 323 | case CAUSE_CODES.UNRECONGNIZED_CHUNK_TYPE: 324 | body = Buffer.from(error.unrecognized_chunk) 325 | break 326 | case CAUSE_CODES.UNRECONGNIZED_PARAMETERS: 327 | body = Buffer.from(error.unrecognized_parameters) 328 | break 329 | case CAUSE_CODES.PROTOCOL_VIOLATION: 330 | body = Buffer.from(error.additional_information || '') 331 | break 332 | case CAUSE_CODES.USER_INITIATED_ABORT: 333 | body = Buffer.from(error.abort_reason || '') 334 | break 335 | default: 336 | body = Buffer.alloc(0) 337 | } 338 | header.writeUInt16BE(body.length + 4, 2) 339 | buffers.push(Buffer.concat([header, body])) 340 | }) 341 | return Buffer.concat(buffers) 342 | }, 343 | decode(buffer) { 344 | let offset = 0 345 | const result = [] 346 | let errorLength 347 | let body 348 | while (offset + 4 <= buffer.length) { 349 | const error = {} 350 | error.cause_code = buffer.readUInt16BE(offset) 351 | error.cause = CAUSE_CODES[error.cause_code] 352 | errorLength = buffer.readUInt16BE(offset + 2) 353 | if (errorLength > 4) { 354 | body = buffer.slice(offset + 4, offset + 4 + errorLength) 355 | switch (error.cause_code) { 356 | case CAUSE_CODES.INVALID_STREAM_IDENTIFIER: 357 | error.stream_id = body.readUInt16BE(0) 358 | break 359 | case CAUSE_CODES.MISSING_MANDATORY_PARAMETER: 360 | // TODO: 361 | break 362 | case CAUSE_CODES.STALE_COOKIE_ERROR: 363 | error.measure_of_staleness = body.readUInt32BE(0) 364 | break 365 | case CAUSE_CODES.OUT_OF_RESOURCE: 366 | break 367 | case CAUSE_CODES.UNRESOLVABLE_ADDRESS: 368 | // https://sourceforge.net/p/lksctp/mailman/message/26542493/ 369 | error.hostname = body.slice(4, 4 + body.readUInt16BE(2)).toString() 370 | break 371 | case CAUSE_CODES.UNRECONGNIZED_CHUNK_TYPE: 372 | error.unrecognized_chunk = body 373 | break 374 | case CAUSE_CODES.INVALID_MANDATORY_PARAMETER: 375 | break 376 | case CAUSE_CODES.UNRECONGNIZED_PARAMETERS: 377 | // TODO: slice 378 | error.unrecognized_parameters = body 379 | break 380 | case CAUSE_CODES.NO_USER_DATA: 381 | error.tsn = body.readUInt32BE(0) 382 | break 383 | case CAUSE_CODES.COOKIE_RECEIVED_WHILE_SHUTTING_DOWN: 384 | break 385 | case CAUSE_CODES.RESTART_WITH_NEW_ADDRESSES: 386 | // TODO: 387 | break 388 | case CAUSE_CODES.USER_INITIATED_ABORT: 389 | error.abort_reason = body.toString() 390 | break 391 | case CAUSE_CODES.PROTOCOL_VIOLATION: 392 | error.additional_information = body.toString() 393 | break 394 | default: 395 | error.body = body 396 | return 397 | } 398 | } 399 | offset += errorLength 400 | result.push(error) 401 | } 402 | return result 403 | } 404 | } 405 | 406 | filters.reconf = { 407 | encode: value => { 408 | const buffer = Buffer.alloc(12) 409 | buffer.writeUInt32BE(value.rsn, 0) 410 | return buffer 411 | }, 412 | decode: buffer => { 413 | if (buffer.length < 4) { 414 | return 415 | } 416 | const value = {} 417 | value.rsn = buffer.readUInt32BE(0) 418 | return value 419 | } 420 | } 421 | 422 | filters.forward_tsn_stream = { 423 | encode: value => { 424 | value = value || {} 425 | const buffer = Buffer.alloc(4) 426 | buffer.writeUInt16BE(value.stream_id, 0) 427 | buffer.writeUInt16BE(value.ssn, 0) 428 | return buffer 429 | }, 430 | decode: buffer => { 431 | if (buffer.length < 4) { 432 | return 433 | } 434 | const value = {} 435 | value.stream_id = buffer.readUInt16BE(0) 436 | value.ssn = buffer.readUInt16BE(2) 437 | return value 438 | } 439 | } 440 | 441 | filters.chunks = { 442 | encode: value => { 443 | if (!Array.isArray(value)) { 444 | return 445 | } 446 | if (value.length > 260) { 447 | return 448 | } 449 | const array = value 450 | .filter(chunkType => typeof chunkType === 'string') 451 | .map(chunkType => chunkdefs[chunkType].id) 452 | return Buffer.from(array) 453 | }, 454 | decode: buffer => { 455 | return [...buffer] 456 | .map(byte => chunkdefs[byte].chunkType) 457 | } 458 | } 459 | 460 | filters.hmac_algo = { 461 | encode: value => { 462 | if (!Array.isArray(value)) { 463 | return 464 | } 465 | const HMAC_ALGO = { 466 | 'SHA-1': 1, 467 | 'SHA-256': 3 468 | } 469 | const array = value 470 | .filter(algo => typeof algo === 'string') 471 | .map(algo => HMAC_ALGO[algo.toUpperCase()]) 472 | .filter(algo => algo) 473 | const buffer = Buffer.alloc(array.length * 2) 474 | array.forEach((number, index) => { 475 | buffer.writeUInt16BE(number, index * 2) 476 | }) 477 | return buffer 478 | }, 479 | decode: buffer => { 480 | const result = [] 481 | const HMAC_ALGO = [ 482 | undefined, 483 | 'SHA-1', 484 | undefined, 485 | 'SHA-256' 486 | ] 487 | for (let index = 0; index <= buffer.length - 2; index += 2) { 488 | const algo = HMAC_ALGO[buffer.readUInt16BE(index)] 489 | if (algo) { 490 | result.push(algo) 491 | } 492 | } 493 | return result 494 | } 495 | } 496 | 497 | const tlvs = { 498 | heartbeat_info: { 499 | id: 0x0001, 500 | type: types.buffer 501 | }, 502 | ipv4_address: { 503 | id: 0x0005, 504 | type: types.buffer, 505 | multiple: true, 506 | filter: filters.ip 507 | }, 508 | ipv6_address: { 509 | id: 0x0006, 510 | type: types.buffer, 511 | multiple: true, 512 | filter: filters.ip 513 | }, 514 | state_cookie: { 515 | id: 0x0007, 516 | type: types.buffer 517 | }, 518 | unrecognized_parameter: { 519 | id: 0x0008, 520 | type: types.buffer, 521 | multiple: true 522 | }, 523 | cookie_preservative: { 524 | id: 0x0009, 525 | type: types.int32 526 | }, 527 | host_name_address: { 528 | id: 0x000B, 529 | type: types.string 530 | }, 531 | supported_address_type: { 532 | id: 0x000C, 533 | type: types.int16 534 | }, 535 | ssn_reset_outgoing: { 536 | id: 13, 537 | type: types.buffer, 538 | filter: filters.reconf 539 | }, 540 | ssn_reset_incoming: { 541 | id: 14, 542 | type: types.buffer, 543 | filter: filters.reconf 544 | }, 545 | ssn_tsn_reset: { 546 | id: 15, 547 | type: types.buffer, 548 | filter: filters.reconf 549 | }, 550 | re_config_response: { 551 | id: 16, 552 | type: types.buffer, 553 | filter: filters.reconf 554 | }, 555 | add_streams_outgoing: { 556 | id: 17, 557 | type: types.buffer, 558 | filter: filters.reconf 559 | }, 560 | add_streams_incoming: { 561 | id: 18, 562 | type: types.buffer, 563 | filter: filters.reconf 564 | }, 565 | ecn_supported: { 566 | id: 0x8000, // 1000 0000 0000 0000 - '10' - skip and continue 567 | type: types.empty 568 | }, 569 | random: { 570 | id: 0x8002, // 1000 0000 0000 0010 571 | type: types.buffer 572 | }, 573 | chunks: { 574 | id: 0x8003, // 1000 0000 0000 0011 575 | type: types.buffer, 576 | filter: filters.chunks 577 | }, 578 | hmac_algo: { 579 | id: 0x8004, // 1000 0000 0000 0100 580 | type: types.buffer, 581 | filter: filters.hmac_algo 582 | }, 583 | pad: { 584 | id: 0x8005, // 1000 0000 0000 0101 585 | type: types.buffer 586 | }, 587 | supported_extensions: { 588 | id: 0x8008, // 1000 0000 0000 1000 589 | type: types.buffer 590 | }, 591 | forward_tsn_supported: { 592 | id: 0xC000, // 1100 0000 0000 0000 - '11' - skip and report 'Unrecognized Chunk Type' 593 | type: types.empty 594 | }, 595 | add_ip_address: { 596 | id: 0xC001, // 1100 0000 0000 0001 597 | type: types.buffer 598 | }, 599 | delete_ip_address: { 600 | id: 0xC002, // 1100 0000 0000 0010 601 | type: types.buffer 602 | }, 603 | error_cause_indication: { 604 | id: 0xC003, // 1100 0000 0000 0011 605 | type: types.buffer 606 | }, 607 | set_primary_address: { 608 | id: 0xC004, // 1100 0000 0000 0100 609 | type: types.buffer 610 | }, 611 | success_indication: { 612 | id: 0xC005, // 1100 0000 0000 0101 613 | type: types.buffer 614 | }, 615 | adaptation_layer_indication: { 616 | id: 0xC006, // 1100 0000 0000 0110 617 | type: types.buffer 618 | } 619 | } 620 | 621 | revert(tlvs, 'id', 'tag') 622 | 623 | const PPID = { 624 | SCTP: 0, 625 | IUA: 1, 626 | M2UA: 2, 627 | M3UA: 3, 628 | SUA: 4, 629 | M2PA: 5, 630 | V5UA: 6, 631 | H248: 7, 632 | BICC: 8, 633 | TALI: 9, 634 | DUA: 10, 635 | ASAP: 11, 636 | ENRP: 12, 637 | H323: 13, 638 | QIPC: 14, 639 | SIMCO: 15, 640 | DDP_CHUNK: 16, 641 | DDP_CONTROL: 17, 642 | S1AP: 18, 643 | RUA: 19, 644 | HNBAP: 20, 645 | FORCES_HP: 21, 646 | FORCES_MP: 22, 647 | FORCES_LP: 23, 648 | SBCAP: 24, 649 | NBAP: 25, 650 | X2AP: 27, 651 | IRCP: 28, 652 | LCSAP: 29, 653 | MPICH2: 30, 654 | SABP: 31, 655 | FGP: 32, 656 | PPP: 33, 657 | CALCAPP: 34, 658 | SSP: 35, 659 | NPMP_CONTROL: 36, 660 | NPMP_DATA: 37, 661 | ECHO: 38, 662 | DISCARD: 39, 663 | DAYTIME: 40, 664 | CHARGEN: 41, 665 | RNA: 42, 666 | M2AP: 43, 667 | M3AP: 44, 668 | SSH: 45, 669 | DIAMETER: 46, 670 | DIAMETER_DTLS: 47, 671 | BER: 48, 672 | WEBRTC_DCEP: 50, 673 | WEBRTC_STRING: 51, 674 | WEBRTC_BINARY: 53, 675 | PUA: 55, 676 | WEBRTC_STRING_EMPTY: 56, 677 | WEBRTC_BINARY_EMPTY: 57, 678 | XWAP: 58, 679 | XWCP: 59, 680 | NGAP: 60, 681 | XNAP: 61 682 | } 683 | 684 | revert(PPID) 685 | 686 | const chunkdefs = { 687 | data: { 688 | id: 0x00, 689 | size: 16, 690 | params: { 691 | tsn: {type: types.int32, default: null}, 692 | stream_id: {type: types.int16}, 693 | ssn: {type: types.int16}, 694 | ppid: {type: types.int32}, 695 | user_data: {type: types.buffer} 696 | }, 697 | flags_filter: filters.data_flags 698 | }, 699 | init: { 700 | id: 0x01, 701 | size: 20, 702 | params: { 703 | initiate_tag: {type: types.int32}, 704 | a_rwnd: {type: types.int32}, 705 | outbound_streams: {type: types.int16}, 706 | inbound_streams: {type: types.int16}, 707 | initial_tsn: {type: types.int32} 708 | } 709 | }, 710 | init_ack: { 711 | id: 0x02, 712 | size: 20, 713 | params: { 714 | initiate_tag: {type: types.int32}, 715 | a_rwnd: {type: types.int32}, 716 | outbound_streams: {type: types.int16}, 717 | inbound_streams: {type: types.int16}, 718 | initial_tsn: {type: types.int32} 719 | } 720 | }, 721 | sack: { 722 | id: 0x03, 723 | size: 16, 724 | params: { 725 | c_tsn_ack: {type: types.int32}, 726 | a_rwnd: {type: types.int32}, 727 | gap_blocks_number: {type: types.int16}, 728 | duplicate_tsn_number: {type: types.int16}, 729 | sack_info: {type: types.buffer, filter: filters.sack_info} 730 | } 731 | }, 732 | heartbeat: { 733 | id: 0x04, 734 | size: 4 735 | }, 736 | heartbeat_ack: { 737 | id: 0x05, 738 | size: 4 739 | }, 740 | abort: { 741 | id: 0x06, 742 | size: 4, 743 | params: { 744 | error_causes: {type: types.buffer, filter: filters.error_causes} 745 | }, 746 | flags_filter: filters.reflect_flag 747 | }, 748 | shutdown: { 749 | id: 0x07, 750 | size: 8, 751 | params: { 752 | c_tsn_ack: {type: types.int32} 753 | } 754 | }, 755 | shutdown_ack: { 756 | id: 0x08, 757 | size: 4 758 | }, 759 | error: { 760 | id: 0x09, 761 | size: 4, // Tolerate absence of causes? 762 | params: { 763 | error_causes: {type: types.buffer, filter: filters.error_causes} 764 | } 765 | }, 766 | cookie_echo: { 767 | id: 0x0A, 768 | size: 4, 769 | params: { 770 | cookie: {type: types.buffer} 771 | } 772 | }, 773 | cookie_ack: { 774 | id: 0x0B, 775 | size: 4 776 | }, 777 | ecne: { 778 | id: 0x0C 779 | }, 780 | cwr: { 781 | id: 0x0D 782 | }, 783 | shutdown_complete: { 784 | id: 0x0E, 785 | flags_filter: filters.reflect_flag 786 | }, 787 | auth: { 788 | id: 0x0F, 789 | params: { 790 | shared_key_id: {type: types.int16}, 791 | hmac_id: {type: types.int16}, 792 | hmac: {type: types.buffer} 793 | } 794 | }, 795 | i_data: { 796 | id: 0x40, // 64, 0100 0010 797 | params: { 798 | tsn: {type: types.int32, default: null}, 799 | stream_id: {type: types.int16}, 800 | ssn: {type: types.int16}, 801 | message_id: {type: types.int16}, 802 | ppid: {type: types.int32}, 803 | user_data: {type: types.buffer} 804 | }, 805 | flags_filter: filters.data_flags 806 | }, 807 | asconf_ack: { 808 | id: 0x80, // 128, 1000 0000, 809 | seq: {type: types.int32} 810 | }, 811 | re_config: { 812 | id: 0x82 // 130, 1000 0010 813 | }, 814 | pad: { 815 | id: 0x84, // 132, 1000 0100 816 | params: { 817 | padding_data: {type: types.buffer} 818 | } 819 | }, 820 | forward_tsn: { 821 | id: 0xC0, // 192, 1100 0000 822 | params: { 823 | new_c_tsn: {type: types.int32}, 824 | streams: {type: types.buffer, multiple: true, filter: filters.forward_tsn_stream} 825 | } 826 | }, 827 | asconf: { 828 | id: 0xC1, // 193, 1100 0001 829 | params: { 830 | seq: {type: types.int32}, 831 | address: {type: types.buffer} 832 | } 833 | }, 834 | i_forward_tsn: { 835 | id: 0xC2, // 194, 1100 0010 836 | params: { 837 | new_c_tsn: {type: types.int32}, 838 | streams: {type: types.buffer, multiple: true} 839 | } 840 | } 841 | } 842 | 843 | revert(chunkdefs, 'id', 'chunkType') 844 | 845 | module.exports = { 846 | NET_SCTP, 847 | filters, 848 | chunkdefs, 849 | types, 850 | tlvs, 851 | CAUSE_CODES, 852 | PPID 853 | } 854 | -------------------------------------------------------------------------------- /lib/endpoint.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const EventEmitter = require('events').EventEmitter 3 | const debug = require('debug') 4 | 5 | const transport = require('./transport') 6 | const Packet = require('./packet') 7 | const Chunk = require('./chunk') 8 | const Association = require('./association') 9 | const defs = require('./defs') 10 | 11 | debug.formatters.h = v => { 12 | return v.toString('hex') 13 | } 14 | 15 | class Endpoint extends EventEmitter { 16 | constructor(options) { 17 | super() 18 | options = options || {} 19 | this.ootb = options.ootb 20 | this.localPort = options.localPort 21 | if (options.localAddress && options.localAddress.length > 0) { 22 | this.localAddress = options.localAddress 23 | this.localActiveAddress = options.localAddress[0] 24 | } 25 | 26 | this.udpTransport = options.udpTransport 27 | 28 | this.debugger = {} 29 | const label = `[${this.localPort}]` 30 | this.debugger.warn = debug(`sctp:endpoint:### ${label}`) 31 | this.debugger.info = debug(`sctp:endpoint:## ${label}`) 32 | this.debugger.debug = debug(`sctp:endpoint:# ${label}`) 33 | this.debugger.trace = debug(`sctp:endpoint: ${label}`) 34 | 35 | this.debugger.info('creating endpoint %o', options) 36 | 37 | this.MIS = options.MIS || 2 38 | this.OS = options.OS || 2 39 | this.cookieSecretKey = crypto.randomBytes(32) 40 | this.valid_cookie_life = defs.NET_SCTP.valid_cookie_life 41 | this.cookie_hmac_alg = defs.NET_SCTP.cookie_hmac_alg === 'md5' ? 'md5' : 'sha1' 42 | this.cookie_hmac_len = defs.NET_SCTP.cookie_hmac_alg === 'md5' ? 16 : 20 43 | 44 | setInterval(() => { 45 | // Todo change interval when valid_cookie_life changes 46 | this.cookieSecretKey = crypto.randomBytes(32) 47 | }, this.valid_cookie_life * 5) 48 | 49 | this.associations_lookup = {} 50 | this.associations = [] 51 | 52 | this.on('icmp', this.onICMP.bind(this)) 53 | this.on('packet', this.onPacket.bind(this)) 54 | } 55 | 56 | onICMP(packet, src, dst, code) { 57 | const association = this._getAssociation(dst, packet.dst_port) 58 | if (association) { 59 | association.emit('icmp', packet, code) 60 | } 61 | } 62 | 63 | onPacket(packet, src, dst) { 64 | if (!Array.isArray(packet.chunks)) { 65 | this.debugger.warn('< received empty packet from %s:%d', src, packet.src_port) 66 | return 67 | } 68 | this.debugger.debug('< received packet from %s:%d', src, packet.src_port) 69 | let emulateLoss 70 | if (emulateLoss) { 71 | this.debugger.warn('emulate loss of remote packet') 72 | return 73 | } 74 | let lastDataChunk = -1 75 | let decodedChunks = [] 76 | const errors = [] 77 | const chunkTypes = {} 78 | let discardPacket = false 79 | 80 | // Check if packet should be discarded because of unrecognized chunks 81 | // Also collect errors, chunk types present, decoded chunks 82 | packet.chunks.every((buffer, index) => { 83 | const chunk = Chunk.fromBuffer(buffer) 84 | 85 | if (!chunk || chunk.error) { 86 | /* 87 | If the receiver detects a partial chunk, it MUST drop the chunk. 88 | */ 89 | return true 90 | } 91 | 92 | if (chunk.chunkType) { 93 | chunkTypes[chunk.chunkType] = chunk 94 | 95 | decodedChunks.push(chunk) 96 | chunk.buffer = buffer 97 | 98 | if (chunk.chunkType === 'data') { 99 | lastDataChunk = index 100 | } else if (chunk.chunkType === 'init') { 101 | // Ok 102 | } else if (chunk.chunkType === 'abort') { 103 | // Remaining chunks should be ignored 104 | return false 105 | } 106 | } else { 107 | this.debugger.warn('unrecognized chunk %s, action %s', chunk.chunkId, chunk.action) 108 | switch (chunk.action || 0) { 109 | case 0: 110 | /* 00 - Stop processing this SCTP packet and discard it, do not 111 | process any further chunks within it. */ 112 | discardPacket = true 113 | return false 114 | case 1: 115 | /* 01 - Stop processing this SCTP packet and discard it, do not 116 | process any further chunks within it, and report the 117 | unrecognized chunk in an 'Unrecognized Chunk Type'. */ 118 | discardPacket = true 119 | errors.push({ 120 | cause: 'UNRECONGNIZED_CHUNK_TYPE', 121 | unrecognized_chunk: buffer 122 | }) 123 | return false 124 | case 2: 125 | /* 10 - Skip this chunk and continue processing. */ 126 | break 127 | case 3: 128 | /* 11 - Skip this chunk and continue processing, but report in an 129 | ERROR chunk using the 'Unrecognized Chunk Type' cause of 130 | error. */ 131 | errors.push({ 132 | cause: 'UNRECONGNIZED_CHUNK_TYPE', 133 | unrecognized_chunk: buffer 134 | }) 135 | break 136 | default: 137 | } 138 | } 139 | return true 140 | }) 141 | 142 | let association = this._getAssociation(src, packet.src_port) 143 | 144 | if (association) { 145 | if (errors.length > 0 && !chunkTypes.abort) { 146 | this.debugger.warn('informing unrecognized chunks in packet', errors) 147 | association.ERROR(errors, packet.src) 148 | } 149 | } 150 | 151 | if (discardPacket) { 152 | return 153 | } 154 | 155 | if (decodedChunks.length === 0) { 156 | return 157 | } 158 | 159 | if (!association) { 160 | // 8.4. Handle "Out of the Blue" Packets 161 | this.debugger.debug('Handle "Out of the Blue" Packets') 162 | if (chunkTypes.abort) { 163 | // If the OOTB packet contains an ABORT chunk, the receiver MUST 164 | // silently discard the OOTB packet and take no further action. 165 | this.debugger.debug('OOTB ABORT, discard') 166 | return 167 | } 168 | if (chunkTypes.init) { 169 | /* 170 | If the packet contains an INIT chunk with a Verification Tag set 171 | to '0', process it as described in Section 5.1. If, for whatever 172 | reason, the INIT cannot be processed normally and an ABORT has to 173 | be sent in response, the Verification Tag of the packet 174 | containing the ABORT chunk MUST be the Initiate Tag of the 175 | received INIT chunk, and the T bit of the ABORT chunk has to be 176 | set to 0, indicating that the Verification Tag is NOT reflected. 177 | 178 | When an endpoint receives an SCTP packet with the Verification 179 | Tag set to 0, it should verify that the packet contains only an 180 | INIT chunk. Otherwise, the receiver MUST silently discard the 181 | packet. 182 | 183 | Furthermore, we require 184 | that the receiver of an INIT chunk MUST enforce these rules by 185 | silently discarding an arriving packet with an INIT chunk that is 186 | bundled with other chunks or has a non-zero verification tag and 187 | contains an INIT-chunk. 188 | */ 189 | if (packet.v_tag === 0 && packet.chunks.length === 1) { 190 | this.onInit(decodedChunks[0], src, dst, packet) 191 | } else { 192 | // all chunks count, including bogus 193 | this.debugger.warn('INIT rules violation, discard') 194 | } 195 | return 196 | } else if (chunkTypes.cookie_echo && decodedChunks[0].chunkType === 'cookie_echo') { 197 | association = this.onCookieEcho(decodedChunks[0], src, dst, packet) 198 | decodedChunks.shift() 199 | if (!association) { 200 | this.debugger.warn('Cookie Echo failed to establish association') 201 | return 202 | } 203 | } else if (chunkTypes.shutdown_ack) { 204 | /* 205 | If the packet contains a SHUTDOWN ACK chunk, the receiver should 206 | respond to the sender of the OOTB packet with a SHUTDOWN 207 | COMPLETE. When sending the SHUTDOWN COMPLETE, the receiver of 208 | the OOTB packet must fill in the Verification Tag field of the 209 | outbound packet with the Verification Tag received in the 210 | SHUTDOWN ACK and set the T bit in the Chunk Flags to indicate 211 | that the Verification Tag is reflected. 212 | */ 213 | const chunk = new Chunk('shutdown_complete', {flags: {T: 1}}) 214 | this._sendPacket(src, packet.src_port, packet.v_tag, [chunk.toBuffer()]) 215 | return 216 | } else if (chunkTypes.shutdown_complete) { 217 | /* 218 | If the packet contains a SHUTDOWN COMPLETE chunk, the receiver 219 | should silently discard the packet and take no further action. 220 | */ 221 | this.debugger.debug('OOTB SHUTDOWN COMPLETE, discard') 222 | return 223 | } else if (chunkTypes.error) { 224 | /* 225 | If the packet contains a "Stale Cookie" ERROR or a COOKIE ACK, 226 | the SCTP packet should be silently discarded. 227 | */ 228 | // Todo 229 | this.debugger.debug('OOTB ERROR, discard') 230 | return 231 | } else if (chunkTypes.cookie_ack) { 232 | this.debugger.debug('OOTB COOKIE ACK, discard') 233 | return 234 | } else { 235 | /* 236 | The receiver should respond to the sender of the OOTB packet with 237 | an ABORT. When sending the ABORT, the receiver of the OOTB 238 | packet MUST fill in the Verification Tag field of the outbound 239 | packet with the value found in the Verification Tag field of the 240 | OOTB packet and set the T bit in the Chunk Flags to indicate that 241 | the Verification Tag is reflected. After sending this ABORT, the 242 | receiver of the OOTB packet shall discard the OOTB packet and 243 | take no further action. 244 | */ 245 | if (this.ootb) { 246 | this.debugger.debug('OOTB packet, tolerate') 247 | } else { 248 | this.debugger.debug('OOTB packet, abort') 249 | const chunk = new Chunk('abort', {flags: {T: 1}}) 250 | this._sendPacket(src, packet.src_port, packet.v_tag, [chunk.toBuffer()]) 251 | } 252 | return 253 | } 254 | } 255 | 256 | if (!association) { 257 | // To be sure 258 | return 259 | } 260 | 261 | // all chunks count, including bogus 262 | if (packet.chunks.length > 1 && 263 | (chunkTypes.init || chunkTypes.init_ack || chunkTypes.shutdown_complete)) { 264 | this.debugger.warn('MUST NOT bundle INIT, INIT ACK, or SHUTDOWN COMPLETE.') 265 | return 266 | } 267 | 268 | // 8.5.1. Exceptions in Verification Tag Rules 269 | 270 | if (chunkTypes.abort) { 271 | if ( 272 | (packet.v_tag === association.my_tag && !chunkTypes.abort.flags.T) || 273 | (packet.v_tag === association.peer_tag && chunkTypes.abort.flags.T) 274 | ) { 275 | /* 276 | An endpoint MUST NOT respond to any received packet 277 | that contains an ABORT chunk (also see Section 8.4) 278 | */ 279 | association.mute = true 280 | // DATA chunks MUST NOT be bundled with ABORT 281 | // Todo. For now we just keep some types 282 | // init_ack will be ignored, cause it needs reply 283 | // all other control chunks are useful 284 | decodedChunks = decodedChunks.filter(chunk => 285 | chunk.chunkType === 'sack' || 286 | chunk.chunkType === 'cookie_ack' || 287 | chunk.chunkType === 'abort' 288 | ) 289 | } else { 290 | this.debugger.warn('discard according to Rules for packet carrying ABORT %O', packet) 291 | this.debugger.debug( 292 | 'v_tag %d, T-bit %s, my_tag %d, peer_tag %d', 293 | packet.v_tag, 294 | chunkTypes.abort.flags.T, 295 | association.my_tag, 296 | association.peer_tag 297 | ) 298 | return 299 | } 300 | } else if (chunkTypes.init) { 301 | if (packet.v_tag !== 0) { 302 | return 303 | } 304 | } else if (chunkTypes.shutdown_complete) { 305 | /* 306 | - The receiver of a SHUTDOWN COMPLETE shall accept the packet if 307 | the Verification Tag field of the packet matches its own tag and 308 | the T bit is not set OR if it is set to its peer's tag and the T 309 | bit is set in the Chunk Flags. Otherwise, the receiver MUST 310 | silently discard the packet and take no further action. An 311 | endpoint MUST ignore the SHUTDOWN COMPLETE if it is not in the 312 | SHUTDOWN-ACK-SENT state. 313 | */ 314 | if (!(packet.v_tag === association.my_tag && !chunkTypes.shutdown_complete.flags.T || 315 | packet.v_tag === association.peer_tag && chunkTypes.shutdown_complete.flags.T 316 | )) { 317 | return 318 | } 319 | } else { 320 | // 8.5. Verification Tag 321 | if (packet.v_tag !== association.my_tag) { 322 | this.debugger.warn('discarding packet, v_tag %d != my_tag %d', 323 | packet.v_tag, 324 | association.my_tag 325 | ) 326 | return 327 | } 328 | } 329 | 330 | // Todo shutdown_ack and shutdown_complete 331 | 332 | decodedChunks.forEach((chunk, index) => { 333 | chunk.last_in_packet = index === lastDataChunk 334 | this.debugger.debug('processing chunk %s from %s:%d', chunk.chunkType, src, packet.src_port) 335 | this.debugger.debug('emit chunk %s for association', chunk.chunkType) 336 | association.emit(chunk.chunkType, chunk, src, packet) 337 | }) 338 | } 339 | 340 | onInit(chunk, src, dst, header) { 341 | this.debugger.info('< CHUNK init', chunk.initiate_tag) 342 | 343 | // Check for errors in parameters. Note that chunk can already have parse errors. 344 | const errors = [] 345 | if ( 346 | chunk.initiate_tag === 0 || 347 | chunk.a_rwnd < 1500 || 348 | chunk.inbound_streams === 0 || 349 | chunk.outbound_streams === 0 350 | ) { 351 | /* 352 | If the value of the Initiate Tag in a received INIT chunk is found 353 | to be 0, the receiver MUST treat it as an error and close the 354 | association by transmitting an ABORT. 355 | An SCTP receiver MUST be able to receive a minimum of 1500 bytes in 356 | one SCTP packet. This means that an SCTP endpoint MUST NOT indicate 357 | less than 1500 bytes in its initial a_rwnd sent in the INIT or INIT 358 | ACK. 359 | A receiver of an INIT with the MIS value of 0 SHOULD abort 360 | the association. 361 | Note: A receiver of an INIT with the OS value set to 0 SHOULD 362 | abort the association. 363 | 364 | Invalid Mandatory Parameter: This error cause is returned to the 365 | originator of an INIT or INIT ACK chunk when one of the mandatory 366 | parameters is set to an invalid value. 367 | */ 368 | errors.push({cause: 'INVALID_MANDATORY_PARAMETER'}) 369 | } 370 | if (errors.length > 0) { 371 | const abort = new Chunk('abort', {error_causes: errors}) 372 | this._sendPacket(src, header.src_port, chunk.initiate_tag, [abort.toBuffer()]) 373 | return 374 | } 375 | const myTag = crypto.randomBytes(4).readUInt32BE(0) 376 | const cookie = this.createCookie(chunk, header, myTag) 377 | const initAck = new Chunk('init_ack', { 378 | initiate_tag: myTag, 379 | initial_tsn: myTag, 380 | a_rwnd: defs.NET_SCTP.RWND, 381 | state_cookie: cookie, 382 | outbound_streams: chunk.inbound_streams, 383 | inbound_streams: this.MIS 384 | }) 385 | if (this.localAddress) { 386 | initAck.ipv4_address = this.localAddress 387 | } 388 | if (chunk.errors) { 389 | this.debugger.warn('< CHUNK has errors (unrecognized parameters)', chunk.errors) 390 | initAck.unrecognized_parameter = chunk.errors 391 | } 392 | this.debugger.trace('> sending cookie', cookie) 393 | this._sendPacket(src, header.src_port, chunk.initiate_tag, [initAck.toBuffer()]) 394 | /* 395 | After sending the INIT ACK with the State Cookie parameter, the 396 | sender SHOULD delete the TCB and any other local resource related to 397 | the new association, so as to prevent resource attacks. 398 | */ 399 | } 400 | 401 | onCookieEcho(chunk, src, dst, header) { 402 | this.debugger.info('< CHUNK cookie_echo ', chunk.cookie) 403 | /* 404 | If the State Cookie is valid, create an association to the sender 405 | of the COOKIE ECHO chunk with the information in the TCB data 406 | carried in the COOKIE ECHO and enter the ESTABLISHED state. 407 | */ 408 | const cookieData = this.validateCookie(chunk.cookie, header) 409 | if (cookieData) { 410 | this.debugger.trace('cookie is valid') 411 | const initChunk = Chunk.fromBuffer(cookieData.initChunk) 412 | if (initChunk.chunkType !== 'init') { 413 | this.debugger.warn('--> this should be init chunk', initChunk) 414 | throw new Error('bug in chunk validation function') 415 | } 416 | const options = { 417 | remoteAddress: src, 418 | my_tag: cookieData.my_tag, 419 | remotePort: cookieData.src_port, 420 | MIS: this.MIS, 421 | OS: this.OS 422 | } 423 | const association = new Association(this, options) 424 | this.emit('association', association) 425 | association.acceptRemote(initChunk) 426 | return association 427 | } 428 | } 429 | 430 | _sendPacket(host, port, vTag, chunks, callback) { 431 | this.debugger.debug('> send packet %d chunks %s -> %s:%d vTag %d', 432 | chunks.length, 433 | this.localActiveAddress, 434 | host, 435 | port, 436 | vTag 437 | ) 438 | const packet = new Packet( 439 | { 440 | src_port: this.localPort, 441 | dst_port: port, 442 | v_tag: vTag 443 | }, 444 | chunks 445 | ) 446 | // Todo multi-homing select active address 447 | this.transport.sendPacket(this.localActiveAddress, host, packet, callback) 448 | } 449 | 450 | createCookie(chunk, header, myTag) { 451 | const created = Math.floor(new Date() / 1000) 452 | const information = Buffer.alloc(16) 453 | information.writeUInt32BE(created, 0) 454 | information.writeUInt32BE(this.valid_cookie_life, 4) 455 | information.writeUInt16BE(header.src_port, 8) 456 | information.writeUInt16BE(header.dst_port, 10) 457 | information.writeUInt32BE(myTag, 12) 458 | const hash = crypto.createHash(this.cookie_hmac_alg) 459 | hash.update(information) 460 | /* 461 | The receiver of the PAD 462 | parameter MUST silently discard this parameter and continue 463 | processing the rest of the INIT chunk. This means that the size of 464 | the generated COOKIE parameter in the INIT-ACK MUST NOT depend on the 465 | existence of the PAD parameter in the INIT chunk. A receiver of a 466 | PAD parameter MUST NOT include the PAD parameter within any State 467 | Cookie parameter it generates. 468 | 469 | Note: sctp_test doesn't follow this rule. 470 | */ 471 | delete chunk.pad 472 | const strippedInit = new Chunk('init', chunk) 473 | const initBuffer = strippedInit.toBuffer() 474 | hash.update(initBuffer) 475 | hash.update(this.cookieSecretKey) 476 | const mac = hash.digest() 477 | this.debugger.debug('created cookie mac %h %d bytes', mac, mac.length) 478 | /* 479 | 0 1 2 3 4 480 | 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 481 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 482 | | MAC | Information | INIT chunk ... 483 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 484 | | MAC | time | life |spt|dpt| my tag | INIT chunk ... 485 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 486 | */ 487 | return Buffer.concat([mac, information, initBuffer]) 488 | } 489 | 490 | validateCookie(cookie, header) { 491 | let result 492 | // MAC 16 + Info 16 + Init chunk 20 = 52 493 | if (cookie.length < 52) { 494 | return 495 | } 496 | const receivedMAC = cookie.slice(0, this.cookie_hmac_len) 497 | const information = cookie.slice(this.cookie_hmac_len, this.cookie_hmac_len + 16) 498 | const initChunk = cookie.slice(this.cookie_hmac_len + 16) 499 | /* 500 | Compute a MAC using the TCB data carried in the State Cookie and 501 | the secret key (note the timestamp in the State Cookie MAY be 502 | used to determine which secret key to use). 503 | */ 504 | const hash = crypto.createHash(defs.NET_SCTP.cookie_hmac_alg) 505 | hash.update(information) 506 | hash.update(initChunk) 507 | hash.update(this.cookieSecretKey) 508 | const mac = hash.digest() 509 | /* 510 | Authenticate the State Cookie as one that it previously generated 511 | by comparing the computed MAC against the one carried in the 512 | State Cookie. If this comparison fails, the SCTP header, 513 | including the COOKIE ECHO and any DATA chunks, should be silently 514 | discarded 515 | */ 516 | if (mac.equals(receivedMAC)) { 517 | result = { 518 | created: new Date(information.readUInt32BE(0) * 1000), 519 | cookie_lifespan: information.readUInt32BE(4), 520 | src_port: information.readUInt16BE(8), 521 | dst_port: information.readUInt16BE(10), 522 | my_tag: information.readUInt32BE(12) 523 | } 524 | /* 525 | Compare the port numbers and the Verification Tag contained 526 | within the COOKIE ECHO chunk to the actual port numbers and the 527 | Verification Tag within the SCTP common header of the received 528 | header. If these values do not match, the packet MUST be 529 | silently discarded. 530 | */ 531 | if ( 532 | header.src_port === result.src_port && 533 | header.dst_port === result.dst_port && 534 | header.v_tag === result.my_tag 535 | ) { 536 | /* 537 | Compare the creation timestamp in the State Cookie to the current 538 | local time. If the elapsed time is longer than the lifespan 539 | carried in the State Cookie, then the packet, including the 540 | COOKIE ECHO and any attached DATA chunks, SHOULD be discarded, 541 | and the endpoint MUST transmit an ERROR chunk with a "Stale 542 | Cookie" error cause to the peer endpoint. 543 | */ 544 | if (new Date() - result.created < result.cookie_lifespan) { 545 | result.initChunk = initChunk 546 | return result 547 | } 548 | } else { 549 | this.debugger.warn('port verification error', header, result) 550 | } 551 | } else { 552 | this.debugger.warn('mac verification error %h != %h', receivedMAC, mac) 553 | } 554 | } 555 | 556 | close() { 557 | this.emit('close') 558 | this.associations.forEach(association => { 559 | association.emit('COMMUNICATION LOST') 560 | association._destroy() 561 | }) 562 | this._destroy() 563 | } 564 | 565 | _destroy() { 566 | this.transport.unallocate(this.localPort) 567 | } 568 | 569 | _getAssociation(host, port) { 570 | const key = host + ':' + port 571 | this.debugger.trace('trying to find association for %s', key) 572 | return this.associations_lookup[key] 573 | } 574 | 575 | ASSOCIATE(options) { 576 | /* 577 | Format: ASSOCIATE(local SCTP instance name, 578 | destination transport addr, outbound stream count) 579 | -> association id [,destination transport addr list] 580 | [,outbound stream count] 581 | */ 582 | 583 | this.debugger.info('API ASSOCIATE', options) 584 | options = options || {} 585 | if (!options.remotePort) { 586 | throw new Error('port is required') 587 | } 588 | options.OS = options.OS || this.OS 589 | options.MIS = options.MIS || this.MIS 590 | const association = new Association(this, options) 591 | association.init() 592 | return association 593 | } 594 | 595 | DESTROY() { 596 | /* 597 | Format: DESTROY(local SCTP instance name) 598 | */ 599 | this.debugger.trace('API DESTROY') 600 | this._destroy() 601 | } 602 | 603 | static INITIALIZE(options, callback) { 604 | const endpoint = new Endpoint(options) 605 | // Todo register is synchronous for now, but could be async 606 | const port = transport.register(endpoint) 607 | if (port) { 608 | callback(null, endpoint) 609 | } else { 610 | callback(new Error('bind EADDRINUSE 0.0.0.0:' + options.localPort)) 611 | } 612 | } 613 | } 614 | 615 | module.exports = Endpoint 616 | -------------------------------------------------------------------------------- /lib/association.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const EventEmitter = require('events').EventEmitter 3 | const debug = require('debug') 4 | const ip = require('ip') 5 | const Chunk = require('./chunk') 6 | const defs = require('./defs') 7 | const SN = require('./serial') 8 | const Reassembly = require('./reassembly') 9 | 10 | class Association extends EventEmitter { 11 | constructor(endpoint, options) { 12 | super() 13 | 14 | this.state = 'CLOSED' 15 | this.endpoint = endpoint 16 | this.localPort = endpoint.localPort 17 | 18 | this.remoteAddress = options.remoteAddress || undefined 19 | this.remotePort = options.remotePort 20 | 21 | this.my_tag = options.my_tag || crypto.randomBytes(4).readUInt32BE(0) 22 | this.OS = options.OS 23 | this.MIS = options.MIS 24 | 25 | this.debugger = {} 26 | const label = `[${this.localPort}/${this.remoteAddress}:${this.remotePort}]` 27 | this.debugger.warn = debug(`sctp:assoc:### ${label}`) 28 | this.debugger.info = debug(`sctp:assoc:## ${label}`) 29 | this.debugger.debug = debug(`sctp:assoc:# ${label}`) 30 | this.debugger.trace = debug(`sctp:assoc: ${label}`) 31 | this.debugger.debug('create association') 32 | 33 | // Todo provide also good way to iterate 34 | const key = this.remoteAddress + ':' + this.remotePort 35 | endpoint.associations_lookup[key] = this 36 | endpoint.associations.push(this) 37 | 38 | this.reassembly = new Reassembly({ 39 | rwnd: defs.NET_SCTP.RWND 40 | }) 41 | 42 | this.reassembly.on('data', (buffer, stream) => { 43 | this._deliver(buffer, stream) 44 | }) 45 | 46 | this.rto_initial = defs.NET_SCTP.rto_initial 47 | this.rto_min = defs.NET_SCTP.rto_min 48 | this.rto_max = defs.NET_SCTP.rto_max 49 | 50 | const PMTU = 1500 // Todo 51 | 52 | this.peer_rwnd = 0 53 | 54 | // TODO: for each remote address if multi-homing 55 | this.sack_timeout = defs.NET_SCTP.sack_timeout 56 | this.sack_freq = defs.NET_SCTP.sack_freq 57 | this.hb_interval = defs.NET_SCTP.hb_interval 58 | this.flightsize = 0 59 | 60 | // 13.3. Per Transport Address Data 61 | this.default_address_data = { 62 | active: false, 63 | errors: 0, 64 | error_threshold: 10, 65 | cwnd: Math.min(4 * PMTU, Math.max(2 * PMTU, 4380)), // 7.2.1. Slow-Start 66 | RTO: this.rto_initial, 67 | SRTT: 0, 68 | RTTVAR: 0, 69 | PMTU, 70 | rtoPending: false // RTO-Pending 71 | } 72 | Object.assign(this, this.default_address_data) // Todo 73 | 74 | this.destinations = {} 75 | if (this.remoteAddress) { 76 | this.destinations[this.remoteAddress] = this.default_address_data 77 | } 78 | 79 | this.my_next_tsn = new SN(this.my_tag) 80 | this.HTNA = this.my_next_tsn.copy() 81 | 82 | this.bundling = 0 83 | this.sacks = 0 84 | this.ssn = [] 85 | this.fastRecovery = false 86 | this.readBuffer = [] 87 | this.everSentSack = false 88 | this.packetsSinceLastSack = 0 89 | this.queue = [] 90 | this.sent = {} 91 | this.countRcv = 0 92 | this.nonces = {} 93 | this.mute = false // If received ABORT chunk 94 | 95 | this.on('data', this.onData.bind(this)) 96 | this.on('sack', this.onSack.bind(this)) 97 | this.on('init', this.onInit.bind(this)) 98 | this.on('init_ack', this.onInitAck.bind(this)) 99 | this.on('heartbeat', this.onHeartbeat.bind(this)) 100 | this.on('heartbeat_ack', this.onHeartbeatAck.bind(this)) 101 | this.on('cookie_echo', this.onCookieEcho.bind(this)) 102 | this.on('cookie_ack', this.onCookieAck.bind(this)) 103 | this.on('shutdown', this.onShutdown.bind(this)) 104 | this.on('shutdown_ack', this.onShutdownAck.bind(this)) 105 | this.on('shutdown_complete', this.onShutdownComplete.bind(this)) 106 | this.on('error', this.onError.bind(this)) 107 | this.on('abort', this.onAbort.bind(this)) 108 | 109 | this.on('icmp', this.onICMP.bind(this)) 110 | } 111 | 112 | acceptRemote(chunk) { 113 | if (!chunk) { 114 | throw new Error('peer init chunk not provided') 115 | } 116 | this.debugger.debug('accept remote association with chunk %O', chunk) 117 | this._updatePeer(chunk) 118 | /* 119 | Todo 120 | A COOKIE ACK MAY be sent to an UNCONFIRMED address, 121 | but it MUST be bundled with a HEARTBEAT including a nonce. 122 | An implementation that does NOT support bundling 123 | MUST NOT send a COOKIE ACK to an UNCONFIRMED address. 124 | 2) For the receiver of the COOKIE ECHO, 125 | the only CONFIRMED address is the one to which the INIT-ACK was sent. 126 | */ 127 | this._up() 128 | this._sendChunk('cookie_ack', {}) 129 | } 130 | 131 | onInit(chunk, src, header) { 132 | this.debugger.warn( 133 | 'rfc4960 "5.2.2. Unexpected INIT ...', 134 | this.state, 135 | chunk, src, header 136 | ) 137 | /* 138 | Before responding, the endpoint MUST check to see if the 139 | unexpected INIT adds new addresses to the association. If new 140 | addresses are added to the association, the endpoint MUST respond 141 | with an ABORT, copying the 'Initiate Tag' of the unexpected INIT into 142 | the 'Verification Tag' of the outbound packet carrying the ABORT. In 143 | the ABORT response, the cause of error MAY be set to 'restart of an 144 | association with new addresses'. The error SHOULD list the addresses 145 | that were added to the restarting association. 146 | */ 147 | if (chunk.ipv4_address) { 148 | // todo check if addresses are really new. now is a placeholder 149 | let abort = new Chunk('abort', {}) 150 | // we MUST copy initiate_tag, so use low-level endpoint function to send packet 151 | this.endpoint._sendPacket( 152 | src, 153 | this.remotePort, 154 | chunk.initiate_tag, 155 | [abort.toBuffer()], 156 | () => { 157 | this.debugger.info('responded with an ABORT') 158 | } 159 | ) 160 | } 161 | } 162 | 163 | onCookieEcho(chunk, src, header) { 164 | this.debugger.warn( 165 | 'Handle a COOKIE ECHO when a TCB Exists', 166 | this.state, 167 | chunk 168 | ) 169 | const cookieData = this.endpoint.validateCookie(chunk.cookie, header) 170 | let initChunk 171 | if (cookieData) { 172 | this.debugger.trace('cookie is valid %O', cookieData) 173 | initChunk = Chunk.fromBuffer(cookieData.initChunk) 174 | } 175 | this.debugger.debug('association my_tag %d peer_tag %d, cookie my_tag %d peer_tag %d', 176 | this.my_tag, this.peer_tag, cookieData.my_tag, initChunk.initiate_tag) 177 | let action = '' 178 | if (this.my_tag === cookieData.my_tag) { 179 | // B or D 180 | if (this.peer_tag === initChunk.initiate_tag) { 181 | action = 'D' 182 | } else { 183 | action = 'B' 184 | } 185 | } else if (this.peer_tag === initChunk.initiate_tag) { 186 | // Todo tmp, implement tie-tags 187 | const tieTagsUnknown = true 188 | if (tieTagsUnknown) { 189 | action = 'C' 190 | } 191 | } else { 192 | // Todo tmp, implement tie-tags 193 | const tieTagsMatch = true 194 | if (tieTagsMatch) { 195 | action = 'A' 196 | } 197 | } 198 | this.debugger.warn('COOKIE ECHO action', action) 199 | switch (action) { 200 | case 'A': 201 | /* 202 | Todo tie-tags 203 | todo SHUTDOWN-ACK-SENT state 204 | A) In this case, the peer may have restarted. When the endpoint 205 | recognizes this potential 'restart', the existing session is 206 | treated the same as if it received an ABORT followed by a new 207 | COOKIE ECHO with the following exceptions: 208 | 209 | - Any SCTP DATA chunks MAY be retained (this is an 210 | implementation-specific option). 211 | 212 | - A notification of RESTART SHOULD be sent to the ULP instead of 213 | a "COMMUNICATION LOST" notification. 214 | 215 | All the congestion control parameters (e.g., cwnd, ssthresh) 216 | related to this peer MUST be reset to their initial values (see 217 | Section 6.2.1). 218 | 219 | After this, the endpoint shall enter the ESTABLISHED state. 220 | 221 | If the endpoint is in the SHUTDOWN-ACK-SENT state and recognizes 222 | that the peer has restarted (Action A), it MUST NOT set up a new 223 | association but instead resend the SHUTDOWN ACK and send an ERROR 224 | chunk with a "Cookie Received While Shutting Down" error cause to 225 | its peer. 226 | */ 227 | if (this.state === 'SHUTDOWN-ACK-SENT') { 228 | this._sendChunk('shutdown_ack', {}, src, () => { 229 | this.debugger.info('sent shutdown_ack') 230 | }) 231 | return 232 | } 233 | // Todo 234 | this.debugger.warn('association restart is not implemented and was not tested!') 235 | this.emit('RESTART') 236 | break 237 | case 'B': 238 | /* 239 | Todo init collision 240 | B) In this case, both sides may be attempting to start an association 241 | at about the same time, but the peer endpoint started its INIT 242 | after responding to the local endpoint's INIT. Thus, it may have 243 | picked a new Verification Tag, not being aware of the previous tag 244 | it had sent this endpoint. The endpoint should stay in or enter 245 | the ESTABLISHED state, but it MUST update its peer's Verification 246 | Tag from the State Cookie, stop any init or cookie timers that may 247 | be running, and send a COOKIE ACK. 248 | */ 249 | this.peer_tag = initChunk.initiate_tag 250 | // Todo stop init & cookie timers 251 | this._sendChunk('cookie_ack') 252 | break 253 | case 'C': 254 | /* 255 | C) In this case, the local endpoint's cookie has arrived late. 256 | Before it arrived, the local endpoint sent an INIT and received an 257 | INIT ACK and finally sent a COOKIE ECHO with the peer's same tag 258 | but a new tag of its own. The cookie should be silently 259 | discarded. The endpoint SHOULD NOT change states and should leave 260 | any timers running. 261 | */ 262 | break 263 | case 'D': 264 | /* 265 | D) When both local and remote tags match, the endpoint should enter 266 | the ESTABLISHED state, if it is in the COOKIE-ECHOED state. It 267 | should stop any cookie timer that may be running and send a COOKIE ACK. 268 | */ 269 | if (this.state === 'COOKIE-ECHOED') { 270 | this.state = 'ESTABLISHED' 271 | } 272 | // Todo should be already running, state also be ESTABLISHED 273 | // this._enableHeartbeat() 274 | // todo stop cookie timer 275 | this.debugger.warn('COOKIE ECHO send cookie_ack') 276 | this._sendChunk('cookie_ack') 277 | break 278 | default: 279 | /* 280 | Note: For any case not shown in Table 2, 281 | the cookie should be silently discarded 282 | */ 283 | } 284 | } 285 | 286 | onICMP(packet, code) { 287 | /* 288 | An implementation MAY ignore any ICMPv4 messages where the code 289 | does not indicate "Protocol Unreachable" or "Fragmentation Needed". 290 | */ 291 | this.debugger.warn('< received ICMP code %d', code) 292 | if (packet.v_tag && packet.v_tag !== this.peer_tag) { 293 | return 294 | } 295 | if (code === 4) { 296 | if (packet.v_tag === 0 && packet.chunks.length === 1) { 297 | const chunk = packet.chunks[0] 298 | if (chunk.chunkType === 'init' && chunk.initiate_tag === this.my_tag) { 299 | this.debugger.warn('< ICMP fragmentation needed') 300 | // Todo process this information as defined for PATH MTU discovery 301 | } 302 | } 303 | } else { 304 | this.debugger.warn('< ICMP signals that peer unreachable') 305 | this.emit('COMMUNICATION LOST') 306 | this._destroy() 307 | } 308 | } 309 | 310 | onData(chunk, source) { 311 | this.countRcv++ 312 | this.debugger.debug('< DATA %d, total: %d', chunk.tsn, this.countRcv) 313 | if (!(this.state === 'ESTABLISHED' || 314 | this.state === 'SHUTDOWN-PENDING' || 315 | this.state === 'SHUTDOWN-SENT' 316 | )) { 317 | return 318 | } 319 | if (!chunk.user_data || !chunk.user_data.length > 0) { 320 | this.debugger.warn('< received empty DATA chunk %o', chunk) 321 | this._abort({error_causes: [{cause: 'NO_USER_DATA', tsn: chunk.tsn}]}, source) 322 | return 323 | } 324 | 325 | const dataAccepted = this.reassembly.process(chunk) 326 | 327 | if (this.state === 'SHUTDOWN-SENT') { 328 | /* 329 | While in the SHUTDOWN-SENT state, the SHUTDOWN sender MUST 330 | immediately respond to each received packet containing one or more 331 | DATA chunks with a SHUTDOWN chunk and restart the T2-shutdown timer. 332 | If a SHUTDOWN chunk by itself cannot acknowledge all of the received 333 | DATA chunks (i.e., there are TSNs that can be acknowledged that are 334 | larger than the cumulative TSN, and thus gaps exist in the TSN 335 | sequence), or if duplicate TSNs have been received, then a SACK chunk 336 | MUST also be sent. 337 | */ 338 | this.debugger.trace('we are in the SHUTDOWN-SENT state - repeat SHUTDOWN') 339 | this._sendChunk('shutdown', {c_tsn_ack: this.reassembly.peer_c_tsn.number}) 340 | if (!this.reassembly.have_gaps && this.duplicates.length === 0) { 341 | return 342 | } 343 | } 344 | 345 | /* 346 | The guidelines on delayed acknowledgement algorithm specified in 347 | Section 4.2 of [RFC2581] SHOULD be followed. Specifically, an 348 | acknowledgement SHOULD be generated for at least every second packet 349 | (not every second DATA chunk) received, and SHOULD be generated 350 | within 200 ms of the arrival of any unacknowledged DATA chunk. In 351 | some situations, it may be beneficial for an SCTP transmitter to be 352 | more conservative than the algorithms detailed in this document 353 | allow. However, an SCTP transmitter MUST NOT be more aggressive than 354 | the following algorithms allow. 355 | */ 356 | 357 | if (!chunk.last_in_packet) { 358 | /* 359 | An SCTP receiver MUST NOT generate more than one SACK for every 360 | incoming packet, other than to update the offered window as the 361 | receiving application consumes new data. 362 | */ 363 | // todo sacks on data consumption? 364 | return 365 | } 366 | 367 | let immediately = false 368 | if (dataAccepted) { 369 | if (++this.packetsSinceLastSack >= this.sack_freq) { 370 | immediately = true 371 | } 372 | } 373 | if (!this.everSentSack) { 374 | /* 375 | After the reception of the first DATA chunk in an association the 376 | endpoint MUST immediately respond with a SACK to acknowledge the DATA 377 | chunk. Subsequent acknowledgements should be done as described in 378 | Section 6.2. 379 | */ 380 | immediately = true 381 | } 382 | if (this.reassembly.have_gaps) { 383 | /* 384 | If the endpoint detects a gap in the received DATA chunk sequence, 385 | it SHOULD send a SACK with Gap Ack Blocks immediately. 386 | */ 387 | immediately = true 388 | } 389 | 390 | if (this.reassembly.duplicates.length > 0) { 391 | /* 392 | When a packet arrives with duplicate DATA chunk(s) and with no new 393 | DATA chunk(s), the endpoint MUST immediately send a SACK with no delay. 394 | */ 395 | immediately = true 396 | } 397 | 398 | if (!dataAccepted) { 399 | /* 400 | In either case, if such a DATA chunk is dropped, the 401 | receiver MUST immediately send back a SACK with the current receive 402 | window showing only DATA chunks received and accepted so far. 403 | */ 404 | immediately = true 405 | } 406 | 407 | if (immediately) { 408 | // For all such we do sack immediately 409 | this.debugger.trace('SACK immediately') 410 | // Serves to group sack sending for chunks in packet 411 | this.sacks++ 412 | this._sack() 413 | } else { 414 | this.scheduleSack() 415 | } 416 | } 417 | 418 | scheduleSack() { 419 | if (this._sack_timer) { 420 | this.debugger.trace('SACK timer already set') 421 | } else { 422 | this.debugger.trace('SACK timer set', this.sack_timeout) 423 | this._sack_timer = setTimeout(() => { 424 | this.debugger.trace('SACK timer expired', this.sack_timeout) 425 | this._sack() 426 | }, this.sack_timeout) 427 | } 428 | } 429 | 430 | onSack(chunk) { 431 | this.debugger.trace('< sack c_tsn %d, peer_rwnd %d', chunk.c_tsn_ack, chunk.a_rwnd) 432 | /* 433 | A SACK MUST be processed in ESTABLISHED, SHUTDOWN-PENDING, and 434 | SHUTDOWN-RECEIVED. An incoming SACK MAY be processed in COOKIE- 435 | ECHOED. A SACK in the CLOSED state is out of the blue and SHOULD be 436 | processed according to the rules in Section 8.4. A SACK chunk 437 | received in any other state SHOULD be discarded. 438 | */ 439 | if ( 440 | !( 441 | this.state === 'ESTABLISHED' || 442 | this.state === 'SHUTDOWN-PENDING' || 443 | this.state === 'SHUTDOWN-RECEIVED' || 444 | this.state === 'COOKIE-ECHOED' 445 | ) 446 | ) { 447 | return 448 | } 449 | 450 | // Todo 'PROTOCOL_VIOLATION' The cumulative tsn ack beyond the max tsn currently sent 451 | // This.debugger.warn('< sack %O', chunk) 452 | this.peer_rwnd = chunk.a_rwnd 453 | 454 | if (this.drain_callback && this.drain()) { 455 | this.debugger.debug('drain callback c_tsn %d, peer_rwnd %d', chunk.c_tsn_ack, chunk.a_rwnd) 456 | this.drain_callback() 457 | delete this.drain_callback 458 | } 459 | 460 | const cTSN = new SN(chunk.c_tsn_ack) 461 | const ackAdvanced = this.c_tsn_ack ? cTSN.gt(this.c_tsn_ack) : true 462 | this.c_tsn_ack = cTSN.copy() 463 | 464 | if (this.fastRecovery && cTSN.ge(this.fastRecoveryExitPoint)) { 465 | this.fastRecovery = false 466 | this.fastRecoveryExitPoint = null 467 | } 468 | const flightsize = this.flightsize 469 | for (const tsn in this.sent) { 470 | const TSN = new SN(tsn) 471 | if (TSN.le(this.c_tsn_ack)) { 472 | this.debugger.trace('acknowledge tsn %d', tsn) 473 | this._acknowledge(TSN) 474 | } 475 | } 476 | if ( 477 | chunk.sack_info && 478 | chunk.sack_info.gap_blocks && 479 | chunk.sack_info.gap_blocks.length > 0 480 | ) { 481 | const gapBlocks = chunk.sack_info.gap_blocks 482 | this.debugger.trace('< gap blocks ', chunk.c_tsn_ack, gapBlocks) 483 | /* 484 | Whenever an endpoint receives a SACK that indicates that some TSNs 485 | are missing, it SHOULD wait for two further miss indications (via 486 | subsequent SACKs for a total of three missing reports) on the same 487 | TSNs before taking action with regard to Fast Retransmit. 488 | */ 489 | 490 | const absent = [] 491 | gapBlocks.forEach((block, idx) => { 492 | // Todo rewrite with SN api 493 | absent.push({ 494 | start: new SN(idx ? 495 | chunk.c_tsn_ack + gapBlocks[idx - 1].finish + 1 : 496 | chunk.c_tsn_ack + 1 497 | ), 498 | finish: new SN(chunk.c_tsn_ack + block.start - 1) 499 | }) 500 | for ( 501 | let t = this.c_tsn_ack.copy().inc(block.start); 502 | t.le(this.c_tsn_ack.copy().inc(block.finish)); 503 | t.inc(1) 504 | ) { 505 | if (this.sent[t.getNumber()]) { 506 | this._acknowledge(t) 507 | } 508 | } 509 | }) 510 | // 7.2.4. Fast Retransmit on Gap Reports 511 | /* 512 | Whenever an endpoint receives a SACK that indicates that some TSNs 513 | are missing, it SHOULD wait for two further miss indications (via 514 | subsequent SACKs for a total of three missing reports) on the same 515 | TSNs before taking action with regard to Fast Retransmit. 516 | */ 517 | let doFastRetransmit = false 518 | absent.forEach(block => { 519 | for (let TSN = block.start.copy(); TSN.le(block.finish); TSN.inc(1)) { 520 | const tsn = TSN.getNumber() 521 | if (this.sent[tsn]) { 522 | /* 523 | Miss indications SHOULD follow the HTNA (Highest TSN Newly 524 | Acknowledged) algorithm. For each incoming SACK, miss indications 525 | are incremented only for missing TSNs prior to the highest TSN newly 526 | acknowledged in the SACK. A newly acknowledged DATA chunk is one not 527 | previously acknowledged in a SACK. If an endpoint is in Fast 528 | Recovery and a SACK arrives that advances the Cumulative TSN Ack 529 | Point, the miss indications are incremented for all TSNs reported 530 | missing in the SACK. 531 | */ 532 | this.debugger.trace( 533 | 'fast retransmit %d ? HTNA %d, fast recovery %s, ack advanced %s', 534 | tsn, 535 | this.HTNA.number, 536 | this.fastRecovery, 537 | ackAdvanced 538 | ) 539 | if (TSN.lt(this.HTNA) || (this.fastRecovery && ackAdvanced)) { 540 | this.sent[tsn].losses++ 541 | this.debugger.trace( 542 | 'increase miss indications for %d to %d', 543 | tsn, 544 | this.sent[tsn].losses 545 | ) 546 | if (this.sent[tsn].losses >= 3) { 547 | /* 548 | Mark the DATA chunk(s) with three miss indications for 549 | retransmission. 550 | A straightforward implementation of the above keeps a counter for 551 | each TSN hole reported by a SACK. The counter increments for each 552 | consecutive SACK reporting the TSN hole. After reaching 3 and 553 | starting the Fast-Retransmit procedure, the counter resets to 0. 554 | */ 555 | this.sent[tsn].losses = 0 556 | this.sent[tsn].fastRetransmit = true 557 | doFastRetransmit = true 558 | } 559 | } 560 | } 561 | } 562 | }) 563 | if (doFastRetransmit) { 564 | this._fastRetransmit() 565 | } 566 | /* 567 | Whenever a SACK is received missing a TSN that was previously 568 | acknowledged via a Gap Ack Block, start the T3-rtx for the 569 | destination address to which the DATA chunk was originally 570 | transmitted if it is not already running. 571 | */ 572 | } else if (this.my_next_tsn.eq(this.c_tsn_ack.copy().inc(1))) { 573 | /* 574 | Whenever all outstanding data sent to an address have been 575 | acknowledged, turn off the T3-rtx timer of that address. 576 | */ 577 | this.flightsize = 0 578 | this.debugger.trace('all outstanding data has been acknowledged') 579 | this._stopT3() 580 | if (this.state === 'SHUTDOWN-PENDING') { 581 | this._shutdown() 582 | return 583 | } 584 | } 585 | if (chunk.sack_info && chunk.sack_info.duplicate_tsn && 586 | chunk.sack_info.duplicate_tsn.length > 0) { 587 | this.debugger.trace('peer indicates duplicates %o', chunk.sack_info.duplicate_tsn) 588 | } 589 | if (ackAdvanced && this.flightsize) { 590 | /* 591 | When cwnd is less than or equal to ssthresh, an SCTP endpoint MUST 592 | use the slow-start algorithm to increase cwnd only if the current 593 | congestion window is being fully utilized, an incoming SACK 594 | advances the Cumulative TSN Ack Point, and the data sender is not 595 | in Fast Recovery. Only when these three conditions are met can 596 | the cwnd be increased; otherwise, the cwnd MUST not be increased. 597 | If these conditions are met, then cwnd MUST be increased by, at 598 | most, the lesser of 1) the total size of the previously 599 | outstanding DATA chunk(s) acknowledged, and 2) the destination's 600 | path MTU. This upper bound protects against the ACK-Splitting 601 | attack outlined in [SAVAGE99]. 602 | */ 603 | // TODO: rule to increase cwnd is unclear to me 604 | if (this.cwnd <= this.ssthresh && this.cwnd <= this.flightsize && !this.fastRecovery) { 605 | const totalAcknowledgedSize = flightsize - this.flightsize 606 | const cwndIncrease = Math.min(totalAcknowledgedSize, this.PMTU) 607 | const previousCwnd = this.cwnd 608 | this.cwnd += cwndIncrease 609 | this.debugger.debug('increase cwnd +%d from %d to %d, ssthresh %d', 610 | cwndIncrease, previousCwnd, this.cwnd, this.ssthresh) 611 | } 612 | /* 613 | Whenever a SACK is received that acknowledges the DATA chunk 614 | with the earliest outstanding TSN for that address, restart the 615 | T3-rtx timer for that address with its current RTO (if there is 616 | still outstanding data on that address). 617 | */ 618 | this.debugger.trace('c_tsn_ack advanced to %d', this.c_tsn_ack.number) 619 | this._restartT3() 620 | } 621 | if (this.flightsize > 0 && this.flightsize < this.cwnd) { 622 | this.debugger.trace('flightsize %d < cwnd %d', this.flightsize, this.cwnd) 623 | this._retransmit() 624 | } 625 | } 626 | 627 | onInitAck(chunk, source) { 628 | if (this.state === 'COOKIE-WAIT') { 629 | this.debugger.debug('< init_ack cookie', chunk.state_cookie) 630 | clearTimeout(this.T1) 631 | if (chunk.inbound_streams === 0) { 632 | // Receiver of an INIT ACK with the MIS value set to 0 633 | // SHOULD destroy the association discarding its TCB 634 | this._abort({error_causes: [{cause: 'INVALID_MANDATORY_PARAMETER'}]}, source) 635 | return 636 | } 637 | this._updatePeer(chunk) 638 | this._sendChunk('cookie_echo', {cookie: chunk.state_cookie}, source, () => { 639 | this.debugger.debug('sent cookie_echo', chunk.state_cookie) 640 | }) 641 | /* 642 | If the receiver of an INIT ACK chunk detects unrecognized parameters 643 | and has to report them according to Section 3.2.1, it SHOULD bundle 644 | the ERROR chunk containing the 'Unrecognized Parameters' error cause 645 | with the COOKIE ECHO chunk sent in response to the INIT ACK chunk. 646 | If the receiver of the INIT ACK cannot bundle the COOKIE ECHO chunk 647 | with the ERROR chunk, the ERROR chunk MAY be sent separately but not 648 | before the COOKIE ACK has been received. 649 | 650 | Note: Any time a COOKIE ECHO is sent in a packet, it MUST be the 651 | first chunk. 652 | */ 653 | if (chunk.errors) { 654 | this.ERROR([{ 655 | cause: 'UNRECONGNIZED_PARAMETERS', 656 | unrecognized_parameters: Buffer.concat(chunk.errors) 657 | }], source) 658 | } 659 | this.state = 'COOKIE-ECHOED' 660 | } else { 661 | /* 662 | 5.2.3. Unexpected INIT ACK 663 | 664 | If an INIT ACK is received by an endpoint in any state other than the 665 | COOKIE-WAIT state, the endpoint should discard the INIT ACK chunk. 666 | An unexpected INIT ACK usually indicates the processing of an old or 667 | duplicated INIT chunk. 668 | */ 669 | this.debugger.warn('Unexpected INIT ACK') 670 | } 671 | } 672 | 673 | ERROR(errors, dst) { 674 | this._sendChunk('error', { 675 | error_causes: errors 676 | }, dst) 677 | } 678 | 679 | onHeartbeat(chunk, source) { 680 | /* 681 | A receiver of a HEARTBEAT MUST respond to a 682 | HEARTBEAT with a HEARTBEAT-ACK after entering the COOKIE-ECHOED state 683 | (INIT sender) or the ESTABLISHED state (INIT receiver), up until 684 | reaching the SHUTDOWN-SENT state (SHUTDOWN sender) or the SHUTDOWN- 685 | ACK-SENT state (SHUTDOWN receiver). todo 686 | */ 687 | this.debugger.trace( 688 | '< HEARTBEAT', 689 | chunk.heartbeat_info.length, 690 | chunk.heartbeat_info 691 | ) 692 | this._sendChunk( 693 | 'heartbeat_ack', 694 | {heartbeat_info: chunk.heartbeat_info}, 695 | source 696 | ) 697 | } 698 | 699 | onHeartbeatAck(chunk) { 700 | this.debugger.trace( 701 | '< HEARTBEAT ACK', 702 | chunk.heartbeat_info.length, 703 | chunk.heartbeat_info 704 | ) 705 | /* 706 | Upon receipt of the HEARTBEAT ACK, a verification is made that the 707 | nonce included in the HEARTBEAT parameter is the one sent to the 708 | address indicated inside the HEARTBEAT parameter. When this match 709 | occurs, the address that the original HEARTBEAT was sent to is now 710 | considered CONFIRMED and available for normal data transfer. 711 | */ 712 | const nonce = chunk.heartbeat_info.readUInt32BE(0) 713 | if (this.nonces[nonce]) { 714 | const address = ip.toString(chunk.heartbeat_info, 8, 4) 715 | this.debugger.trace('address confirmed alive', address) 716 | } 717 | delete this.nonces[nonce] 718 | } 719 | 720 | onCookieAck() { 721 | this.debugger.debug('< COOKIE ACK in state %s', this.state) 722 | if (this.state === 'COOKIE-ECHOED') { 723 | this._up() 724 | } 725 | } 726 | 727 | onShutdown(chunk, source) { 728 | // TODO: 9.2. Shutdown of an Association 729 | if (this.state === 'SHUTDOWN-RECEIVED') { 730 | /* 731 | Once an endpoint has reached the SHUTDOWN-RECEIVED state, it MUST NOT 732 | send a SHUTDOWN in response to a ULP request, and should discard 733 | subsequent SHUTDOWN chunks. 734 | */ 735 | return 736 | } 737 | 738 | this.debugger.info('< SHUTDOWN') 739 | 740 | // Todo check c_tsn_ack 741 | /* 742 | cause: 'PROTOCOL_VIOLATION', 743 | additional_information: 744 | 'The cumulative tsn ack beyond the max tsn currently sent:...' 745 | 746 | verify, by checking the Cumulative TSN Ack field of the chunk, 747 | that all its outstanding DATA chunks have been received by the 748 | SHUTDOWN sender. 749 | If there are still outstanding DATA chunks left, the SHUTDOWN 750 | receiver MUST continue to follow normal data transmission procedures 751 | defined in Section 6, until all outstanding DATA chunks are 752 | acknowledged; however, the SHUTDOWN receiver MUST NOT accept new data 753 | from its SCTP user. 754 | */ 755 | 756 | if (this.state === 'SHUTDOWN-SENT') { 757 | /* 758 | If an endpoint is in the SHUTDOWN-SENT state and receives a SHUTDOWN 759 | chunk from its peer, the endpoint shall respond immediately with a 760 | SHUTDOWN ACK to its peer, and move into the SHUTDOWN-ACK-SENT state 761 | restarting its T2-shutdown timer. 762 | */ 763 | this._sendChunk('shutdown_ack', {}, source, () => { 764 | this.debugger.info('sent shutdown_ack') 765 | }) 766 | this._restartT2() 767 | return 768 | } 769 | 770 | this.state = 'SHUTDOWN-RECEIVED' 771 | if (this.flightsize === 0) { 772 | this._sendChunk('shutdown_ack', {}, source, () => { 773 | this.state = 'SHUTDOWN-ACK-SENT' 774 | this.debugger.info('sent shutdown_ack') 775 | }) 776 | this._startT2() 777 | } 778 | } 779 | 780 | onShutdownAck(chunk, source) { 781 | /* 782 | Upon the receipt of the SHUTDOWN ACK, the SHUTDOWN sender shall stop 783 | the T2-shutdown timer, send a SHUTDOWN COMPLETE chunk to its peer, 784 | and remove all record of the association. 785 | */ 786 | this.debugger.info('< SHUTDOWN ACK in state %s', this.state) 787 | if (this.state === 'SHUTDOWN-SENT' || this.state === 'SHUTDOWN-ACK-SENT') { 788 | this._down() 789 | this.state = 'CLOSED' 790 | this.debugger.info('> sending SHUTDOWN COMPLETE') 791 | this._sendChunk('shutdown_complete', {}, source, () => { 792 | this.debugger.trace('sent SHUTDOWN COMPLETE') 793 | this.emit('SHUTDOWN COMPLETE') 794 | this._destroy() 795 | }) 796 | } 797 | } 798 | 799 | onShutdownComplete() { 800 | /* 801 | - The receiver of a SHUTDOWN COMPLETE shall accept the packet if 802 | the Verification Tag field of the packet matches its own tag and 803 | the T bit is not set OR if it is set to its peer's tag and the T 804 | bit is set in the Chunk Flags. Otherwise, the receiver MUST 805 | silently discard the packet and take no further action. An 806 | endpoint MUST ignore the SHUTDOWN COMPLETE if it is not in the 807 | SHUTDOWN-ACK-SENT state. 808 | 809 | Upon reception of the SHUTDOWN COMPLETE chunk, the endpoint will 810 | verify that it is in the SHUTDOWN-ACK-SENT state; if it is not, the 811 | chunk should be discarded. If the endpoint is in the SHUTDOWN-ACK- 812 | SENT state, the endpoint should stop the T2-shutdown timer and remove 813 | all knowledge of the association (and thus the association enters the 814 | CLOSED state). 815 | */ 816 | if (this.state === 'SHUTDOWN-ACK-SENT') { 817 | this._down() 818 | this.debugger.info('< SHUTDOWN COMPLETE') 819 | this.emit('SHUTDOWN COMPLETE') 820 | this._destroy() 821 | } 822 | } 823 | 824 | onError(chunk) { 825 | this.debugger.warn('< ERROR', chunk) 826 | if ( 827 | chunk.error_causes.some( 828 | item => item.cause === 'STALE_COOKIE_ERROR' 829 | ) 830 | ) { 831 | // TODO: 5.2.6. Handle Stale COOKIE Error 832 | } 833 | this.emit('COMMUNICATION ERROR', chunk.error_causes) 834 | } 835 | 836 | onAbort(chunk) { 837 | this.debugger.info('< ABORT, connection closed') 838 | if (chunk.error_causes) { 839 | this.debugger.warn('< ABORT has error causes', chunk.error_causes) 840 | } 841 | this._down() 842 | if (this.queue.length > 0) { 843 | this.debugger.trace('abandon sending of chunks', this.queue.length) 844 | } 845 | this.queue = [] 846 | this.emit('COMMUNICATION LOST', 'abort', chunk.error_causes) 847 | this._destroy() 848 | } 849 | 850 | init() { 851 | const initParams = { 852 | initiate_tag: this.my_tag, 853 | a_rwnd: defs.NET_SCTP.RWND, 854 | outbound_streams: this.OS, 855 | inbound_streams: this.MIS, 856 | initial_tsn: this.my_next_tsn.number 857 | } 858 | 859 | if (this.endpoint.localAddress) { 860 | initParams.ipv4_address = this.endpoint.localAddress 861 | } 862 | this.debugger.info('INIT params', initParams) 863 | 864 | let counter = 0 865 | this.RTI = this.rto_initial 866 | const init = () => { 867 | if (counter >= defs.NET_SCTP.max_init_retransmits) { 868 | // Fail 869 | } else { 870 | if (counter) { 871 | // Not from RFC, but from lk-sctp 872 | this.RTI *= 2 873 | if (this.RTI > this.rto_max) { 874 | this.RTI = this.rto_max 875 | } 876 | } 877 | this._sendChunk('init', initParams) 878 | counter++ 879 | this.T1 = setTimeout(init, this.RTO) 880 | } 881 | } 882 | init() 883 | this.state = 'COOKIE-WAIT' 884 | } 885 | 886 | _sendChunk(chunkType, options, destination, callback) { 887 | const chunk = new Chunk(chunkType, options) 888 | this.debugger.debug('> send chunk', chunkType) 889 | this.debugger.trace('> %O', chunk) 890 | /* 891 | By default, an endpoint SHOULD always transmit to the primary path, 892 | unless the SCTP user explicitly specifies the destination transport 893 | address (and possibly source transport address) to use. 894 | 895 | An endpoint SHOULD transmit reply chunks (e.g., SACK, HEARTBEAT ACK, 896 | etc.) to the same destination transport address from which it 897 | received the DATA or control chunk to which it is replying. This 898 | rule should also be followed if the endpoint is bundling DATA chunks 899 | together with the reply chunk. 900 | 901 | However, when acknowledging multiple DATA chunks received in packets 902 | from different source addresses in a single SACK, the SACK chunk may 903 | be transmitted to one of the destination transport addresses from 904 | which the DATA or control chunks being acknowledged were received. 905 | */ 906 | if ( 907 | chunkType === 'data' || 908 | chunkType === 'sack' || 909 | chunkType === 'heartbeat_ack' 910 | ) { 911 | // RFC allows to bundle other control chunks, 912 | // but this gives almost no benefits 913 | this.debugger.trace('> bundle-send', chunkType, options) 914 | chunk.callback = callback 915 | if (chunkType === 'data') { 916 | // Do not encode here because we'll add tsn later, during bundling 917 | chunk.size = chunk.user_data.length + 16 918 | } else { 919 | chunk.buffer = chunk.toBuffer() 920 | chunk.size = chunk.buffer.length 921 | } 922 | this.queue.push(chunk) 923 | this.bundling++ 924 | setTimeout(() => { 925 | this._bundle() 926 | }) 927 | } else { 928 | // No bundle 929 | // setTimeout(() => { 930 | // use nextTick to be in order with bundled chunks 931 | const buffer = chunk.toBuffer() 932 | this.debugger.trace('> no-bundle send', chunkType) 933 | this._sendPacket([buffer], destination, [callback]) 934 | // }, 0) 935 | } 936 | } 937 | 938 | _sack() { 939 | if (this._sack_timer) { 940 | this.debugger.trace('cancel SACK timer and do it now') 941 | clearTimeout(this._sack_timer) 942 | delete this._sack_timer 943 | } 944 | this.sacks-- 945 | if (this.sacks > 0) { 946 | // Wait for last sack request in idle cycle 947 | this.debugger.trace('grouping SACKs, wait %d more...', this.sacks) 948 | return 949 | } 950 | const sackOptions = this.reassembly.sackInfo() 951 | this.debugger.trace('prepared SACK %O', sackOptions) 952 | this._sendChunk('sack', sackOptions) 953 | this.everSentSack = true 954 | this.packetsSinceLastSack = 0 955 | } 956 | 957 | _acknowledge(TSN) { 958 | this.debugger.trace('acknowledge tsn %d, peer_rwnd %d', TSN.number, this.peer_rwnd) 959 | this.flightsize -= this.sent[TSN.getNumber()].size 960 | if (!this.HTNA || TSN.gt(this.HTNA)) { 961 | this.HTNA = TSN.copy() 962 | } 963 | delete this.sent[TSN.getNumber()] 964 | // RTO calculation 965 | if (this.rtoPending && this.rtoPending.tsn.eq(TSN)) { 966 | this._updateRTO(new Date() - this.rtoPending.sent) 967 | this.rtoPending = false 968 | } 969 | } 970 | 971 | _updateRTO(R) { 972 | if (this.SRTT) { 973 | const alpha = 1 / defs.NET_SCTP.rto_alpha_exp_divisor 974 | const beta = 1 / defs.NET_SCTP.rto_beta_exp_divisor 975 | this.RTTVAR = (1 - beta) * this.RTTVAR + beta * Math.abs(this.SRTT - R) 976 | this.RTTVAR = Math.max(this.RTTVAR, defs.NET_SCTP.G) 977 | this.SRTT = (1 - alpha) * this.SRTT + alpha * R 978 | this.RTO = this.SRTT + 4 * this.RTTVAR 979 | } else { 980 | this.SRTT = R 981 | this.RTTVAR = R / 2 982 | this.RTTVAR = Math.max(this.RTTVAR, defs.NET_SCTP.G) 983 | this.RTO = this.SRTT + 4 * this.RTTVAR 984 | } 985 | if (this.RTO < this.rto_min) { 986 | this.RTO = this.rto_min 987 | } 988 | if (this.RTO > this.rto_max) { 989 | this.RTO = this.rto_max 990 | } 991 | this.debugger.trace('new RTO %d', this.RTO) 992 | } 993 | 994 | _startT2() { 995 | if (this.T2) { 996 | this.debugger.trace('T2-shutdown timer is already running') 997 | return 998 | } 999 | this.debugger.trace('start T3-shutdown timer (RTO %d)', this.RTO) 1000 | this.T2 = setTimeout(this._expireT2.bind(this), this.RTO) 1001 | } 1002 | 1003 | _stopT2() { 1004 | if (this.T2) { 1005 | this.debugger.trace('stop T2-shutdown timer') 1006 | clearTimeout(this.T2) 1007 | this.T2 = null 1008 | } 1009 | } 1010 | 1011 | _restartT2() { 1012 | this.debugger.trace('restart T2-shutdown timer') 1013 | this._stopT2() 1014 | this._startT2() 1015 | } 1016 | 1017 | _expireT2() { 1018 | if (this.state === 'SHUTDOWN-SENT') { 1019 | // todo source 1020 | this._sendChunk('shutdown', {}, null, () => { 1021 | this.debugger.info('resent shutdown') 1022 | }) 1023 | } else if (this.state === 'SHUTDOWN-ACK-SENT') { 1024 | // todo source 1025 | this._sendChunk('shutdown_ack', {}, null, () => { 1026 | this.debugger.info('resent shutdown ack') 1027 | }) 1028 | } 1029 | } 1030 | 1031 | _startT3() { 1032 | if (this.T3) { 1033 | this.debugger.trace('T3-rtx timer is already running') 1034 | return 1035 | } 1036 | this.debugger.trace('start T3-rtx timer (RTO %d)', this.RTO) 1037 | this.T3 = setTimeout(this._expireT3.bind(this), this.RTO) 1038 | } 1039 | 1040 | _stopT3() { 1041 | if (this.T3) { 1042 | this.debugger.trace('stop T3-rtx timer') 1043 | clearTimeout(this.T3) 1044 | this.T3 = null 1045 | } 1046 | } 1047 | 1048 | _restartT3() { 1049 | this.debugger.trace('restart T3-rtx timer') 1050 | this._stopT3() 1051 | this._startT3() 1052 | } 1053 | 1054 | _expireT3() { 1055 | this.T3 = null 1056 | this.debugger.trace('T3-rtx timer has expired') 1057 | if (Object.keys(this.sent).length === 0) { 1058 | this.debugger.warn('bug: there are no chunks in flight') 1059 | return 1060 | } 1061 | 1062 | /* 1063 | 6.3.3. Handle T3-rtx Expiration 1064 | 1065 | Whenever the retransmission timer T3-rtx expires for a destination 1066 | address, do the following: 1067 | 1068 | E1) For the destination address for which the timer expires, adjust 1069 | its ssthresh with rules defined in Section 7.2.3 and set the 1070 | cwnd <- MTU. 1071 | 1072 | When the T3-rtx timer expires on an address, SCTP should perform slow 1073 | start by: 1074 | 1075 | ssthresh = max(cwnd/2, 4*MTU) 1076 | cwnd = 1*MTU 1077 | 1078 | and ensure that no more than one SCTP packet will be in flight for 1079 | that address until the endpoint receives acknowledgement for 1080 | successful delivery of data to that address. 1081 | */ 1082 | this.ssthresh = Math.max(this.cwnd / 2, 4 * this.PMTU) 1083 | this.cwnd = this.PMTU 1084 | /* 1085 | E2) For the destination address for which the timer expires, set RTO 1086 | <- RTO * 2 ("back off the timer"). The maximum value discussed 1087 | in rule C7 above (RTO.max) may be used to provide an upper bound 1088 | to this doubling operation. 1089 | */ 1090 | if (this.RTO < this.rto_max) { 1091 | this.RTO *= 2 1092 | if (this.RTO > this.rto_max) { 1093 | this.RTO = this.rto_max 1094 | } 1095 | } 1096 | this.debugger.trace( 1097 | 'adjustments on expire: cwnd %d / ssthresh %d / RTO %d', 1098 | this.cwnd, 1099 | this.ssthresh, 1100 | this.RTO 1101 | ) 1102 | /* 1103 | E3) Determine how many of the earliest (i.e., lowest TSN) 1104 | outstanding DATA chunks for the address for which the T3-rtx has 1105 | expired will fit into a single packet, subject to the MTU 1106 | constraint for the path corresponding to the destination 1107 | transport address to which the retransmission is being sent 1108 | (this may be different from the address for which the timer 1109 | expires; see Section 6.4). Call this value K. Bundle and 1110 | retransmit those K DATA chunks in a single packet to the 1111 | destination endpoint. 1112 | */ 1113 | let bundledLength = 20 1114 | let bundledCount = 0 1115 | const tsns = [] 1116 | for (const tsn in this.sent) { 1117 | const chunk = this.sent[tsn] 1118 | this.debugger.trace('retransmit tsn %d', chunk.tsn) 1119 | if (bundledLength + chunk.user_data.length + 16 > this.PMTU) { 1120 | /* 1121 | Note: Any DATA chunks that were sent to the address for which the 1122 | T3-rtx timer expired but did not fit in one MTU (rule E3 above) 1123 | should be marked for retransmission and sent as soon as cwnd allows 1124 | (normally, when a SACK arrives). 1125 | */ 1126 | this.debugger.trace('retransmit tsn later %d', chunk.tsn) 1127 | chunk.retransmit = true 1128 | } else { 1129 | bundledCount++ 1130 | bundledLength += chunk.user_data.length + 16 1131 | tsns.push(chunk.tsn) 1132 | this._sendChunk('data', chunk) 1133 | } 1134 | } 1135 | this.debugger.trace( 1136 | 'retransmit %d chunks, %d bytes, %o', 1137 | bundledLength, 1138 | bundledCount, 1139 | tsns 1140 | ) 1141 | if (bundledCount > 0) { 1142 | /* 1143 | E4) Start the retransmission timer T3-rtx on the destination address 1144 | to which the retransmission is sent, if rule R1 above indicates 1145 | to do so. The RTO to be used for starting T3-rtx should be the 1146 | one for the destination address to which the retransmission is 1147 | sent, which, when the receiver is multi-homed, may be different 1148 | from the destination address for which the timer expired (see 1149 | Section 6.4 below). 1150 | */ 1151 | this._startT3() 1152 | } 1153 | /* 1154 | After retransmitting, once a new RTT measurement is obtained (which 1155 | can happen only when new data has been sent and acknowledged, per 1156 | rule C5, or for a measurement made from a HEARTBEAT; see Section 1157 | 8.3), the computation in rule C3 is performed, including the 1158 | computation of RTO, which may result in "collapsing" RTO back down 1159 | after it has been subject to doubling (rule E2). 1160 | */ 1161 | } 1162 | 1163 | _retransmit() { 1164 | this.debugger.trace('check retransmits') 1165 | for (const tsn in this.sent) { 1166 | const chunk = this.sent[tsn] 1167 | if (chunk.retransmit) { 1168 | // Todo explain 1169 | this.debugger.warn('more retransmit', chunk.tsn) 1170 | this._sendChunk('data', chunk) 1171 | } 1172 | } 1173 | } 1174 | 1175 | _fastRetransmit() { 1176 | /* 1177 | Note: Before the above adjustments, if the received SACK also 1178 | acknowledges new DATA chunks and advances the Cumulative TSN Ack 1179 | Point, the cwnd adjustment rules defined in Section 7.2.1 and Section 1180 | 7.2.2 must be applied first. 1181 | */ 1182 | if (!this.fastRecovery) { 1183 | /* 1184 | If not in Fast Recovery, adjust the ssthresh and cwnd of the 1185 | destination address(es) to which the missing DATA chunks were 1186 | last sent, according to the formula described in Section 7.2.3. 1187 | 1188 | ssthresh = max(cwnd/2, 4*MTU) 1189 | cwnd = ssthresh 1190 | partial_bytes_acked = 0 1191 | 1192 | Basically, a packet loss causes cwnd to be cut in half. 1193 | */ 1194 | this.ssthresh = Math.max(this.cwnd / 2, 4 * this.PMTU) 1195 | this.cwnd = this.ssthresh 1196 | this.partial_bytes_acked = 0 // Todo 1197 | /* 1198 | If not in Fast Recovery, enter Fast Recovery and mark the highest 1199 | outstanding TSN as the Fast Recovery exit point. When a SACK 1200 | acknowledges all TSNs up to and including this exit point, Fast 1201 | Recovery is exited. While in Fast Recovery, the ssthresh and 1202 | cwnd SHOULD NOT change for any destinations due to a subsequent 1203 | Fast Recovery event (i.e., one SHOULD NOT reduce the cwnd further 1204 | due to a subsequent Fast Retransmit). 1205 | */ 1206 | this.fastRecovery = true 1207 | this.fastRecoveryExitPoint = this.my_next_tsn.prev() 1208 | this.debugger.trace('entered fast recovery mode, cwnd %d, ssthresh %d', 1209 | this.cwnd, 1210 | this.ssthresh 1211 | ) 1212 | } 1213 | /* 1214 | 3) Determine how many of the earliest (i.e., lowest TSN) DATA chunks 1215 | marked for retransmission will fit into a single packet, subject 1216 | to constraint of the path MTU of the destination transport 1217 | address to which the packet is being sent. Call this value K. 1218 | Retransmit those K DATA chunks in a single packet. When a Fast 1219 | Retransmit is being performed, the sender SHOULD ignore the value 1220 | of cwnd and SHOULD NOT delay retransmission for this single 1221 | packet. 1222 | */ 1223 | let bundledLength = 36 // 20 + 16 1224 | let bundledCount = 0 1225 | const tsns = [] 1226 | for (const tsn in this.sent) { 1227 | const chunk = this.sent[tsn] 1228 | if (chunk.fastRetransmit) { 1229 | this.debugger.trace('fast retransmit tsn %d', chunk.tsn) 1230 | if (bundledLength + chunk.user_data.length + 16 > this.PMTU) { 1231 | return true 1232 | } 1233 | bundledCount++ 1234 | bundledLength += chunk.user_data.length + 16 1235 | tsns.push(chunk.tsn) 1236 | this._sendChunk('data', chunk) 1237 | } 1238 | } 1239 | 1240 | this.debugger.trace( 1241 | 'fast retransmit %d chunks, %d bytes, %o', 1242 | bundledLength, 1243 | bundledCount, 1244 | tsns 1245 | ) 1246 | /* 1247 | 4) Restart the T3-rtx timer only if the last SACK acknowledged the 1248 | lowest outstanding TSN number sent to that address, or the 1249 | endpoint is retransmitting the first outstanding DATA chunk sent 1250 | to that address. 1251 | */ 1252 | // TODO: Restart the T3-rtx timer only if the last SACK acknowledged 1253 | if (bundledCount > 0) { 1254 | this._restartT3() 1255 | } 1256 | } 1257 | 1258 | _up() { 1259 | /* 1260 | HEARTBEAT sending MAY begin upon reaching the 1261 | ESTABLISHED state and is discontinued after sending either SHUTDOWN 1262 | or SHUTDOWN-ACK. todo 1263 | */ 1264 | this.state = 'ESTABLISHED' 1265 | this._enableHeartbeat() 1266 | this.debugger.info('association established') 1267 | this.emit('COMMUNICATION UP') 1268 | } 1269 | 1270 | _down() { 1271 | clearInterval(this._heartbeatInterval) 1272 | clearTimeout(this.T1) 1273 | clearTimeout(this.T2) 1274 | clearTimeout(this.T3) 1275 | clearTimeout(this._sack_timer) 1276 | } 1277 | 1278 | _enableHeartbeat() { 1279 | this._heartbeatInterval = setInterval(() => { 1280 | /* 1281 | To probe an address for verification, an endpoint will send 1282 | HEARTBEATs including a 64-bit random nonce and a path indicator (to 1283 | identify the address that the HEARTBEAT is sent to) within the 1284 | HEARTBEAT parameter. 1285 | */ 1286 | for (const address in this.destinations) { 1287 | const destination = this.destinations[address] 1288 | const heartbeatInfo = crypto.randomBytes(12) 1289 | const nonce = heartbeatInfo.readUInt32BE(0) 1290 | this.nonces[nonce] = true 1291 | ip.toBuffer(address, heartbeatInfo, 8) 1292 | this.debugger.trace( 1293 | '> heartbeat to %s, %d bytes', 1294 | address, 1295 | heartbeatInfo.length, 1296 | heartbeatInfo, 1297 | destination 1298 | ) 1299 | this._sendChunk('heartbeat', {heartbeat_info: heartbeatInfo}, address) 1300 | /* 1301 | The endpoint should increment the respective error counter of the 1302 | destination transport address each time a HEARTBEAT is sent to that 1303 | address and not acknowledged within one RTO. 1304 | 1305 | When the value of this counter reaches the protocol parameter 1306 | 'Path.Max.Retrans', the endpoint should mark the corresponding 1307 | destination address as inactive if it is not so marked, and may also 1308 | optionally report to the upper layer the change of reachability of 1309 | this destination address. After this, the endpoint should continue 1310 | HEARTBEAT on this destination address but should stop increasing the 1311 | counter. 1312 | */ 1313 | } 1314 | }, this.hb_interval) 1315 | } 1316 | 1317 | _sendPacket(buffers, destination, callbacks) { 1318 | // TODO: order of destroying 1319 | if (this.mute) { 1320 | return 1321 | } 1322 | if (!this.endpoint) { 1323 | return 1324 | } 1325 | this.endpoint._sendPacket( 1326 | destination || this.remoteAddress, 1327 | this.remotePort, 1328 | this.peer_tag, 1329 | buffers, 1330 | () => { 1331 | callbacks.forEach(cb => { 1332 | // Callback for each chunk 1333 | if (typeof cb === 'function') { 1334 | cb() 1335 | } 1336 | }) 1337 | } 1338 | ) 1339 | } 1340 | 1341 | _deliver(data, stream) { 1342 | this.debugger.debug('< receive user data %d bytes', data.length) 1343 | if (this.listeners('DATA ARRIVE')) { 1344 | this.debugger.trace('emit DATA ARRIVE') 1345 | this.readBuffer.push(data) 1346 | this.emit('DATA ARRIVE', stream) 1347 | } 1348 | /* 1349 | An SCTP receiver MUST NOT generate more than one SACK for every 1350 | incoming packet, other than to update the offered window as the 1351 | receiving application consumes new data. 1352 | */ 1353 | this.scheduleSack() 1354 | } 1355 | 1356 | _bundle() { 1357 | if (this.state === 'CLOSED') { 1358 | return 1359 | } 1360 | if (this.queue.length === 0) { 1361 | return 1362 | } 1363 | this.bundling-- 1364 | if (this.bundling > 0) { 1365 | return 1366 | } 1367 | let callbacks = [] 1368 | let bundledChunks = [] 1369 | let bundledLength = 36 // 20 + 16 1370 | const mtu = this.PMTU 1371 | const emulateLoss = false 1372 | let haveCookieEcho = false 1373 | let haveData = false 1374 | let tsns = [] 1375 | let sack 1376 | let skip 1377 | 1378 | // Move last sack to the beginning of queue, ignore others 1379 | const processedQueue = [] 1380 | this.queue.forEach(chunk => { 1381 | if (chunk.chunkType === 'sack') { 1382 | sack = chunk 1383 | } else { 1384 | processedQueue.push(chunk) 1385 | } 1386 | }) 1387 | if (sack) { 1388 | processedQueue.unshift(sack) 1389 | } 1390 | 1391 | this.debugger.trace('process bundle queue %O', processedQueue) 1392 | processedQueue.forEach((chunk, index) => { 1393 | let buffer 1394 | if (chunk.size > mtu) { 1395 | this.debugger.warn('chunk size %d > MTU %d', chunk.size, mtu) 1396 | // Todo split chunks 1397 | skip = true 1398 | } else if (chunk.chunkType === 'data') { 1399 | haveData = true 1400 | /* 1401 | Data transmission MUST only happen in the ESTABLISHED, SHUTDOWN- 1402 | PENDING, and SHUTDOWN-RECEIVED states. The only exception to this is 1403 | that DATA chunks are allowed to be bundled with an outbound COOKIE 1404 | ECHO chunk when in the COOKIE-WAIT state. 1405 | */ 1406 | if ( 1407 | this.state === 'ESTABLISHED' || 1408 | this.state === 'SHUTDOWN-PENDING' || 1409 | this.state === 'SHUTDOWN-RECEIVED' 1410 | ) { 1411 | // Allow 1412 | } else if (this.state === 'COOKIE-WAIT' && haveCookieEcho) { 1413 | // Allow 1414 | } else { 1415 | // TODO: force bundle 1416 | this.debugger.warn( 1417 | 'data transmission MUST only happen ' + 1418 | 'in the ESTABLISHED, SHUTDOWN-PENDING, ' + 1419 | 'and SHUTDOWN-RECEIVED states' 1420 | ) 1421 | return 1422 | } 1423 | /* 1424 | IMPLEMENTATION NOTE: In order to better support the data life time 1425 | option, the transmitter may hold back the assigning of the TSN number 1426 | to an outbound DATA chunk to the last moment. And, for 1427 | implementation simplicity, once a TSN number has been assigned the 1428 | sender should consider the send of this DATA chunk as committed, 1429 | overriding any life time option attached to the DATA chunk. 1430 | */ 1431 | if (chunk.tsn === null) { 1432 | // Not a retransmit 1433 | chunk.tsn = this.my_next_tsn.getNumber() 1434 | this.debugger.trace('last-minute set tsn to %d', chunk.tsn) 1435 | this.my_next_tsn.inc(1) 1436 | } 1437 | if (!this.rtoPending) { 1438 | this.rtoPending = { 1439 | tsn: new SN(chunk.tsn), 1440 | sent: new Date() 1441 | } 1442 | } 1443 | buffer = chunk.toBuffer() 1444 | tsns.push(chunk.tsn) 1445 | chunk.losses = 0 1446 | this.sent[chunk.tsn] = chunk 1447 | this.flightsize += buffer.length 1448 | } else { 1449 | buffer = chunk.buffer 1450 | delete chunk.buffer 1451 | if (chunk.chunkType === 'cookie_echo') { 1452 | haveCookieEcho = true 1453 | } 1454 | } 1455 | 1456 | if (!skip) { 1457 | bundledChunks.push(buffer) 1458 | bundledLength += buffer.length 1459 | callbacks.push(chunk.callback) 1460 | this.debugger.trace( 1461 | 'bundled chunk %s %d bytes, total %d', 1462 | chunk.chunkType, 1463 | buffer.length, 1464 | bundledLength 1465 | ) 1466 | } 1467 | 1468 | const finish = index === processedQueue.length - 1 1469 | const full = bundledLength + chunk.size > mtu 1470 | 1471 | if (finish || full) { 1472 | if (bundledChunks.length > 0) { 1473 | this.debugger.trace( 1474 | 'send bundled chunks %d bytes, %d chunks', 1475 | bundledLength, 1476 | bundledChunks.length 1477 | ) 1478 | if (emulateLoss) { 1479 | this.debugger.warn('emulated loss of packet with tsns %o', tsns) 1480 | } else { 1481 | // Todo select destination here? 1482 | this._sendPacket(bundledChunks, null, callbacks) 1483 | } 1484 | if (haveData) { 1485 | this._startT3() 1486 | } 1487 | bundledChunks = [] 1488 | callbacks = [] 1489 | tsns = [] 1490 | bundledLength = 36 // 20 + 16 1491 | haveCookieEcho = false 1492 | haveData = false 1493 | } 1494 | } 1495 | }) 1496 | this.queue = [] 1497 | } 1498 | 1499 | _shutdown(callback) { 1500 | // this._down() 1501 | this._sendChunk( 1502 | 'shutdown', 1503 | {c_tsn_ack: this.reassembly.peer_c_tsn.number}, 1504 | null, 1505 | () => { 1506 | /* 1507 | It shall then start the T2-shutdown timer and enter the SHUTDOWN-SENT 1508 | state. If the timer expires, the endpoint must resend the SHUTDOWN 1509 | with the updated last sequential TSN received from its peer. 1510 | The rules in Section 6.3 MUST be followed to determine the proper 1511 | timer value for T2-shutdown. 1512 | */ 1513 | // TODO: T2-shutdown timer 1514 | this.state = 'SHUTDOWN-SENT' 1515 | this.debugger.info('> sent SHUTDOWN') 1516 | if (typeof callback === 'function') { 1517 | callback() 1518 | } 1519 | } 1520 | ) 1521 | /* 1522 | The sender of the SHUTDOWN MAY also start an overall guard timer 1523 | 'T5-shutdown-guard' to bound the overall time for the shutdown 1524 | sequence. At the expiration of this timer, the sender SHOULD abort 1525 | the association by sending an ABORT chunk. If the 'T5-shutdown- 1526 | guard' timer is used, it SHOULD be set to the recommended value of 5 1527 | times 'RTO.Max'. 1528 | */ 1529 | this.T5 = setTimeout(() => { 1530 | this._abort() 1531 | }, this.rto_max * 5) 1532 | } 1533 | 1534 | _destroy() { 1535 | this.debugger.trace('destroy association') 1536 | this._down() 1537 | this.state = 'CLOSED' 1538 | clearTimeout(this.T1) 1539 | clearTimeout(this.T3) 1540 | clearTimeout(this.T5) 1541 | // TODO: better destroy assoc first, then endpoint 1542 | // todo delete association properly (dtls) when no addresses, only port 1543 | if (this.endpoint) { 1544 | for (const address in this.destinations) { 1545 | const key = address + ':' + this.remotePort 1546 | this.debugger.trace('destroy remote address %s', key) 1547 | delete this.endpoint.associations_lookup[key] 1548 | } 1549 | const index = this.endpoint.associations.indexOf(this) 1550 | this.endpoint.associations.splice(index, index + 1) 1551 | 1552 | delete this.endpoint 1553 | } 1554 | } 1555 | 1556 | SHUTDOWN(callback) { 1557 | /* 1558 | Format: SHUTDOWN(association id) 1559 | -> result 1560 | */ 1561 | 1562 | this.debugger.trace('API SHUTDOWN in state %s', this.state) 1563 | if (this.state !== 'ESTABLISHED') { 1564 | this.debugger.trace('just destroy association') 1565 | this._destroy() 1566 | return 1567 | } 1568 | this.state = 'SHUTDOWN-PENDING' 1569 | /* 1570 | Upon receipt of the SHUTDOWN primitive from its upper layer, the 1571 | endpoint enters the SHUTDOWN-PENDING state and remains there until 1572 | all outstanding data has been acknowledged by its peer. The endpoint 1573 | accepts no new data from its upper layer, but retransmits data to the 1574 | far end if necessary to fill gaps. 1575 | 1576 | Once all its outstanding data has been acknowledged, the endpoint 1577 | shall send a SHUTDOWN chunk to its peer including in the Cumulative 1578 | TSN Ack field the last sequential TSN it has received from the peer. 1579 | */ 1580 | this._shutdown(callback) 1581 | } 1582 | 1583 | ABORT(reason) { 1584 | /* 1585 | Format: ABORT(association id [, Upper Layer Abort Reason]) -> 1586 | result 1587 | */ 1588 | 1589 | this.debugger.trace('API ABORT') 1590 | this._down() 1591 | // If the association is aborted on request of the upper layer, 1592 | // a User-Initiated Abort error cause (see Section 3.3.10.12) 1593 | // SHOULD be present in the ABORT chunk. 1594 | const errorCause = {cause: 'USER_INITIATED_ABORT'} 1595 | if (reason) { 1596 | errorCause.abort_reason = reason 1597 | } 1598 | this._abort({error_causes: [errorCause]}) 1599 | } 1600 | 1601 | _abort(options, destination) { 1602 | /* 1603 | An abort of an association is abortive by definition in 1604 | that any data pending on either end of the association is discarded 1605 | and not delivered to the peer. A shutdown of an association is 1606 | considered a graceful close where all data in queue by either 1607 | endpoint is delivered to the respective peers. 1608 | 1609 | 9.1. Abort of an Association 1610 | 1611 | When an endpoint decides to abort an existing association, it MUST 1612 | send an ABORT chunk to its peer endpoint. The sender MUST fill in 1613 | the peer's Verification Tag in the outbound packet and MUST NOT 1614 | bundle any DATA chunk with the ABORT. If the association is aborted 1615 | on request of the upper layer, a User-Initiated Abort error cause 1616 | (see Section 3.3.10.12) SHOULD be present in the ABORT chunk. 1617 | 1618 | An endpoint MUST NOT respond to any received packet that contains an 1619 | ABORT chunk (also see Section 8.4). 1620 | 1621 | An endpoint receiving an ABORT MUST apply the special Verification 1622 | Tag check rules described in Section 8.5.1. 1623 | 1624 | After checking the Verification Tag, the receiving endpoint MUST 1625 | remove the association from its record and SHOULD report the 1626 | termination to its upper layer. If a User-Initiated Abort error 1627 | cause is present in the ABORT chunk, the Upper Layer Abort Reason 1628 | SHOULD be made available to the upper layer. 1629 | 1630 | */ 1631 | this._sendChunk('abort', options, destination, () => { 1632 | this.debugger.info('sent abort') 1633 | }) 1634 | this._destroy() 1635 | } 1636 | 1637 | SEND(buffer, options, callback) { 1638 | /* 1639 | Format: SEND(association id, buffer address, byte count [,context] 1640 | [,stream id] [,life time] [,destination transport address] 1641 | [,unordered flag] [,no-bundle flag] [,payload protocol-id] ) 1642 | -> result 1643 | */ 1644 | this.debugger.debug('SEND %d bytes, %o', buffer.length, options) 1645 | this.lastChunkSize = buffer.length 1646 | this.send(buffer, options, error => { 1647 | this.debugger.debug('SEND callback arrived') 1648 | if (error) { 1649 | this.debugger.warn('SEND error', error) 1650 | error = new Error(error) 1651 | } 1652 | const drain = this.drain(buffer.length) 1653 | if (drain || error) { 1654 | this.debugger.debug('drain is %s (flightsize %d cwnd %d peer_rwnd %d)', 1655 | drain, this.flightsize, this.cwnd, this.peer_rwnd) 1656 | if (typeof callback === 'function') { 1657 | callback(error) 1658 | callback = null 1659 | this.drain_callback = null 1660 | } 1661 | } else { 1662 | // if not drained yet, register drain callback for later use in onSack 1663 | this.drain_callback = callback 1664 | } 1665 | }) 1666 | return this.drain() 1667 | } 1668 | 1669 | drain() { 1670 | // Todo refine criterio 1671 | const drain = (this.flightsize < this.cwnd) && 1672 | (this.lastChunkSize + this.flightsize < this.peer_rwnd) 1673 | this.debugger.trace('check drain for chunk size %d =%s (flightsize %d cwnd %d peer_rwnd %d)', 1674 | this.lastChunkSize, drain, this.flightsize, this.cwnd, this.peer_rwnd) 1675 | return drain 1676 | } 1677 | 1678 | send(buffer, options, callback) { 1679 | // TODO: 6.1. Transmission of DATA Chunks 1680 | this.debugger.trace('send %d bytes, %o', buffer.length, options) 1681 | 1682 | const streamId = options.stream_id || 0 1683 | if (streamId < 0 || streamId > this.OS) { 1684 | this.debugger.warn('wrong stream id %d', streamId) 1685 | return callback('wrong stream id ' + streamId) 1686 | } 1687 | 1688 | if (this.state === 'SHUTDOWN-PENDING' || this.state === 'SHUTDOWN-RECEIVED') { 1689 | /* 1690 | Upon receipt of the SHUTDOWN primitive from its upper layer, 1691 | the endpoint enters the SHUTDOWN-PENDING state ... 1692 | accepts no new data from its upper layer 1693 | 1694 | Upon reception of the SHUTDOWN, 1695 | the peer endpoint shall enter the SHUTDOWN-RECEIVED state, 1696 | stop accepting new data from its SCTP user 1697 | */ 1698 | return callback('not accepting new data in SHUTDOWN state') 1699 | } 1700 | 1701 | /* 1702 | D) 1703 | When the time comes for the sender to transmit new DATA chunks, 1704 | the protocol parameter Max.Burst SHOULD be used to limit the 1705 | number of packets sent. The limit MAY be applied by adjusting cwnd as follows: 1706 | 1707 | if((flightsize + Max.Burst*MTU) < cwnd) cwnd = flightsize + 1708 | Max.Burst*MTU 1709 | 1710 | Or it MAY be applied by strictly limiting the number of packets 1711 | emitted by the output routine. 1712 | */ 1713 | if (this.flightsize + defs.NET_SCTP.max_burst * this.PMTU < this.cwnd) { 1714 | // TODO: compare to another adjustments 1715 | this.cwnd = this.flightsize + defs.NET_SCTP.max_burst * this.PMTU 1716 | this.debugger.trace('adjust cwnd to flightsize + Max.Burst*MTU = %d', this.cwnd) 1717 | } 1718 | 1719 | /* 1720 | E) Then, the sender can send out as many new DATA chunks as rule A 1721 | and rule B allow. 1722 | */ 1723 | 1724 | /* 1725 | A) At any given time, the data sender MUST NOT transmit new data to 1726 | any destination transport address if its peer's rwnd indicates 1727 | that the peer has no buffer space (i.e., rwnd is 0; see Section 1728 | 6.2.1). 1729 | */ 1730 | // todo zero window probe 1731 | // todo avoid silly window syndrome (SWS) 1732 | if (buffer.length >= this.peer_rwnd) { 1733 | return callback('peer has no buffer space (rwnd) for new packet ' + this.peer_rwnd) 1734 | } 1735 | 1736 | /* 1737 | B) At any given time, the sender MUST NOT transmit new data to a 1738 | given transport address if it has cwnd or more bytes of data 1739 | outstanding to that transport address. 1740 | */ 1741 | if (this.flightsize >= this.cwnd) { 1742 | return callback('flightsize >= cwnd ' + this.flightsize + ' ' + this.cwnd) 1743 | } 1744 | 1745 | /* 1746 | Before an endpoint transmits a DATA chunk, if any received DATA 1747 | chunks have not been acknowledged (e.g., due to delayed ack), the 1748 | sender should create a SACK and bundle it with the outbound DATA 1749 | chunk, as long as the size of the final SCTP packet does not exceed 1750 | the current MTU. See Section 6.2. 1751 | */ 1752 | if (this._sack_timer) { 1753 | this._sack() 1754 | } 1755 | 1756 | /* 1757 | C) When the time comes for the sender to transmit, before sending new 1758 | DATA chunks, the sender MUST first transmit any outstanding DATA 1759 | chunks that are marked for retransmission (limited by the current cwnd). 1760 | */ 1761 | this._retransmit() 1762 | 1763 | // Now send data 1764 | 1765 | let chunk 1766 | 1767 | if (this.ssn[streamId] === undefined) { 1768 | this.ssn[streamId] = 0 1769 | } 1770 | 1771 | const mtu = this.PMTU - 52 // 16 + 16 + 20 headers 1772 | if (buffer.length > mtu) { 1773 | let offset = 0 1774 | while (offset < buffer.length) { 1775 | chunk = { 1776 | flags: { 1777 | E: buffer.length - offset <= mtu, 1778 | B: offset === 0, 1779 | U: options.unordered, 1780 | I: 0 1781 | }, 1782 | stream_id: streamId, 1783 | ssn: this.ssn[streamId], 1784 | ppid: options.streamId, 1785 | user_data: buffer.slice(offset, offset + mtu) 1786 | } 1787 | offset += mtu 1788 | this._sendChunk('data', chunk, null, callback) 1789 | } 1790 | } else { 1791 | chunk = { 1792 | flags: { 1793 | E: 1, 1794 | B: 1, 1795 | U: options.unordered, 1796 | I: 0 1797 | }, 1798 | stream_id: streamId, 1799 | ssn: this.ssn[streamId], 1800 | ppid: options.streamId, 1801 | user_data: buffer 1802 | } 1803 | this._sendChunk('data', chunk, null, callback) 1804 | } 1805 | this.ssn[streamId]++ 1806 | if (this.ssn[streamId] > 0xFFFF) { 1807 | this.ssn[streamId] = 0 1808 | } 1809 | this.debugger.trace('%d bytes sent, cwnd %d', buffer.length, this.cwnd) 1810 | } 1811 | 1812 | SETPRIMARY() { 1813 | /* 1814 | Format: SETPRIMARY(association id, destination transport address, 1815 | [source transport address] ) 1816 | -> result 1817 | */ 1818 | } 1819 | 1820 | RECEIVE() { 1821 | /* 1822 | Format: RECEIVE(association id, buffer address, buffer size 1823 | [,stream id]) 1824 | -> byte count [,transport address] [,stream id] [,stream sequence 1825 | number] [,partial flag] [,delivery number] [,payload protocol-id] 1826 | */ 1827 | this.debugger.trace('API RECEIVE', this.readBuffer[0]) 1828 | return this.readBuffer.shift() 1829 | } 1830 | 1831 | STATUS() { 1832 | /* 1833 | Format: STATUS(association id) 1834 | -> status data 1835 | 1836 | association connection state, 1837 | destination transport address list, 1838 | destination transport address reachability states, 1839 | current receiver window size, 1840 | current congestion window sizes, 1841 | number of unacknowledged DATA chunks, 1842 | number of DATA chunks pending receipt, 1843 | primary path, 1844 | most recent SRTT on primary path, 1845 | RTO on primary path, 1846 | SRTT and RTO on other destination addresses, etc. 1847 | */ 1848 | } 1849 | 1850 | CHANGEHEARTBEAT() { 1851 | /* 1852 | Format: CHANGE HEARTBEAT(association id, 1853 | destination transport address, new state [,interval]) 1854 | -> result 1855 | */ 1856 | } 1857 | 1858 | REQUESTHEARTBEAT() { 1859 | /* 1860 | Format: REQUESTHEARTBEAT(association id, destination transport 1861 | address) 1862 | -> result 1863 | */ 1864 | } 1865 | 1866 | SETFAILURETHRESHOLD() { 1867 | /* 1868 | Format: SETFAILURETHRESHOLD(association id, destination transport 1869 | address, failure threshold) 1870 | 1871 | -> result 1872 | */ 1873 | } 1874 | 1875 | SETPROTOCOLPARAMETERS() { 1876 | /* 1877 | Format: SETPROTOCOLPARAMETERS(association id, 1878 | [,destination transport address,] 1879 | protocol parameter list) 1880 | -> result 1881 | */ 1882 | } 1883 | 1884 | RECEIVE_UNSENT() { 1885 | /* 1886 | Format: RECEIVE_UNSENT(data retrieval id, buffer address, buffer 1887 | size [,stream id] [, stream sequence number] [,partial 1888 | flag] [,payload protocol-id]) 1889 | 1890 | */ 1891 | } 1892 | 1893 | RECEIVE_UNACKED() { 1894 | /* 1895 | Format: RECEIVE_UNACKED(data retrieval id, buffer address, buffer 1896 | size, [,stream id] [, stream sequence number] [,partial 1897 | flag] [,payload protocol-id]) 1898 | 1899 | */ 1900 | } 1901 | 1902 | _updatePeer(chunk) { 1903 | this.OS = Math.min(this.OS, chunk.inbound_streams) 1904 | this.peer_tag = chunk.initiate_tag 1905 | this.peer_rwnd = chunk.a_rwnd 1906 | this.ssthresh = chunk.a_rwnd 1907 | 1908 | this.debugger.debug('update peer OS %s, peer_tag %s, peer_rwnd %s', 1909 | this.OS, this.peer_tag, this.peer_rwnd) 1910 | 1911 | this.reassembly.init({ 1912 | streams: this.MIS, 1913 | initial_tsn: chunk.initial_tsn 1914 | }) 1915 | 1916 | if (chunk.ipv4_address) { 1917 | chunk.ipv4_address.forEach(address => { 1918 | this.debugger.debug('peer ipv4_address %s', address) 1919 | if (!(address in this.destinations)) { 1920 | this.destinations[address] = this.default_address_data 1921 | const key = address + ':' + this.remotePort 1922 | this.endpoint.associations_lookup[key] = this 1923 | } 1924 | }) 1925 | } 1926 | } 1927 | } 1928 | 1929 | module.exports = Association 1930 | --------------------------------------------------------------------------------