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:
Joe Hildebrand 2019-02-21 15:25:17 -07:00 committed by silverwind
parent afa238119f
commit 4932026f66
4 changed files with 248 additions and 14 deletions

View File

@ -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
View File

@ -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,7 +784,22 @@ 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
View 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
}

68
test.js
View File

@ -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: 'CLIENT_SUBNET', // edns-client-subnet, see RFC 7871
code: 12, ip: 'fe80::',
data: Buffer.alloc(31) 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)