Add EDNS OPT de/encoding
* Add ECS processing. See #46 * Add support for Padding. Simplify ECS to not do CIDR parsing. * Done implementing the EDNS0 options that make sense * Default to string values from IANA for EDNSO option codes. Add tests for mappings. * Add a few more aliases for option codes * Update README for EDNS0 options * I think this is what is desired. Happy to change it more if you want. * Address code review comments from @silverwind
This commit is contained in:
parent
afa238119f
commit
4932026f66
17
README.md
17
README.md
@ -258,8 +258,25 @@ And an answer, additional, or authority looks like this
|
|||||||
udpPayloadSize: 4096,
|
udpPayloadSize: 4096,
|
||||||
flags: packet.DNSSEC_OK,
|
flags: packet.DNSSEC_OK,
|
||||||
options: [{
|
options: [{
|
||||||
|
// pass in any code/data for generic EDNS0 options
|
||||||
code: 12,
|
code: 12,
|
||||||
data: Buffer.alloc(31)
|
data: Buffer.alloc(31)
|
||||||
|
}, {
|
||||||
|
// Several EDNS0 options have explicit support
|
||||||
|
code: 'padding',
|
||||||
|
length: 31
|
||||||
|
}, {
|
||||||
|
code: 'CLIENT-SUBNET',
|
||||||
|
family: 2, // 1 for IPv4, 2 for IPv6
|
||||||
|
sourcePrefixLength: 64, // used to truncate IP address
|
||||||
|
scopePrefixLength: 0,
|
||||||
|
ip: 'fe80::'
|
||||||
|
}, {
|
||||||
|
code: 'tcp-keepalive',
|
||||||
|
timeout: 150 // increments of 100ms. This means 15s.
|
||||||
|
}, {
|
||||||
|
code: 'key-tag',
|
||||||
|
tags: [1, 2, 3]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
108
index.js
108
index.js
@ -4,6 +4,7 @@ const types = require('./types')
|
|||||||
const rcodes = require('./rcodes')
|
const rcodes = require('./rcodes')
|
||||||
const opcodes = require('./opcodes')
|
const opcodes = require('./opcodes')
|
||||||
const classes = require('./classes')
|
const classes = require('./classes')
|
||||||
|
const optioncodes = require('./optioncodes')
|
||||||
const ip = require('ip')
|
const ip = require('ip')
|
||||||
|
|
||||||
const QUERY_FLAG = 0
|
const QUERY_FLAG = 0
|
||||||
@ -671,12 +672,68 @@ roption.encode = function (option, buf, offset) {
|
|||||||
if (!offset) offset = 0
|
if (!offset) offset = 0
|
||||||
const oldOffset = offset
|
const oldOffset = offset
|
||||||
|
|
||||||
buf.writeUInt16BE(option.code, offset)
|
const code = optioncodes.toCode(option.code)
|
||||||
|
buf.writeUInt16BE(code, offset)
|
||||||
offset += 2
|
offset += 2
|
||||||
|
if (option.data) {
|
||||||
buf.writeUInt16BE(option.data.length, offset)
|
buf.writeUInt16BE(option.data.length, offset)
|
||||||
offset += 2
|
offset += 2
|
||||||
option.data.copy(buf, offset)
|
option.data.copy(buf, offset)
|
||||||
offset += option.data.length
|
offset += option.data.length
|
||||||
|
} else {
|
||||||
|
switch (code) {
|
||||||
|
// case 3: NSID. No encode makes sense.
|
||||||
|
// case 5,6,7: Not implementable
|
||||||
|
case 8: // ECS
|
||||||
|
// note: do IP math before calling
|
||||||
|
const spl = option.sourcePrefixLength || 0
|
||||||
|
const fam = option.family || (ip.isV4Format(option.ip) ? 1 : 2)
|
||||||
|
const ipBuf = ip.toBuffer(option.ip)
|
||||||
|
const ipLen = Math.ceil(spl / 8)
|
||||||
|
buf.writeUInt16BE(ipLen + 4, offset)
|
||||||
|
offset += 2
|
||||||
|
buf.writeUInt16BE(fam, offset)
|
||||||
|
offset += 2
|
||||||
|
buf.writeUInt8(spl, offset++)
|
||||||
|
buf.writeUInt8(option.scopePrefixLength || 0, offset++)
|
||||||
|
|
||||||
|
ipBuf.copy(buf, offset, 0, ipLen)
|
||||||
|
offset += ipLen
|
||||||
|
break
|
||||||
|
// case 9: EXPIRE (experimental)
|
||||||
|
// case 10: COOKIE. No encode makes sense.
|
||||||
|
case 11: // KEEP-ALIVE
|
||||||
|
if (option.timeout) {
|
||||||
|
buf.writeUInt16BE(2, offset)
|
||||||
|
offset += 2
|
||||||
|
buf.writeUInt16BE(option.timeout, offset)
|
||||||
|
offset += 2
|
||||||
|
} else {
|
||||||
|
buf.writeUInt16BE(0, offset)
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 12: // PADDING
|
||||||
|
const len = option.length || 0
|
||||||
|
buf.writeUInt16BE(len, offset)
|
||||||
|
offset += 2
|
||||||
|
buf.fill(0, offset, offset + len)
|
||||||
|
offset += len
|
||||||
|
break
|
||||||
|
// case 13: CHAIN. Experimental.
|
||||||
|
case 14: // KEY-TAG
|
||||||
|
const tagsLen = option.tags.length * 2
|
||||||
|
buf.writeUInt16BE(tagsLen, offset)
|
||||||
|
offset += 2
|
||||||
|
for (const tag of option.tags) {
|
||||||
|
buf.writeUInt16BE(tag, offset)
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown roption code: ${option.code}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
roption.encode.bytes = offset - oldOffset
|
roption.encode.bytes = offset - oldOffset
|
||||||
return buf
|
return buf
|
||||||
@ -687,10 +744,38 @@ roption.encode.bytes = 0
|
|||||||
roption.decode = function (buf, offset) {
|
roption.decode = function (buf, offset) {
|
||||||
if (!offset) offset = 0
|
if (!offset) offset = 0
|
||||||
const option = {}
|
const option = {}
|
||||||
|
|
||||||
option.code = buf.readUInt16BE(offset)
|
option.code = buf.readUInt16BE(offset)
|
||||||
const len = buf.readUInt16BE(offset + 2)
|
option.type = optioncodes.toString(option.code)
|
||||||
option.data = buf.slice(offset + 4, offset + 4 + len)
|
offset += 2
|
||||||
|
const len = buf.readUInt16BE(offset)
|
||||||
|
offset += 2
|
||||||
|
option.data = buf.slice(offset, offset + len)
|
||||||
|
switch (option.code) {
|
||||||
|
// case 3: NSID. No decode makes sense.
|
||||||
|
case 8: // ECS
|
||||||
|
option.family = buf.readUInt16BE(offset)
|
||||||
|
offset += 2
|
||||||
|
option.sourcePrefixLength = buf.readUInt8(offset++)
|
||||||
|
option.scopePrefixLength = buf.readUInt8(offset++)
|
||||||
|
const padded = Buffer.alloc((option.family === 1) ? 4 : 16)
|
||||||
|
buf.copy(padded, 0, offset, offset + len - 4)
|
||||||
|
option.ip = ip.toString(padded)
|
||||||
|
break
|
||||||
|
// case 12: Padding. No decode makes sense.
|
||||||
|
case 11: // KEEP-ALIVE
|
||||||
|
if (len > 0) {
|
||||||
|
option.timeout = buf.readUInt16BE(offset)
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 14:
|
||||||
|
option.tags = []
|
||||||
|
for (let i = 0; i < len; i += 2) {
|
||||||
|
option.tags.push(buf.readUInt16BE(offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
// don't worry about default. caller will use data if desired
|
||||||
|
}
|
||||||
|
|
||||||
roption.decode.bytes = len + 4
|
roption.decode.bytes = len + 4
|
||||||
return option
|
return option
|
||||||
@ -699,8 +784,23 @@ roption.decode = function (buf, offset) {
|
|||||||
roption.decode.bytes = 0
|
roption.decode.bytes = 0
|
||||||
|
|
||||||
roption.encodingLength = function (option) {
|
roption.encodingLength = function (option) {
|
||||||
|
if (option.data) {
|
||||||
return option.data.length + 4
|
return option.data.length + 4
|
||||||
}
|
}
|
||||||
|
const code = optioncodes.toCode(option.code)
|
||||||
|
switch (code) {
|
||||||
|
case 8: // ECS
|
||||||
|
const spl = option.sourcePrefixLength || 0
|
||||||
|
return Math.ceil(spl / 8) + 8
|
||||||
|
case 11: // KEEP-ALIVE
|
||||||
|
return (typeof option.timeout === 'number') ? 6 : 4
|
||||||
|
case 12: // PADDING
|
||||||
|
return option.length + 4
|
||||||
|
case 14: // KEY-TAG
|
||||||
|
return 4 + (option.tags.length * 2)
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown roption code: ${option.code}`)
|
||||||
|
}
|
||||||
|
|
||||||
const ropt = exports.opt = {}
|
const ropt = exports.opt = {}
|
||||||
|
|
||||||
|
|||||||
59
optioncodes.js
Normal file
59
optioncodes.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
exports.toString = function (type) {
|
||||||
|
switch (type) {
|
||||||
|
// list at
|
||||||
|
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-11
|
||||||
|
case 1: return 'LLQ'
|
||||||
|
case 2: return 'UL'
|
||||||
|
case 3: return 'NSID'
|
||||||
|
case 5: return 'DAU'
|
||||||
|
case 6: return 'DHU'
|
||||||
|
case 7: return 'N3U'
|
||||||
|
case 8: return 'CLIENT_SUBNET'
|
||||||
|
case 9: return 'EXPIRE'
|
||||||
|
case 10: return 'COOKIE'
|
||||||
|
case 11: return 'TCP_KEEPALIVE'
|
||||||
|
case 12: return 'PADDING'
|
||||||
|
case 13: return 'CHAIN'
|
||||||
|
case 14: return 'KEY_TAG'
|
||||||
|
case 26946: return 'DEVICEID'
|
||||||
|
}
|
||||||
|
if (type < 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return `OPTION_${type}`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.toCode = function (name) {
|
||||||
|
if (typeof name === 'number') {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
switch (name.toUpperCase()) {
|
||||||
|
case 'OPTION_0': return 0
|
||||||
|
case 'LLQ': return 1
|
||||||
|
case 'UL': return 2
|
||||||
|
case 'NSID': return 3
|
||||||
|
case 'OPTION_4': return 4
|
||||||
|
case 'DAU': return 5
|
||||||
|
case 'DHU': return 6
|
||||||
|
case 'N3U': return 7
|
||||||
|
case 'CLIENT_SUBNET': return 8
|
||||||
|
case 'EXPIRE': return 9
|
||||||
|
case 'COOKIE': return 10
|
||||||
|
case 'TCP_KEEPALIVE': return 11
|
||||||
|
case 'PADDING': return 12
|
||||||
|
case 'CHAIN': return 13
|
||||||
|
case 'KEY_TAG': return 14
|
||||||
|
case 'DEVICEID': return 26946
|
||||||
|
case 'OPTION_65535': return 65535
|
||||||
|
}
|
||||||
|
const m = name.match(/_(\d+)$/)
|
||||||
|
if (m) {
|
||||||
|
return parseInt(m[1], 10)
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
66
test.js
66
test.js
@ -4,6 +4,7 @@ const tape = require('tape')
|
|||||||
const packet = require('./')
|
const packet = require('./')
|
||||||
const rcodes = require('./rcodes')
|
const rcodes = require('./rcodes')
|
||||||
const opcodes = require('./opcodes')
|
const opcodes = require('./opcodes')
|
||||||
|
const optioncodes = require('./optioncodes')
|
||||||
|
|
||||||
tape('unknown', function (t) {
|
tape('unknown', function (t) {
|
||||||
testEncoder(t, packet.unknown, Buffer.from('hello world'))
|
testEncoder(t, packet.unknown, Buffer.from('hello world'))
|
||||||
@ -352,10 +353,26 @@ tape('opt', function (t) {
|
|||||||
t.ok(compare(t, 0, additional2.flags), 'flags match')
|
t.ok(compare(t, 0, additional2.flags), 'flags match')
|
||||||
additional1.flags = packet.DNSSEC_OK
|
additional1.flags = packet.DNSSEC_OK
|
||||||
additional1.extendedRcode = 0x80
|
additional1.extendedRcode = 0x80
|
||||||
// padding, see RFC 7830
|
|
||||||
additional1.options = [ {
|
additional1.options = [ {
|
||||||
code: 12,
|
code: 'CLIENT_SUBNET', // edns-client-subnet, see RFC 7871
|
||||||
data: Buffer.alloc(31)
|
ip: 'fe80::',
|
||||||
|
sourcePrefixLength: 64
|
||||||
|
}, {
|
||||||
|
code: 8, // still ECS
|
||||||
|
ip: '5.6.0.0',
|
||||||
|
sourcePrefixLength: 16,
|
||||||
|
scopePrefixLength: 16
|
||||||
|
}, {
|
||||||
|
code: 'padding',
|
||||||
|
length: 31
|
||||||
|
}, {
|
||||||
|
code: 'TCP_KEEPALIVE'
|
||||||
|
}, {
|
||||||
|
code: 'tcp_keepalive',
|
||||||
|
timeout: 150
|
||||||
|
}, {
|
||||||
|
code: 'KEY_TAG',
|
||||||
|
tags: [1, 82, 987]
|
||||||
}]
|
}]
|
||||||
buf = packet.encode(val)
|
buf = packet.encode(val)
|
||||||
val2 = packet.decode(buf)
|
val2 = packet.decode(buf)
|
||||||
@ -363,7 +380,16 @@ tape('opt', function (t) {
|
|||||||
t.ok(compare(t, 1 << 15, additional2.flags), 'DO bit set in flags')
|
t.ok(compare(t, 1 << 15, additional2.flags), 'DO bit set in flags')
|
||||||
t.ok(compare(t, true, additional2.flag_do), 'DO bit set')
|
t.ok(compare(t, true, additional2.flag_do), 'DO bit set')
|
||||||
t.ok(compare(t, additional1.extendedRcode, additional2.extendedRcode), 'extended rcode matches')
|
t.ok(compare(t, additional1.extendedRcode, additional2.extendedRcode), 'extended rcode matches')
|
||||||
t.ok(compare(t, additional1.options, additional2.options), 'options match')
|
t.ok(compare(t, 8, additional2.options[0].code))
|
||||||
|
t.ok(compare(t, 'fe80::', additional2.options[0].ip))
|
||||||
|
t.ok(compare(t, 64, additional2.options[0].sourcePrefixLength))
|
||||||
|
t.ok(compare(t, '5.6.0.0', additional2.options[1].ip))
|
||||||
|
t.ok(compare(t, 16, additional2.options[1].sourcePrefixLength))
|
||||||
|
t.ok(compare(t, 16, additional2.options[1].scopePrefixLength))
|
||||||
|
t.ok(compare(t, additional1.options[2].length, additional2.options[2].data.length))
|
||||||
|
t.ok(compare(t, additional1.options[3].timeout, undefined))
|
||||||
|
t.ok(compare(t, additional1.options[4].timeout, additional2.options[4].timeout))
|
||||||
|
t.ok(compare(t, additional1.options[5].tags, additional2.options[5].tags))
|
||||||
t.end()
|
t.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -509,6 +535,38 @@ tape('unpack', function (t) {
|
|||||||
t.end()
|
t.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tape('optioncodes', function (t) {
|
||||||
|
const opts = [
|
||||||
|
[0, 'OPTION_0'],
|
||||||
|
[1, 'LLQ'],
|
||||||
|
[2, 'UL'],
|
||||||
|
[3, 'NSID'],
|
||||||
|
[4, 'OPTION_4'],
|
||||||
|
[5, 'DAU'],
|
||||||
|
[6, 'DHU'],
|
||||||
|
[7, 'N3U'],
|
||||||
|
[8, 'CLIENT_SUBNET'],
|
||||||
|
[9, 'EXPIRE'],
|
||||||
|
[10, 'COOKIE'],
|
||||||
|
[11, 'TCP_KEEPALIVE'],
|
||||||
|
[12, 'PADDING'],
|
||||||
|
[13, 'CHAIN'],
|
||||||
|
[14, 'KEY_TAG'],
|
||||||
|
[26946, 'DEVICEID'],
|
||||||
|
[65535, 'OPTION_65535'],
|
||||||
|
[64000, 'OPTION_64000'],
|
||||||
|
[65002, 'OPTION_65002'],
|
||||||
|
[-1, null]
|
||||||
|
]
|
||||||
|
for (const [code, str] of opts) {
|
||||||
|
const s = optioncodes.toString(code)
|
||||||
|
t.ok(compare(t, s, str), `${code} => ${str}`)
|
||||||
|
t.ok(compare(t, optioncodes.toCode(s), code), `${str} => ${code}`)
|
||||||
|
}
|
||||||
|
t.ok(compare(t, optioncodes.toCode('INVALIDINVALID'), -1))
|
||||||
|
t.end()
|
||||||
|
})
|
||||||
|
|
||||||
function testEncoder (t, rpacket, val) {
|
function testEncoder (t, rpacket, val) {
|
||||||
const buf = rpacket.encode(val)
|
const buf = rpacket.encode(val)
|
||||||
const val2 = rpacket.decode(buf)
|
const val2 = rpacket.decode(buf)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user