|
| 1 | +/* eslint-disable required-modules */ |
| 2 | +'use strict'; |
| 3 | + |
| 4 | +// Naïve DNS parser/serializer. |
| 5 | + |
| 6 | +const assert = require('assert'); |
| 7 | +const os = require('os'); |
| 8 | + |
| 9 | +const types = { |
| 10 | + A: 1, |
| 11 | + AAAA: 28, |
| 12 | + NS: 2, |
| 13 | + CNAME: 5, |
| 14 | + SOA: 6, |
| 15 | + PTR: 12, |
| 16 | + MX: 15, |
| 17 | + TXT: 16, |
| 18 | + ANY: 255 |
| 19 | +}; |
| 20 | + |
| 21 | +const classes = { |
| 22 | + IN: 1 |
| 23 | +}; |
| 24 | + |
| 25 | +function readDomainFromPacket(buffer, offset) { |
| 26 | + assert.ok(offset < buffer.length); |
| 27 | + const length = buffer[offset]; |
| 28 | + if (length === 0) { |
| 29 | + return { nread: 1, domain: '' }; |
| 30 | + } else if ((length & 0xC0) === 0) { |
| 31 | + offset += 1; |
| 32 | + const chunk = buffer.toString('ascii', offset, offset + length); |
| 33 | + // Read the rest of the domain. |
| 34 | + const { nread, domain } = readDomainFromPacket(buffer, offset + length); |
| 35 | + return { |
| 36 | + nread: 1 + length + nread, |
| 37 | + domain: domain ? `${chunk}.${domain}` : chunk |
| 38 | + }; |
| 39 | + } else { |
| 40 | + // Pointer to another part of the packet. |
| 41 | + assert.strictEqual(length & 0xC0, 0xC0); |
| 42 | + // eslint-disable-next-line |
| 43 | + const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000; |
| 44 | + return { |
| 45 | + nread: 2, |
| 46 | + domain: readDomainFromPacket(buffer, pointeeOffset) |
| 47 | + }; |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +function parseDNSPacket(buffer) { |
| 52 | + assert.ok(buffer.length > 12); |
| 53 | + |
| 54 | + const parsed = { |
| 55 | + id: buffer.readUInt16BE(0), |
| 56 | + flags: buffer.readUInt16BE(2), |
| 57 | + }; |
| 58 | + |
| 59 | + const counts = [ |
| 60 | + ['questions', buffer.readUInt16BE(4)], |
| 61 | + ['answers', buffer.readUInt16BE(6)], |
| 62 | + ['authorityAnswers', buffer.readUInt16BE(8)], |
| 63 | + ['additionalRecords', buffer.readUInt16BE(10)] |
| 64 | + ]; |
| 65 | + |
| 66 | + let offset = 12; |
| 67 | + for (const [ sectionName, count ] of counts) { |
| 68 | + parsed[sectionName] = []; |
| 69 | + for (let i = 0; i < count; ++i) { |
| 70 | + const { nread, domain } = readDomainFromPacket(buffer, offset); |
| 71 | + offset += nread; |
| 72 | + |
| 73 | + const type = buffer.readUInt16BE(offset); |
| 74 | + |
| 75 | + const rr = { |
| 76 | + domain, |
| 77 | + cls: buffer.readUInt16BE(offset + 2), |
| 78 | + }; |
| 79 | + offset += 4; |
| 80 | + |
| 81 | + for (const name in types) { |
| 82 | + if (types[name] === type) |
| 83 | + rr.type = name; |
| 84 | + } |
| 85 | + |
| 86 | + if (sectionName !== 'questions') { |
| 87 | + rr.ttl = buffer.readInt32BE(offset); |
| 88 | + const dataLength = buffer.readUInt16BE(offset); |
| 89 | + offset += 6; |
| 90 | + |
| 91 | + switch (type) { |
| 92 | + case types.A: |
| 93 | + assert.strictEqual(dataLength, 4); |
| 94 | + rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` + |
| 95 | + `${buffer[offset + 2]}.${buffer[offset + 3]}`; |
| 96 | + break; |
| 97 | + case types.AAAA: |
| 98 | + assert.strictEqual(dataLength, 16); |
| 99 | + rr.address = buffer.toString('hex', offset, offset + 16) |
| 100 | + .replace(/(.{4}(?!$))/g, '$1:'); |
| 101 | + break; |
| 102 | + case types.TXT: |
| 103 | + { |
| 104 | + let position = offset; |
| 105 | + rr.entries = []; |
| 106 | + while (position < offset + dataLength) { |
| 107 | + const txtLength = buffer[offset]; |
| 108 | + rr.entries.push(buffer.toString('utf8', |
| 109 | + position + 1, |
| 110 | + position + 1 + txtLength)); |
| 111 | + position += 1 + txtLength; |
| 112 | + } |
| 113 | + assert.strictEqual(position, offset + dataLength); |
| 114 | + break; |
| 115 | + } |
| 116 | + case types.MX: |
| 117 | + { |
| 118 | + rr.priority = buffer.readInt16BE(buffer, offset); |
| 119 | + offset += 2; |
| 120 | + const { nread, domain } = readDomainFromPacket(buffer, offset); |
| 121 | + rr.exchange = domain; |
| 122 | + assert.strictEqual(nread, dataLength); |
| 123 | + break; |
| 124 | + } |
| 125 | + case types.NS: |
| 126 | + case types.CNAME: |
| 127 | + case types.PTR: |
| 128 | + { |
| 129 | + const { nread, domain } = readDomainFromPacket(buffer, offset); |
| 130 | + rr.value = domain; |
| 131 | + assert.strictEqual(nread, dataLength); |
| 132 | + break; |
| 133 | + } |
| 134 | + case types.SOA: |
| 135 | + { |
| 136 | + const mname = readDomainFromPacket(buffer, offset); |
| 137 | + const rname = readDomainFromPacket(buffer, offset + mname.nread); |
| 138 | + rr.nsname = mname.domain; |
| 139 | + rr.hostmaster = rname.domain; |
| 140 | + const trailerOffset = offset + mname.nread + rname.nread; |
| 141 | + rr.serial = buffer.readUInt32BE(trailerOffset); |
| 142 | + rr.refresh = buffer.readUInt32BE(trailerOffset + 4); |
| 143 | + rr.retry = buffer.readUInt32BE(trailerOffset + 8); |
| 144 | + rr.expire = buffer.readUInt32BE(trailerOffset + 12); |
| 145 | + rr.minttl = buffer.readUInt32BE(trailerOffset + 16); |
| 146 | + |
| 147 | + assert.strictEqual(trailerOffset + 20, dataLength); |
| 148 | + break; |
| 149 | + } |
| 150 | + default: |
| 151 | + throw new Error(`Unknown RR type ${rr.type}`); |
| 152 | + } |
| 153 | + offset += dataLength; |
| 154 | + } |
| 155 | + |
| 156 | + parsed[sectionName].push(rr); |
| 157 | + |
| 158 | + assert.ok(offset <= buffer.length); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + assert.strictEqual(offset, buffer.length); |
| 163 | + return parsed; |
| 164 | +} |
| 165 | + |
| 166 | +function writeIPv6(ip) { |
| 167 | + const parts = ip.replace(/^:|:$/g, '').split(':'); |
| 168 | + const buf = Buffer.alloc(16); |
| 169 | + |
| 170 | + let offset = 0; |
| 171 | + for (const part of parts) { |
| 172 | + if (part === '') { |
| 173 | + offset += 16 - 2 * (parts.length - 1); |
| 174 | + } else { |
| 175 | + buf.writeUInt16BE(parseInt(part, 16), offset); |
| 176 | + offset += 2; |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + return buf; |
| 181 | +} |
| 182 | + |
| 183 | +function writeDomainName(domain) { |
| 184 | + return Buffer.concat(domain.split('.').map((label) => { |
| 185 | + assert(label.length < 64); |
| 186 | + return Buffer.concat([ |
| 187 | + Buffer.from([label.length]), |
| 188 | + Buffer.from(label, 'ascii') |
| 189 | + ]); |
| 190 | + }).concat([Buffer.alloc(1)])); |
| 191 | +} |
| 192 | + |
| 193 | +function writeDNSPacket(parsed) { |
| 194 | + const buffers = []; |
| 195 | + const kStandardResponseFlags = 0x8180; |
| 196 | + |
| 197 | + buffers.push(new Uint16Array([ |
| 198 | + parsed.id, |
| 199 | + parsed.flags === undefined ? kStandardResponseFlags : parsed.flags, |
| 200 | + parsed.questions && parsed.questions.length, |
| 201 | + parsed.answers && parsed.answers.length, |
| 202 | + parsed.authorityAnswers && parsed.authorityAnswers.length, |
| 203 | + parsed.additionalRecords && parsed.additionalRecords.length, |
| 204 | + ])); |
| 205 | + |
| 206 | + for (const q of parsed.questions) { |
| 207 | + assert(types[q.type]); |
| 208 | + buffers.push(writeDomainName(q.domain)); |
| 209 | + buffers.push(new Uint16Array([ |
| 210 | + types[q.type], |
| 211 | + q.cls === undefined ? classes.IN : q.cls |
| 212 | + ])); |
| 213 | + } |
| 214 | + |
| 215 | + for (const rr of [].concat(parsed.answers, |
| 216 | + parsed.authorityAnswers, |
| 217 | + parsed.additionalRecords)) { |
| 218 | + if (!rr) continue; |
| 219 | + |
| 220 | + assert(types[rr.type]); |
| 221 | + buffers.push(writeDomainName(rr.domain)); |
| 222 | + buffers.push(new Uint16Array([ |
| 223 | + types[rr.type], |
| 224 | + rr.cls === undefined ? classes.IN : rr.cls |
| 225 | + ])); |
| 226 | + buffers.push(new Int32Array([rr.ttl])); |
| 227 | + |
| 228 | + const rdLengthBuf = new Uint16Array(1); |
| 229 | + buffers.push(rdLengthBuf); |
| 230 | + |
| 231 | + switch (rr.type) { |
| 232 | + case 'A': |
| 233 | + rdLengthBuf[0] = 4; |
| 234 | + buffers.push(new Uint8Array(rr.address.split('.'))); |
| 235 | + break; |
| 236 | + case 'AAAA': |
| 237 | + rdLengthBuf[0] = 16; |
| 238 | + buffers.push(writeIPv6(rr.address)); |
| 239 | + break; |
| 240 | + case 'TXT': |
| 241 | + const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b); |
| 242 | + // Total length of all strings + 1 byte each for their lengths. |
| 243 | + rdLengthBuf[0] = rr.entries.length + total; |
| 244 | + for (const txt of rr.entries) { |
| 245 | + buffers.push(new Uint8Array([Buffer.byteLength(txt)])); |
| 246 | + buffers.push(Buffer.from(txt)); |
| 247 | + } |
| 248 | + break; |
| 249 | + case 'MX': |
| 250 | + rdLengthBuf[0] = 2; |
| 251 | + buffers.push(new Uint16Array([rr.priority])); |
| 252 | + // fall through |
| 253 | + case 'NS': |
| 254 | + case 'CNAME': |
| 255 | + case 'PTR': |
| 256 | + { |
| 257 | + const domain = writeDomainName(rr.exchange || rr.value); |
| 258 | + rdLengthBuf[0] += domain.length; |
| 259 | + buffers.push(domain); |
| 260 | + break; |
| 261 | + } |
| 262 | + case 'SOA': |
| 263 | + { |
| 264 | + const mname = writeDomainName(rr.nsname); |
| 265 | + const rname = writeDomainName(rr.hostmaster); |
| 266 | + rdLengthBuf[0] = mname.length + rname.length + 20; |
| 267 | + buffers.push(mname, rname); |
| 268 | + buffers.push(new Uint32Array([ |
| 269 | + rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl |
| 270 | + ])); |
| 271 | + break; |
| 272 | + } |
| 273 | + default: |
| 274 | + throw new Error(`Unknown RR type ${rr.type}`); |
| 275 | + } |
| 276 | + } |
| 277 | + |
| 278 | + return Buffer.concat(buffers.map((typedArray) => { |
| 279 | + const buf = Buffer.from(typedArray.buffer, |
| 280 | + typedArray.byteOffset, |
| 281 | + typedArray.byteLength); |
| 282 | + if (os.endianness() === 'LE') { |
| 283 | + if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16(); |
| 284 | + if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32(); |
| 285 | + } |
| 286 | + return buf; |
| 287 | + })); |
| 288 | +} |
| 289 | + |
| 290 | +module.exports = { types, classes, writeDNSPacket, parseDNSPacket }; |
0 commit comments