From f6db3d3d5f6accf0b129237185e9caa07fa78855 Mon Sep 17 00:00:00 2001 From: Joe Hildebrand Date: Wed, 4 Apr 2018 09:07:11 -0600 Subject: [PATCH] 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 --- index.js | 135 ++++++++++++++++++++++++++++++++++++++++++++++++------- test.js | 38 ++++++++++++++++ 2 files changed, 158 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 4a3d6dc..e0dac38 100644 --- a/index.js +++ b/index.js @@ -77,6 +77,7 @@ name.decode = function (buf, offset) { name.decode.bytes = 0 name.encodingLength = function (n) { + if (n === '.') return 1 return Buffer.byteLength(n) + 2 } @@ -664,6 +665,84 @@ raaaa.encodingLength = function () { 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) { switch (type.toUpperCase()) { case 'A': return ra @@ -679,6 +758,7 @@ const renc = exports.record = function (type) { case 'NS': return rns case 'SOA': return rsoa case 'MX': return rmx + case 'OPT': return ropt } return runknown } @@ -696,15 +776,29 @@ answer.encode = function (a, buf, offset) { buf.writeUInt16BE(types.toType(a.type), offset) - 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 - buf.writeUInt16BE(klass, offset + 2) + 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) - buf.writeUInt32BE(a.ttl || 0, offset + 4) + offset += 8 + ropt.encode(a.options || [], buf, offset) + offset += ropt.encode.bytes + } else { + 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 + buf.writeUInt16BE(klass, offset + 2) + buf.writeUInt32BE(a.ttl || 0, offset + 4) - const enc = renc(a.type) - enc.encode(a.data, buf, offset + 8) - offset += 8 + enc.encode.bytes + offset += 8 + const enc = renc(a.type) + enc.encode(a.data, buf, offset) + offset += enc.encode.bytes + } answer.encode.bytes = offset - oldOffset return buf @@ -721,15 +815,25 @@ answer.decode = function (buf, offset) { a.name = name.decode(buf, offset) offset += name.decode.bytes a.type = types.toString(buf.readUInt16BE(offset)) - const klass = buf.readUInt16BE(offset + 2) - a.ttl = buf.readUInt32BE(offset + 4) + 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) + a.ttl = buf.readUInt32BE(offset + 4) - a.class = classes.toString(klass & NOT_FLUSH_MASK) - a.flush = !!(klass & FLUSH_MASK) + a.class = classes.toString(klass & NOT_FLUSH_MASK) + a.flush = !!(klass & FLUSH_MASK) - const enc = renc(a.type) - a.data = enc.decode(buf, offset + 8) - offset += 8 + enc.decode.bytes + const enc = renc(a.type) + a.data = enc.decode(buf, offset + 8) + offset += 8 + enc.decode.bytes + } answer.decode.bytes = offset - oldOffset return a @@ -738,7 +842,7 @@ answer.decode = function (buf, offset) { answer.decode.bytes = 0 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 = {} @@ -798,6 +902,7 @@ exports.RECURSION_DESIRED = 1 << 8 exports.RECURSION_AVAILABLE = 1 << 7 exports.AUTHENTIC_DATA = 1 << 5 exports.CHECKING_DISABLED = 1 << 4 +exports.DNSSEC_OK = 1 << 15 exports.encode = function (result, buf, offset) { if (!buf) buf = Buffer.allocUnsafe(exports.encodingLength(result)) diff --git a/test.js b/test.js index eb208ed..31f2579 100644 --- a/test.js +++ b/test.js @@ -321,6 +321,44 @@ tape('stream', function (t) { 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) { const buf = Buffer.from([ 0x00, 0x79,