Add basic EDNS(0) OPT RR support

* Add EDNS(0) OPT RR for UDP payload size in query.

* Rework PR #34, fixes #36.  Add support for options in an OPT pseudo-RR.

* Include tweaks from @silverwind
This commit is contained in:
Joe Hildebrand 2018-04-04 09:07:11 -06:00 committed by silverwind
parent 40d8e5657f
commit f6db3d3d5f
2 changed files with 158 additions and 15 deletions

113
index.js
View File

@ -77,6 +77,7 @@ name.decode = function (buf, offset) {
name.decode.bytes = 0 name.decode.bytes = 0
name.encodingLength = function (n) { name.encodingLength = function (n) {
if (n === '.') return 1
return Buffer.byteLength(n) + 2 return Buffer.byteLength(n) + 2
} }
@ -664,6 +665,84 @@ raaaa.encodingLength = function () {
return 18 return 18
} }
const roption = exports.option = {}
roption.encode = function (option, buf, offset) {
if (!buf) buf = Buffer.allocUnsafe(roption.encodingLength(option))
if (!offset) offset = 0
const oldOffset = offset
buf.writeUInt16BE(option.code, offset)
offset += 2
buf.writeUInt16BE(option.data.length, offset)
offset += 2
option.data.copy(buf, offset)
offset += option.data.length
roption.encode.bytes = offset - oldOffset
return buf
}
roption.encode.bytes = 0
roption.decode = function (buf, offset) {
if (!offset) offset = 0
const option = {}
option.code = buf.readUInt16BE(offset)
const len = buf.readUInt16BE(offset + 2)
option.data = buf.slice(offset + 4, offset + 4 + len)
roption.decode.bytes = len + 4
return option
}
roption.decode.bytes = 0
roption.encodingLength = function (option) {
return option.data.length + 4
}
const ropt = exports.opt = {}
ropt.encode = function (options, buf, offset) {
if (!buf) buf = Buffer.allocUnsafe(ropt.encodingLength(options))
if (!offset) offset = 0
const oldOffset = offset
const rdlen = encodingLengthList(options, roption)
buf.writeUInt16BE(rdlen, offset)
offset = encodeList(options, roption, buf, offset + 2)
ropt.encode.bytes = offset - oldOffset
return buf
}
ropt.encode.bytes = 0
ropt.decode = function (buf, offset) {
if (!offset) offset = 0
const oldOffset = offset
const options = []
let rdlen = buf.readUInt16BE(offset)
offset += 2
let o = 0
while (rdlen > 0) {
options[o++] = roption.decode(buf, offset)
offset += roption.decode.bytes
rdlen -= roption.decode.bytes
}
ropt.decode.bytes = offset - oldOffset
return options
}
ropt.decode.bytes = 0
ropt.encodingLength = function (options) {
return 2 + encodingLengthList(options || [], roption)
}
const renc = exports.record = function (type) { const renc = exports.record = function (type) {
switch (type.toUpperCase()) { switch (type.toUpperCase()) {
case 'A': return ra case 'A': return ra
@ -679,6 +758,7 @@ const renc = exports.record = function (type) {
case 'NS': return rns case 'NS': return rns
case 'SOA': return rsoa case 'SOA': return rsoa
case 'MX': return rmx case 'MX': return rmx
case 'OPT': return ropt
} }
return runknown return runknown
} }
@ -696,15 +776,29 @@ answer.encode = function (a, buf, offset) {
buf.writeUInt16BE(types.toType(a.type), offset) buf.writeUInt16BE(types.toType(a.type), offset)
if (a.type.toUpperCase() === 'OPT') {
if (a.name !== '.') {
throw new Error('OPT name must be root.')
}
buf.writeUInt16BE(a.updPayloadSize || 4096, offset + 2)
buf.writeUInt8(a.extendedRcode || 0, offset + 4)
buf.writeUInt8(a.ednsVersion || 0, offset + 5)
buf.writeUInt16BE(a.flags || 0, offset + 6)
offset += 8
ropt.encode(a.options || [], buf, offset)
offset += ropt.encode.bytes
} else {
let klass = classes.toClass(a.class === undefined ? 'IN' : a.class) let klass = classes.toClass(a.class === undefined ? 'IN' : a.class)
if (a.flush) klass |= FLUSH_MASK // the 1st bit of the class is the flush bit if (a.flush) klass |= FLUSH_MASK // the 1st bit of the class is the flush bit
buf.writeUInt16BE(klass, offset + 2) buf.writeUInt16BE(klass, offset + 2)
buf.writeUInt32BE(a.ttl || 0, offset + 4) buf.writeUInt32BE(a.ttl || 0, offset + 4)
offset += 8
const enc = renc(a.type) const enc = renc(a.type)
enc.encode(a.data, buf, offset + 8) enc.encode(a.data, buf, offset)
offset += 8 + enc.encode.bytes offset += enc.encode.bytes
}
answer.encode.bytes = offset - oldOffset answer.encode.bytes = offset - oldOffset
return buf return buf
@ -721,6 +815,15 @@ answer.decode = function (buf, offset) {
a.name = name.decode(buf, offset) a.name = name.decode(buf, offset)
offset += name.decode.bytes offset += name.decode.bytes
a.type = types.toString(buf.readUInt16BE(offset)) a.type = types.toString(buf.readUInt16BE(offset))
if (a.type === 'OPT') {
a.udpPayloadSize = buf.readUInt16BE(offset + 2)
a.extendedRcode = buf.readUInt8(offset + 4)
a.ednsVersion = buf.readUInt8(offset + 5)
a.flags = buf.readUInt16BE(offset + 6)
a.flag_do = ((a.flags >> 15) & 0x1) === 1
a.options = ropt.decode(buf, offset + 8)
offset += 8 + ropt.decode.bytes
} else {
const klass = buf.readUInt16BE(offset + 2) const klass = buf.readUInt16BE(offset + 2)
a.ttl = buf.readUInt32BE(offset + 4) a.ttl = buf.readUInt32BE(offset + 4)
@ -730,6 +833,7 @@ answer.decode = function (buf, offset) {
const enc = renc(a.type) const enc = renc(a.type)
a.data = enc.decode(buf, offset + 8) a.data = enc.decode(buf, offset + 8)
offset += 8 + enc.decode.bytes offset += 8 + enc.decode.bytes
}
answer.decode.bytes = offset - oldOffset answer.decode.bytes = offset - oldOffset
return a return a
@ -738,7 +842,7 @@ answer.decode = function (buf, offset) {
answer.decode.bytes = 0 answer.decode.bytes = 0
answer.encodingLength = function (a) { answer.encodingLength = function (a) {
return name.encodingLength(a.name) + 8 + renc(a.type).encodingLength(a.data) return name.encodingLength(a.name) + 8 + renc(a.type).encodingLength(a.data || a.options)
} }
const question = exports.question = {} const question = exports.question = {}
@ -798,6 +902,7 @@ exports.RECURSION_DESIRED = 1 << 8
exports.RECURSION_AVAILABLE = 1 << 7 exports.RECURSION_AVAILABLE = 1 << 7
exports.AUTHENTIC_DATA = 1 << 5 exports.AUTHENTIC_DATA = 1 << 5
exports.CHECKING_DISABLED = 1 << 4 exports.CHECKING_DISABLED = 1 << 4
exports.DNSSEC_OK = 1 << 15
exports.encode = function (result, buf, offset) { exports.encode = function (result, buf, offset) {
if (!buf) buf = Buffer.allocUnsafe(exports.encodingLength(result)) if (!buf) buf = Buffer.allocUnsafe(exports.encodingLength(result))

38
test.js
View File

@ -321,6 +321,44 @@ tape('stream', function (t) {
t.end() t.end()
}) })
tape('opt', function (t) {
const val = {
type: 'query',
questions: [{
type: 'A',
name: 'hello.a.com'
}],
additionals: [{
type: 'OPT',
name: '.',
udpPayloadSize: 4096
}]
}
testEncoder(t, packet, val)
let buf = packet.encode(val)
let val2 = packet.decode(buf)
const additional1 = val.additionals[0]
let additional2 = val2.additionals[0]
t.ok(compare(t, additional1.name, additional2.name), 'name matches')
t.ok(compare(t, additional1.udpPayloadSize, additional2.udpPayloadSize), 'udp payload size matches')
t.ok(compare(t, 0, additional2.flags), 'flags match')
additional1.flags = packet.DNSSEC_OK
additional1.extendedRcode = 0x80
// padding, see RFC 7830
additional1.options = [{
code: 12,
data: Buffer.alloc(31)
}]
buf = packet.encode(val)
val2 = packet.decode(buf)
additional2 = val2.additionals[0]
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, additional1.extendedRcode, additional2.extendedRcode), 'extended rcode matches')
t.ok(compare(t, additional1.options, additional2.options), 'options match')
t.end()
})
tape('unpack', function (t) { tape('unpack', function (t) {
const buf = Buffer.from([ const buf = Buffer.from([
0x00, 0x79, 0x00, 0x79,