sctp
Version:
SCTP network protocol (RFC4960) in plain Javascript
855 lines (824 loc) • 19 kB
JavaScript
/*
https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml
*/
const debug = require('debug')('sctp:defs')
const ip = require('ip')
const NET_SCTP = {
G: 50, // Granularity
RWND: 1024 * 100,
rto_initial: 3000,
rto_min: 1000,
rto_max: 60000,
rto_alpha_exp_divisor: 3,
rto_beta_exp_divisor: 2,
valid_cookie_life: 60000,
max_burst: 4,
association_max_retrans: 10, // TODO
cookie_hmac_alg: 'sha1',
max_init_retransmits: 8,
hb_interval: 30000,
sack_timeout: 180,
sack_freq: 2,
PMTU: 1500
}
const CAUSE_CODES = {
INVALID_STREAM_IDENTIFIER: 0x0001,
MISSING_MANDATORY_PARAMETER: 0x0002,
STALE_COOKIE_ERROR: 0x0003,
OUT_OF_RESOURCE: 0x0004,
UNRESOLVABLE_ADDRESS: 0x0005,
UNRECONGNIZED_CHUNK_TYPE: 0x0006,
INVALID_MANDATORY_PARAMETER: 0x0007,
UNRECONGNIZED_PARAMETERS: 0x0008,
NO_USER_DATA: 0x0009,
COOKIE_RECEIVED_WHILE_SHUTTING_DOWN: 0x000A,
RESTART_WITH_NEW_ADDRESSES: 0x000B,
USER_INITIATED_ABORT: 0x000C,
PROTOCOL_VIOLATION: 0x000D,
UNSUPPORTED_HMAC_IDENTIFIER: 0x0105
}
revert(CAUSE_CODES)
/*
TODO
sysctl -a | grep sctp
net.sctp.addip_enable = 0
net.sctp.addip_noauth_enable = 0
net.sctp.addr_scope_policy = 1
net.sctp.association_max_retrans = 10
net.sctp.auth_enable = 0
net.sctp.cookie_hmac_alg = sha1
net.sctp.cookie_preserve_enable = 1
net.sctp.default_auto_asconf = 0
net.sctp.hb_interval = 30000
net.sctp.max_autoclose = 2147483
net.sctp.max_burst = 4
net.sctp.max_init_retransmits = 8
net.sctp.path_max_retrans = 5
net.sctp.pf_retrans = 0
net.sctp.prsctp_enable = 1
net.sctp.rcvbuf_policy = 0
net.sctp.rto_alpha_exp_divisor = 3
net.sctp.rto_beta_exp_divisor = 2
net.sctp.rto_initial = 3000
net.sctp.rto_max = 60000
net.sctp.rto_min = 1000
net.sctp.rwnd_update_shift = 4
net.sctp.sack_timeout = 200
net.sctp.sctp_mem = 42486 56648 84972
net.sctp.sctp_rmem = 4096 865500 1812736
net.sctp.sctp_wmem = 4096 16384 1812736
net.sctp.sndbuf_policy = 0
net.sctp.valid_cookie_life = 60000
*/
function revert (hash, key1, key2) {
for (const key in hash) {
const value = hash[key]
if (key1 && key2) {
hash[value[key1]] = value
value[key2] = key
} else {
hash[value] = key
}
}
}
const types = {
int8: {
read (buffer, offset) {
return buffer.readUInt8(offset)
},
write (value, buffer, offset) {
value = value || 0
buffer.writeUInt8(value, offset)
},
size () {
return 1
},
default: 0
},
int16: {
read (buffer, offset) {
return buffer.readUInt16BE(offset)
},
write (value, buffer, offset) {
value = value || 0
buffer.writeUInt16BE(value, offset)
},
size () {
return 2
},
default: 0
},
int32: {
read (buffer, offset) {
return buffer.readUInt32BE(offset)
},
write (value, buffer, offset) {
value = value || 0
buffer.writeUInt32BE(value, offset)
},
size () {
return 4
},
default: 0
},
buffer: {
read (buffer, offset, length) {
return buffer.slice(offset, offset + length)
// Return Buffer.from(buffer.slice(offset, offset + length))
},
write (value, buffer, offset) {
if (typeof value === 'string') {
value = Buffer.from(value, 'ascii')
}
value.copy(buffer, offset)
},
size (value) {
return value ? value.length : 0
},
default: Buffer.alloc(0)
},
empty: {
read () {
return true
},
write () {
},
size () {
return 0
},
default: false
},
string: {
read (buffer, offset, length) {
return buffer.slice(offset, offset + length).toString('ascii')
},
write (value, buffer, offset) {
Buffer.from(value, 'ascii').copy(buffer, offset)
},
size (value) {
return value ? value.length : 0
},
default: ''
}
}
const filters = {}
filters.data_flags = {
encode (value) {
let result
if (typeof value === 'object') {
result =
(value.E ? 0x01 : 0x00) |
(value.B ? 0x02 : 0x00) |
(value.U ? 0x04 : 0x00) |
(value.I ? 0x08 : 0x00)
} else {
result = value
}
return result
},
decode (value) {
const result = {
B: (value >> 1) & 0x01,
E: value & 0x01,
U: (value >> 2) & 0x01,
I: (value >> 3) & 0x01
}
return result
}
}
filters.reflect_flag = {
encode (value) {
let result
if (typeof value === 'object') {
result = value.T ? 0x01 : 0x00
} else {
result = value
}
return result
},
decode (value) {
const result = {
T: value & 0x01
}
return result
}
}
filters.ip = {
encode (value) {
if (Buffer.isBuffer(value)) {
return value
}
return ip.toBuffer(value)
},
decode (value) {
if (!Buffer.isBuffer(value)) {
return value
}
return ip.toString(value)
}
}
filters.sack_info = {
encode (value) {
let result = Buffer.alloc(0)
if (!value) {
return result
}
// If (typeof value === 'object') {
let offset = 0
if (Array.isArray(value.gap_blocks) && value.gap_blocks.length > 0) {
this.gap_blocks_number = value.gap_blocks.length
offset = 0
const gapBlocksBuffer = Buffer.alloc(value.gap_blocks.length * 4)
value.gap_blocks.forEach(gapBlock => {
if (offset <= gapBlocksBuffer.length - 4) {
debug('gapBlock.start %d, gapBlock.finish %d', gapBlock.start, gapBlock.finish)
gapBlocksBuffer.writeUInt16BE(gapBlock.start, offset)
gapBlocksBuffer.writeUInt16BE(gapBlock.finish, offset + 2)
offset += 4
} else {
// TODO tmp to catch bug if any
throw new Error('incorrect buffer length for gap blocks')
}
})
result = gapBlocksBuffer
}
if (Array.isArray(value.duplicate_tsn) && value.duplicate_tsn.length > 0) {
this.duplicate_tsn_number = value.duplicate_tsn.length
offset = 0
const duplicateTsnBuffer = Buffer.alloc(value.duplicate_tsn.length * 4)
value.duplicate_tsn.forEach(tsn => {
duplicateTsnBuffer.writeUInt32BE(tsn, offset)
offset += 4
})
result = Buffer.concat([result, duplicateTsnBuffer])
}
// }
return result
},
decode (buffer) {
const result = {
gap_blocks: [],
duplicate_tsn: []
}
let offset = 0
let gapBlock
for (let n = 1; n <= this.gap_blocks_number; n++) {
if (offset > buffer.length - 4) {
break
}
gapBlock = {
start: buffer.readUInt16BE(offset),
finish: buffer.readUInt16BE(offset + 2)
}
result.gap_blocks.push(gapBlock)
offset += 4
}
for (let x = 1; x <= this.duplicate_tsn_number; x++) {
if (offset > buffer.length - 4) {
break
}
result.duplicate_tsn.push(buffer.readUInt32BE(offset))
offset += 4
}
return result
}
}
filters.error_causes = {
encode (value) {
if (!Array.isArray(value) || value.length === 0) {
return Buffer.alloc(0)
}
const buffers = []
let header
let body
value.forEach(error => {
header = Buffer.alloc(4)
if (error.cause) {
error.cause_code = CAUSE_CODES[error.cause]
}
header.writeUInt16BE(error.cause_code, 0)
switch (error.cause_code) {
case CAUSE_CODES.INVALID_STREAM_IDENTIFIER:
body = Buffer.alloc(4)
body.writeUInt16BE(error.stream_id, 0)
break
case CAUSE_CODES.UNRECONGNIZED_CHUNK_TYPE:
body = Buffer.from(error.unrecognized_chunk)
break
case CAUSE_CODES.UNRECONGNIZED_PARAMETERS:
body = Buffer.from(error.unrecognized_parameters)
break
case CAUSE_CODES.PROTOCOL_VIOLATION:
body = Buffer.from(error.additional_information || '')
break
case CAUSE_CODES.USER_INITIATED_ABORT:
body = Buffer.from(error.abort_reason || '')
break
default:
body = Buffer.alloc(0)
}
header.writeUInt16BE(body.length + 4, 2)
buffers.push(Buffer.concat([header, body]))
})
return Buffer.concat(buffers)
},
decode (buffer) {
let offset = 0
const result = []
let errorLength
let body
while (offset + 4 <= buffer.length) {
const error = {}
error.cause_code = buffer.readUInt16BE(offset)
error.cause = CAUSE_CODES[error.cause_code]
errorLength = buffer.readUInt16BE(offset + 2)
if (errorLength > 4) {
body = buffer.slice(offset + 4, offset + 4 + errorLength)
switch (error.cause_code) {
case CAUSE_CODES.INVALID_STREAM_IDENTIFIER:
error.stream_id = body.readUInt16BE(0)
break
case CAUSE_CODES.MISSING_MANDATORY_PARAMETER:
// TODO:
break
case CAUSE_CODES.STALE_COOKIE_ERROR:
error.measure_of_staleness = body.readUInt32BE(0)
break
case CAUSE_CODES.OUT_OF_RESOURCE:
break
case CAUSE_CODES.UNRESOLVABLE_ADDRESS:
// https://sourceforge.net/p/lksctp/mailman/message/26542493/
error.hostname = body.slice(4, 4 + body.readUInt16BE(2)).toString()
break
case CAUSE_CODES.UNRECONGNIZED_CHUNK_TYPE:
error.unrecognized_chunk = body
break
case CAUSE_CODES.INVALID_MANDATORY_PARAMETER:
break
case CAUSE_CODES.UNRECONGNIZED_PARAMETERS:
// TODO: slice
error.unrecognized_parameters = body
break
case CAUSE_CODES.NO_USER_DATA:
error.tsn = body.readUInt32BE(0)
break
case CAUSE_CODES.COOKIE_RECEIVED_WHILE_SHUTTING_DOWN:
break
case CAUSE_CODES.RESTART_WITH_NEW_ADDRESSES:
// TODO:
break
case CAUSE_CODES.USER_INITIATED_ABORT:
error.abort_reason = body.toString()
break
case CAUSE_CODES.PROTOCOL_VIOLATION:
error.additional_information = body.toString()
break
default:
error.body = body
return
}
}
offset += errorLength
result.push(error)
}
return result
}
}
filters.reconf = {
encode: value => {
const buffer = Buffer.alloc(12)
buffer.writeUInt32BE(value.rsn, 0)
return buffer
},
decode: buffer => {
if (buffer.length < 4) {
return
}
const value = {}
value.rsn = buffer.readUInt32BE(0)
return value
}
}
filters.forward_tsn_stream = {
encode: value => {
value = value || {}
const buffer = Buffer.alloc(4)
buffer.writeUInt16BE(value.stream_id, 0)
buffer.writeUInt16BE(value.ssn, 0)
return buffer
},
decode: buffer => {
if (buffer.length < 4) {
return
}
const value = {}
value.stream_id = buffer.readUInt16BE(0)
value.ssn = buffer.readUInt16BE(2)
return value
}
}
filters.chunks = {
encode: value => {
if (!Array.isArray(value)) {
return
}
if (value.length > 260) {
return
}
const array = value
.filter(chunkType => typeof chunkType === 'string')
.map(chunkType => chunkdefs[chunkType].id)
return Buffer.from(array)
},
decode: buffer => {
return [...buffer]
.map(byte => chunkdefs[byte].chunkType)
}
}
filters.hmac_algo = {
encode: value => {
if (!Array.isArray(value)) {
return
}
const HMAC_ALGO = {
'SHA-1': 1,
'SHA-256': 3
}
const array = value
.filter(algo => typeof algo === 'string')
.map(algo => HMAC_ALGO[algo.toUpperCase()])
.filter(algo => algo)
const buffer = Buffer.alloc(array.length * 2)
array.forEach((number, index) => {
buffer.writeUInt16BE(number, index * 2)
})
return buffer
},
decode: buffer => {
const result = []
const HMAC_ALGO = [
undefined,
'SHA-1',
undefined,
'SHA-256'
]
for (let index = 0; index <= buffer.length - 2; index += 2) {
const algo = HMAC_ALGO[buffer.readUInt16BE(index)]
if (algo) {
result.push(algo)
}
}
return result
}
}
const tlvs = {
heartbeat_info: {
id: 0x0001,
type: types.buffer
},
ipv4_address: {
id: 0x0005,
type: types.buffer,
multiple: true,
filter: filters.ip
},
ipv6_address: {
id: 0x0006,
type: types.buffer,
multiple: true,
filter: filters.ip
},
state_cookie: {
id: 0x0007,
type: types.buffer
},
unrecognized_parameter: {
id: 0x0008,
type: types.buffer,
multiple: true
},
cookie_preservative: {
id: 0x0009,
type: types.int32
},
host_name_address: {
id: 0x000B,
type: types.string
},
supported_address_type: {
id: 0x000C,
type: types.int16
},
ssn_reset_outgoing: {
id: 13,
type: types.buffer,
filter: filters.reconf
},
ssn_reset_incoming: {
id: 14,
type: types.buffer,
filter: filters.reconf
},
ssn_tsn_reset: {
id: 15,
type: types.buffer,
filter: filters.reconf
},
re_config_response: {
id: 16,
type: types.buffer,
filter: filters.reconf
},
add_streams_outgoing: {
id: 17,
type: types.buffer,
filter: filters.reconf
},
add_streams_incoming: {
id: 18,
type: types.buffer,
filter: filters.reconf
},
ecn_supported: {
id: 0x8000, // 1000 0000 0000 0000 - '10' - skip and continue
type: types.empty
},
random: {
id: 0x8002, // 1000 0000 0000 0010
type: types.buffer
},
chunks: {
id: 0x8003, // 1000 0000 0000 0011
type: types.buffer,
filter: filters.chunks
},
hmac_algo: {
id: 0x8004, // 1000 0000 0000 0100
type: types.buffer,
filter: filters.hmac_algo
},
pad: {
id: 0x8005, // 1000 0000 0000 0101
type: types.buffer
},
supported_extensions: {
id: 0x8008, // 1000 0000 0000 1000
type: types.buffer
},
forward_tsn_supported: {
id: 0xC000, // 1100 0000 0000 0000 - '11' - skip and report 'Unrecognized Chunk Type'
type: types.empty
},
add_ip_address: {
id: 0xC001, // 1100 0000 0000 0001
type: types.buffer
},
delete_ip_address: {
id: 0xC002, // 1100 0000 0000 0010
type: types.buffer
},
error_cause_indication: {
id: 0xC003, // 1100 0000 0000 0011
type: types.buffer
},
set_primary_address: {
id: 0xC004, // 1100 0000 0000 0100
type: types.buffer
},
success_indication: {
id: 0xC005, // 1100 0000 0000 0101
type: types.buffer
},
adaptation_layer_indication: {
id: 0xC006, // 1100 0000 0000 0110
type: types.buffer
}
}
revert(tlvs, 'id', 'tag')
const PPID = {
SCTP: 0,
IUA: 1,
M2UA: 2,
M3UA: 3,
SUA: 4,
M2PA: 5,
V5UA: 6,
H248: 7,
BICC: 8,
TALI: 9,
DUA: 10,
ASAP: 11,
ENRP: 12,
H323: 13,
QIPC: 14,
SIMCO: 15,
DDP_CHUNK: 16,
DDP_CONTROL: 17,
S1AP: 18,
RUA: 19,
HNBAP: 20,
FORCES_HP: 21,
FORCES_MP: 22,
FORCES_LP: 23,
SBCAP: 24,
NBAP: 25,
X2AP: 27,
IRCP: 28,
LCSAP: 29,
MPICH2: 30,
SABP: 31,
FGP: 32,
PPP: 33,
CALCAPP: 34,
SSP: 35,
NPMP_CONTROL: 36,
NPMP_DATA: 37,
ECHO: 38,
DISCARD: 39,
DAYTIME: 40,
CHARGEN: 41,
RNA: 42,
M2AP: 43,
M3AP: 44,
SSH: 45,
DIAMETER: 46,
DIAMETER_DTLS: 47,
BER: 48,
WEBRTC_DCEP: 50,
WEBRTC_STRING: 51,
WEBRTC_BINARY: 53,
PUA: 55,
WEBRTC_STRING_EMPTY: 56,
WEBRTC_BINARY_EMPTY: 57,
XWAP: 58,
XWCP: 59,
NGAP: 60,
XNAP: 61
}
revert(PPID)
const chunkdefs = {
data: {
id: 0x00,
size: 16,
params: {
tsn: { type: types.int32, default: null },
stream_id: { type: types.int16 },
ssn: { type: types.int16 },
ppid: { type: types.int32 },
user_data: { type: types.buffer }
},
flags_filter: filters.data_flags
},
init: {
id: 0x01,
size: 20,
params: {
initiate_tag: { type: types.int32 },
a_rwnd: { type: types.int32 },
outbound_streams: { type: types.int16 },
inbound_streams: { type: types.int16 },
initial_tsn: { type: types.int32 }
}
},
init_ack: {
id: 0x02,
size: 20,
params: {
initiate_tag: { type: types.int32 },
a_rwnd: { type: types.int32 },
outbound_streams: { type: types.int16 },
inbound_streams: { type: types.int16 },
initial_tsn: { type: types.int32 }
}
},
sack: {
id: 0x03,
size: 16,
params: {
c_tsn_ack: { type: types.int32 },
a_rwnd: { type: types.int32 },
gap_blocks_number: { type: types.int16 },
duplicate_tsn_number: { type: types.int16 },
sack_info: { type: types.buffer, filter: filters.sack_info }
}
},
heartbeat: {
id: 0x04,
size: 4
},
heartbeat_ack: {
id: 0x05,
size: 4
},
abort: {
id: 0x06,
size: 4,
params: {
error_causes: { type: types.buffer, filter: filters.error_causes }
},
flags_filter: filters.reflect_flag
},
shutdown: {
id: 0x07,
size: 8,
params: {
c_tsn_ack: { type: types.int32 }
}
},
shutdown_ack: {
id: 0x08,
size: 4
},
error: {
id: 0x09,
size: 4, // Tolerate absence of causes?
params: {
error_causes: { type: types.buffer, filter: filters.error_causes }
}
},
cookie_echo: {
id: 0x0A,
size: 4,
params: {
cookie: { type: types.buffer }
}
},
cookie_ack: {
id: 0x0B,
size: 4
},
ecne: {
id: 0x0C
},
cwr: {
id: 0x0D
},
shutdown_complete: {
id: 0x0E,
flags_filter: filters.reflect_flag
},
auth: {
id: 0x0F,
params: {
shared_key_id: { type: types.int16 },
hmac_id: { type: types.int16 },
hmac: { type: types.buffer }
}
},
i_data: {
id: 0x40, // 64, 0100 0010
params: {
tsn: { type: types.int32, default: null },
stream_id: { type: types.int16 },
ssn: { type: types.int16 },
message_id: { type: types.int16 },
ppid: { type: types.int32 },
user_data: { type: types.buffer }
},
flags_filter: filters.data_flags
},
asconf_ack: {
id: 0x80, // 128, 1000 0000,
seq: { type: types.int32 }
},
re_config: {
id: 0x82 // 130, 1000 0010
},
pad: {
id: 0x84, // 132, 1000 0100
params: {
padding_data: { type: types.buffer }
}
},
forward_tsn: {
id: 0xC0, // 192, 1100 0000
params: {
new_c_tsn: { type: types.int32 },
streams: { type: types.buffer, multiple: true, filter: filters.forward_tsn_stream }
}
},
asconf: {
id: 0xC1, // 193, 1100 0001
params: {
seq: { type: types.int32 },
address: { type: types.buffer }
}
},
i_forward_tsn: {
id: 0xC2, // 194, 1100 0010
params: {
new_c_tsn: { type: types.int32 },
streams: { type: types.buffer, multiple: true }
}
}
}
revert(chunkdefs, 'id', 'chunkType')
module.exports = {
NET_SCTP,
filters,
chunkdefs,
types,
tlvs,
CAUSE_CODES,
PPID
}