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