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