diff --git a/lib/constants.js b/lib/constants.js index 7682f39..4cb6031 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -432,6 +432,9 @@ const eflagsByVal = { */ const options = { + // Reference: + // https://www.iana.org/assignments/dns-parameters/ + // dns-parameters.xhtml#dns-parameters-11 RESERVED: 0, // None LLQ: 1, // Long Lived Queries UL: 2, // Update Lease Draft @@ -446,10 +449,17 @@ const options = { PADDING: 12, // Padding CHAIN: 13, // Chain KEYTAG: 14, // EDNS Key Tag + EDE: 15, // Extended DNS Errors (RFC 8914) + // EDNS-Client-Tag: 16 + // EDNS-Server-Tag: 17 - // 15-26945 are unassigned + // 18-20291 are unassigned - // DEVICEID: 26946, + // Umbrella Ident: 20292 + + // 20293-26945 are unassigned + + // DEVICEID: 26946 // 26947-65000 are unassigned @@ -484,6 +494,7 @@ const optionsByVal = { [options.PADDING]: 'PADDING', [options.CHAIN]: 'CHAIN', [options.KEYTAG]: 'KEYTAG', + [options.EDE]: 'EDE', // [options.DEVICEID]: 'DEVICEID', [options.LOCAL]: 'LOCAL' }; diff --git a/lib/internal/schema.js b/lib/internal/schema.js index 8754c99..bb44a2a 100644 --- a/lib/internal/schema.js +++ b/lib/internal/schema.js @@ -520,7 +520,8 @@ const EXPIRESchema = [ ]; const COOKIESchema = [ - ['cookie', HEXEND] + ['clientCookie', HEXEND], + ['serverCookie', HEXEND] ]; const TCPKEEPALIVESchema = [ @@ -540,6 +541,11 @@ const KEYTAGSchema = [ ['tags', TAGS] ]; +const EDESchema = [ + ['infoCode', U16], + ['extraText', CHAR] +]; + const LOCALSchema = [ ['data', HEXEND] ]; @@ -660,6 +666,7 @@ const opts = { [options.PADDING]: PADDINGSchema, [options.CHAIN]: CHAINSchema, [options.KEYTAG]: KEYTAGSchema, + [options.EDE]: EDESchema, [options.LOCAL]: LOCALSchema, [options.LOCALSTART]: LOCALSchema, [options.LOCALEND]: LOCALSchema diff --git a/lib/resolver/dns.js b/lib/resolver/dns.js index c7dcdbf..3f3f84b 100644 --- a/lib/resolver/dns.js +++ b/lib/resolver/dns.js @@ -450,7 +450,7 @@ class DNSResolver extends EventEmitter { // servers get mad at this, most // notably alidns' servers. if (this.rd) - req.edns.setCookie(util.cookie()); + req.edns.setClientCookie(util.cookie()); } if (this.rd) diff --git a/lib/server/dns.js b/lib/server/dns.js index fd4414d..db28ea4 100644 --- a/lib/server/dns.js +++ b/lib/server/dns.js @@ -14,10 +14,10 @@ const dnssec = require('../dnssec'); const DNSError = require('../error'); const {Server} = require('../internal/net'); const wire = require('../wire'); +const util = require('../util'); const { codes, - options, types, MAX_EDNS_SIZE, MAX_UDP_SIZE, @@ -25,7 +25,8 @@ const { } = constants; const { - Message + Message, + Question } = wire; /** @@ -179,14 +180,12 @@ class DNSServer extends EventEmitter { if (this.edns && req.isEDNS()) { res.setEDNS(this.ednsSize, ds); - if (this.ra) { - // Echo cookies if we're recursive. - for (const opt of req.edns.options) { - if (opt.code === options.COOKIE) { - res.edns.options.push(opt); - break; - } - } + // Echo client cookie if present + // and add server cookie + const cookie = req.edns.getCookie(); + if (cookie) { + res.edns.setClientCookie(cookie.clientCookie); + res.edns.addServerCookie(util.cookie()); } } else { res.unsetEDNS(); @@ -298,16 +297,20 @@ class DNSServer extends EventEmitter { } catch (e) { this.emit('error', e); - if (msg.length < 2) - return; - - res = new Message(); - res.id = bio.readU16BE(msg, 0); - res.ra = this.ra; - res.qr = true; - res.code = codes.FORMERR; - - this.send(null, res, rinfo); + // Try to at least echo the msg id and the (first) question + // in the error response + try { + res = new Message(); + res.id = bio.readU16BE(msg, 0); + res.ra = this.ra; + res.qr = true; + res.code = codes.FORMERR; + res.question = [Question.decode(msg.slice(12))]; + + this.send(null, res, rinfo); + } catch (e) { + // ...otherwise just ignore + } return; } diff --git a/lib/wire.js b/lib/wire.js index 2804510..67e7601 100644 --- a/lib/wire.js +++ b/lib/wire.js @@ -418,7 +418,6 @@ class Message extends Struct { assert((size & 0xffff) === size); assert(typeof dnssec === 'boolean'); - this.edns.reset(); this.edns.enabled = true; this.edns.size = size; this.edns.dnssec = dnssec; @@ -1506,20 +1505,30 @@ class EDNS extends Struct { return this.set(option.code, option); } - setCookie(cookie) { + setClientCookie(cookie) { assert(Buffer.isBuffer(cookie)); const option = new COOKIEOption(); - option.cookie = cookie; + option.clientCookie = cookie; return this.add(option); } + addServerCookie(cookie) { + assert(Buffer.isBuffer(cookie)); + const opt = this.getCookie(); + + if (!opt) + return; + + opt.serverCookie = cookie; + } + getCookie() { const opt = this.get(options.COOKIE); if (!opt) return null; - return opt.cookie; + return opt; } hasCookie() { @@ -1535,6 +1544,20 @@ class EDNS extends Struct { return opt.cookie; } + setEDE(infoCode, extraText) { + assert((infoCode & 0xffff) === infoCode); + + const option = new EDEOption(); + option.infoCode = infoCode; + + if (extraText != null) { + assert(typeof extraText === 'string'); + option.extraText = extraText; + } + + return this.add(option); + } + getDataSize(map) { let size = 0; for (const opt of this.options) @@ -5836,7 +5859,8 @@ class EXPIREOption extends OptionData { class COOKIEOption extends OptionData { constructor() { super(); - this.cookie = DUMMY; + this.clientCookie = DUMMY8; + this.serverCookie = DUMMY; } get code() { @@ -5844,18 +5868,35 @@ class COOKIEOption extends OptionData { } getSize() { - return this.cookie.length; + return this.clientCookie.length + this.serverCookie.length; } write(bw) { - bw.writeBytes(this.cookie); + bw.writeBytes(this.clientCookie); + bw.writeBytes(this.serverCookie); return this; } read(br) { - this.cookie = br.readBytes(br.left()); + this.clientCookie = br.readBytes(8); + if (br.left()) { + assert(br.left() >= 8); + this.serverCookie = br.readBytes(br.left()); + } + return this; } + + setServerCookie(data) { + if (!data) { + this.serverCookie = DUMMY; + } else { + assert(Buffer.isBuffer(data)); + assert(data.length >= 8); + assert(data.length <= 32); + this.serverCookie = data; + } + } } /** @@ -5993,6 +6034,40 @@ class KEYTAGOption extends OptionData { } } +/** + * EDE Option + * EDNS Extended DNS Errors Option + * @see https://www.rfc-editor.org/rfc/rfc8914.html + */ + +class EDEOption extends OptionData { + constructor() { + super(); + this.infoCode = 0; // default "other" + this.extraText = ''; + } + + get code() { + return options.EDE; + } + + getSize() { + return 2 + sizeString(this.extraText); + } + + write(bw) { + bw.writeU16BE(this.infoCode); + writeStringBW(bw, this.extraText); + return this; + } + + read(br) { + this.infoCode = br.readU16BE(); + this.extraText = readStringBR(br); + return this; + } +} + /** * LOCAL Option * EDNS Local Option @@ -6389,6 +6464,7 @@ opts = { PADDING: PADDINGOption, CHAIN: CHAINOption, KEYTAG: KEYTAGOption, + EDE: EDEOption, LOCAL: LOCALOption, LOCALSTART: LOCALOption, LOCALEND: LOCALOption @@ -6414,6 +6490,7 @@ optsByVal = { [options.PADDING]: PADDINGOption, [options.CHAIN]: CHAINOption, [options.KEYTAG]: KEYTAGOption, + [options.EDE]: EDEOption, [options.LOCAL]: LOCALOption, [options.LOCALSTART]: LOCALOption, [options.LOCALEND]: LOCALOption @@ -6628,6 +6705,8 @@ function readOption(code, br) { return CHAINOption.read(br); case options.KEYTAG: return KEYTAGOption.read(br); + case options.EDE: + return EDEOption.read(br); default: if (code >= options.LOCALSTART && code <= options.LOCALEND) return LOCALOption.read(br); @@ -6979,6 +7058,7 @@ exports.TCPKEEPALIVEOption = TCPKEEPALIVEOption; exports.PADDINGOption = PADDINGOption; exports.CHAINOption = CHAINOption; exports.KEYTAGOption = KEYTAGOption; +exports.EDEOption = EDEOption; exports.LOCALOption = LOCALOption; exports.AP = AP; diff --git a/test/server-test.js b/test/server-test.js index 0f9ebb1..09533ba 100644 --- a/test/server-test.js +++ b/test/server-test.js @@ -11,13 +11,14 @@ const util = require('../lib/util'); const wire = require('../lib/wire'); const Server = require('../lib/server/dns'); const api = require('../lib/dns'); +const constants = require('../lib/constants'); const StubResolver = require('../lib/resolver/stub'); const RecursiveResolver = require('../lib/resolver/recursive'); const UnboundResolver = require('../lib/resolver/unbound'); const RootResolver = require('../lib/resolver/root'); const AuthServer = require('../lib/server/auth'); const RecursiveServer = require('../lib/server/recursive'); -const {types, codes, Record, KSK_2010} = wire; +const {types, codes, Record, KSK_2010, Message, options} = wire; const ROOT_ZONE = Path.resolve(__dirname, 'data', 'root.zone'); const COM_RESPONSE = Path.resolve(__dirname, 'data', 'com-response.zone'); @@ -35,12 +36,6 @@ if (process.browser) describe('Server', function() { this.timeout(20000); - let server = null; - let dns = null; - let authServer = null; - let recServer = null; - let authQueries = 0; - let recQueries = 0; let inet6 = true; before(() => { @@ -61,447 +56,561 @@ describe('Server', function() { }); }); - it('should listen on port 5300', async () => { - server = new Server({ - tcp: true, - maxConnections: 20, - edns: true, - dnssec: true - }); - - server.on('error', (err) => { - throw err; - }); + describe('Abstract DNS Server', function() { + let server, dns; - const getAnswer = (type) => { - const txt = serverRecords[wire.typeToString(type)]; + before(() => { + server = new Server(); - if (!Array.isArray(txt)) - return null; - - return wire.fromZone(txt.join('\n')); - }; + server.initOptions({ + tcp: true, + maxConnections: 20, + edns: true, + dnssec: true + }); - server.on('query', (req, res, rinfo) => { - const [qs] = req.question; - const answer = getAnswer(qs.type); + const getAnswer = (type) => { + const txt = serverRecords[wire.typeToString(type)]; - if (!answer || !util.equal(qs.name, answer[0].name)) { - res.code = wire.codes.NXDOMAIN; - res.send(); - return; - } + if (!Array.isArray(txt)) + return null; - res.answer = answer; - res.send(); - }); + return wire.fromZone(txt.join('\n')); + }; - await server.bind(5300, '127.0.0.1'); - }); + server.on('query', (req, res, rinfo) => { + const [qs] = req.question; + const answer = getAnswer(qs.type); - it('should instantiate resolver', async () => { - dns = new api.Resolver(); + if (!answer || !util.equal(qs.name, answer[0].name)) { + res.code = wire.codes.NXDOMAIN; + res.send(); + return; + } - dns.setServers(['127.0.0.1:5300']); - }); + res.answer = answer; + res.send(); + }); - it('should respond to A request', async () => { - assert.deepStrictEqual(await dns.lookup('icanhazip.com'), { - address: '147.75.40.2', - family: 4 + dns = new api.Resolver(); }); - }); - it('should respond to PTR request', async () => { - assert.deepStrictEqual(await dns.lookupService('172.217.0.46', 80), { - hostname: 'lga15s43-in-f46.1e100.net', - service: 'http' + after(async () => { + await server.close(); }); - }); - - it('should respond to ANY request', async () => { - assert.deepStrictEqual(await dns.resolveAny('google.com'), [ - { address: '216.58.195.78', ttl: 300, type: 'A' }, - { address: '2607:f8b0:4005:807::200e', ttl: 300, type: 'AAAA' }, - { entries: ['v=spf1 include:_spf.google.com ~all'], type: 'TXT' }, - { exchange: 'aspmx.l.google.com', priority: 10, type: 'MX' }, - { exchange: 'alt2.aspmx.l.google.com', priority: 30, type: 'MX' }, - { - nsname: 'ns1.google.com', - hostmaster: 'dns-admin.google.com', - serial: 213603989, - refresh: 900, - retry: 900, - expire: 1800, - minttl: 60, - type: 'SOA' - }, - { - entries: [ - 'facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95' - ], - type: 'TXT' - }, - { exchange: 'alt4.aspmx.l.google.com', priority: 50, type: 'MX' }, - { value: 'ns2.google.com', type: 'NS' }, - { value: 'ns4.google.com', type: 'NS' }, - { value: 'ns3.google.com', type: 'NS' }, - { exchange: 'alt3.aspmx.l.google.com', priority: 40, type: 'MX' }, - { - entries: ['docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e'], - type: 'TXT' - }, - { value: 'ns1.google.com', type: 'NS' }, - { exchange: 'alt1.aspmx.l.google.com', priority: 20, type: 'MX' } - ]); - }); - - it('should respond to A request', async () => { - assert.deepStrictEqual(await dns.resolve4('icanhazip.com'), [ - '147.75.40.2' - ]); - }); - it('should respond to AAAA request', async () => { - assert.deepStrictEqual(await dns.resolve6('icanhazip.com'), [ - '2604:1380:1000:af00::1', - '2604:1380:3000:3b00::1', - '2604:1380:1:cd00::1' - ]); - }); - - it('should respond to CNAME request', async () => { - assert.deepStrictEqual(await dns.resolveCname('mail.google.com'), [ - 'googlemail.l.google.com' - ]); - }); - - it('should respond to MX request', async () => { - assert.deepStrictEqual(await dns.resolveMx('google.com'), [ - { exchange: 'alt3.aspmx.l.google.com', priority: 40 }, - { exchange: 'alt1.aspmx.l.google.com', priority: 20 }, - { exchange: 'alt4.aspmx.l.google.com', priority: 50 }, - { exchange: 'alt2.aspmx.l.google.com', priority: 30 }, - { exchange: 'aspmx.l.google.com', priority: 10 } - ]); - }); - - it('should respond to NAPTR request', async () => { - assert.deepStrictEqual(await dns.resolveNaptr('apple.com'), [ - { - flags: 'se', - service: 'SIPS+D2T', - regexp: '', - replacement: '_sips._tcp.apple.com', - order: 50, - preference: 50 - }, - { - flags: 'se', - service: 'SIP+D2T', - regexp: '', - replacement: '_sip._tcp.apple.com', - order: 90, - preference: 50 - }, - { - flags: 'se', - service: 'SIP+D2U', - regexp: '', - replacement: '_sip._udp.apple.com', - order: 100, - preference: 50 - } - ]); - }); - - it('should respond to PTR request', async () => { - assert.deepStrictEqual(await dns.resolvePtr('46.0.217.172.in-addr.arpa.'), [ - 'lga15s43-in-f46.1e100.net', - 'sfo07s26-in-f14.1e100.net', - 'lga15s43-in-f14.1e100.net', - 'lga15s43-in-f46.1e100.net', - 'sfo07s26-in-f14.1e100.net', - 'lga15s43-in-f14.1e100.net' - ]); - }); - - it('should respond to SOA request', async () => { - assert.deepStrictEqual(await dns.resolveSoa('google.com'), { - nsname: 'ns1.google.com', - hostmaster: 'dns-admin.google.com', - serial: 213603989, - refresh: 900, - retry: 900, - expire: 1800, - minttl: 60 + it('should listen on port 5300', async () => { + await server.bind(5300, '127.0.0.1'); }); - }); - it('should respond to SRV request', async () => { - assert.deepStrictEqual( - await dns.resolveSrv('_xmpp-server._tcp.gmail.com'), - [ - { name: 'alt4.xmpp-server.l.google.com', - port: 5269, - priority: 20, - weight: 0 }, - { name: 'alt3.xmpp-server.l.google.com', - port: 5269, - priority: 20, - weight: 0 }, - { name: 'xmpp-server.l.google.com', - port: 5269, - priority: 5, - weight: 0 }, - { name: 'alt1.xmpp-server.l.google.com', - port: 5269, - priority: 20, - weight: 0 }, - { name: 'alt2.xmpp-server.l.google.com', - port: 5269, - priority: 20, - weight: 0 } - ] - ); - }); - - it('should respond to TXT request', async () => { - assert.deepStrictEqual(await dns.resolveTxt('google.com'), [ - ['facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95'], - ['docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e'], - ['v=spf1 include:_spf.google.com ~all'] - ]); - }); - - it('should respond to PTR request', async () => { - assert.deepStrictEqual(await dns.reverse('172.217.0.46'), [ - 'lga15s43-in-f46.1e100.net', - 'sfo07s26-in-f14.1e100.net', - 'lga15s43-in-f14.1e100.net', - 'lga15s43-in-f46.1e100.net', - 'sfo07s26-in-f14.1e100.net', - 'lga15s43-in-f14.1e100.net' - ]); - }); - - it('should close server', async () => { - await server.close(); - }); - - it('should open authoritative server', async () => { - authServer = new AuthServer({ - tcp: true, - edns: true, - dnssec: true + it('should point resolver at server', async () => { + dns.setServers(['127.0.0.1:5300']); + const res = dns.getServers(); + assert.deepStrictEqual(res, ['127.0.0.1:5300']); }); - authServer.on('error', (err) => { - throw err; + it('should respond to A request', async () => { + assert.deepStrictEqual(await dns.lookup('icanhazip.com'), { + address: '147.75.40.2', + family: 4 + }); }); - authServer.on('query', () => { - authQueries += 1; + it('should respond to PTR request', async () => { + assert.deepStrictEqual(await dns.lookupService('172.217.0.46', 80), { + hostname: 'lga15s43-in-f46.1e100.net', + service: 'http' + }); }); - authServer.setOrigin('.'); - authServer.setFile(ROOT_ZONE); - - await authServer.bind(5301, '127.0.0.1'); - }); - - it('should open recursive server', async () => { - recServer = new RecursiveServer({ - tcp: true, - inet6, - edns: true, - dnssec: true + it('should respond to ANY request', async () => { + assert.deepStrictEqual(await dns.resolveAny('google.com'), [ + { address: '216.58.195.78', ttl: 300, type: 'A' }, + { address: '2607:f8b0:4005:807::200e', ttl: 300, type: 'AAAA' }, + { entries: ['v=spf1 include:_spf.google.com ~all'], type: 'TXT' }, + { exchange: 'aspmx.l.google.com', priority: 10, type: 'MX' }, + { exchange: 'alt2.aspmx.l.google.com', priority: 30, type: 'MX' }, + { + nsname: 'ns1.google.com', + hostmaster: 'dns-admin.google.com', + serial: 213603989, + refresh: 900, + retry: 900, + expire: 1800, + minttl: 60, + type: 'SOA' + }, + { + entries: [ + 'facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95' + ], + type: 'TXT' + }, + { exchange: 'alt4.aspmx.l.google.com', priority: 50, type: 'MX' }, + { value: 'ns2.google.com', type: 'NS' }, + { value: 'ns4.google.com', type: 'NS' }, + { value: 'ns3.google.com', type: 'NS' }, + { exchange: 'alt3.aspmx.l.google.com', priority: 40, type: 'MX' }, + { + entries: ['docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e'], + type: 'TXT' + }, + { value: 'ns1.google.com', type: 'NS' }, + { exchange: 'alt1.aspmx.l.google.com', priority: 20, type: 'MX' } + ]); }); - recServer.on('error', (err) => { - throw err; + it('should respond to A request', async () => { + assert.deepStrictEqual(await dns.resolve4('icanhazip.com'), [ + '147.75.40.2' + ]); }); - recServer.on('query', () => { - recQueries += 1; + it('should respond to AAAA request', async () => { + assert.deepStrictEqual(await dns.resolve6('icanhazip.com'), [ + '2604:1380:1000:af00::1', + '2604:1380:3000:3b00::1', + '2604:1380:1:cd00::1' + ]); }); - recServer.resolver.setStub( - '127.0.0.1', - 5301, - Record.fromString(KSK_2010) - ); - - await recServer.bind(5302, '127.0.0.1'); - }); + it('should respond to CNAME request', async () => { + assert.deepStrictEqual(await dns.resolveCname('mail.google.com'), [ + 'googlemail.l.google.com' + ]); + }); - it('should query authoritative server (stub)', async () => { - const stub = new StubResolver({ - rd: false, - cd: false, - edns: true, - dnssec: true, - hosts: [ - ['localhost.', '127.0.0.1'], - ['localhost.', '::1'] - ], - servers: ['127.0.0.1:5301'] + it('should respond to MX request', async () => { + assert.deepStrictEqual(await dns.resolveMx('google.com'), [ + { exchange: 'alt3.aspmx.l.google.com', priority: 40 }, + { exchange: 'alt1.aspmx.l.google.com', priority: 20 }, + { exchange: 'alt4.aspmx.l.google.com', priority: 50 }, + { exchange: 'alt2.aspmx.l.google.com', priority: 30 }, + { exchange: 'aspmx.l.google.com', priority: 10 } + ]); }); - stub.on('error', (err) => { - throw err; + it('should respond to NAPTR request', async () => { + assert.deepStrictEqual(await dns.resolveNaptr('apple.com'), [ + { + flags: 'se', + service: 'SIPS+D2T', + regexp: '', + replacement: '_sips._tcp.apple.com', + order: 50, + preference: 50 + }, + { + flags: 'se', + service: 'SIP+D2T', + regexp: '', + replacement: '_sip._tcp.apple.com', + order: 90, + preference: 50 + }, + { + flags: 'se', + service: 'SIP+D2U', + regexp: '', + replacement: '_sip._udp.apple.com', + order: 100, + preference: 50 + } + ]); }); - await stub.open(); + it('should respond to PTR request', async () => { + assert.deepStrictEqual(await dns.resolvePtr('46.0.217.172.in-addr.arpa.'), [ + 'lga15s43-in-f46.1e100.net', + 'sfo07s26-in-f14.1e100.net', + 'lga15s43-in-f14.1e100.net', + 'lga15s43-in-f46.1e100.net', + 'sfo07s26-in-f14.1e100.net', + 'lga15s43-in-f14.1e100.net' + ]); + }); - { - const msg = await stub.lookup('com.', types.NS); - assert(msg.code === codes.NOERROR); - assert(!msg.aa); + it('should respond to SOA request', async () => { + assert.deepStrictEqual(await dns.resolveSoa('google.com'), { + nsname: 'ns1.google.com', + hostmaster: 'dns-admin.google.com', + serial: 213603989, + refresh: 900, + retry: 900, + expire: 1800, + minttl: 60 + }); + }); - const expect = wire.fromZone(comResponse); - assert.deepStrictEqual(msg.authority, expect); + it('should respond to SRV request', async () => { + assert.deepStrictEqual( + await dns.resolveSrv('_xmpp-server._tcp.gmail.com'), + [ + { name: 'alt4.xmpp-server.l.google.com', + port: 5269, + priority: 20, + weight: 0 }, + { name: 'alt3.xmpp-server.l.google.com', + port: 5269, + priority: 20, + weight: 0 }, + { name: 'xmpp-server.l.google.com', + port: 5269, + priority: 5, + weight: 0 }, + { name: 'alt1.xmpp-server.l.google.com', + port: 5269, + priority: 20, + weight: 0 }, + { name: 'alt2.xmpp-server.l.google.com', + port: 5269, + priority: 20, + weight: 0 } + ] + ); + }); - const glue = wire.fromZone(comGlue); - assert.deepStrictEqual(msg.additional, glue); - } + it('should respond to TXT request', async () => { + assert.deepStrictEqual(await dns.resolveTxt('google.com'), [ + ['facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95'], + ['docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e'], + ['v=spf1 include:_spf.google.com ~all'] + ]); + }); - { - const msg = await stub.lookup('idontexist.', types.A); - assert(!msg.aa); - assert(msg.code === codes.NXDOMAIN); - assert(msg.answer.length === 0); + it('should respond to PTR request', async () => { + assert.deepStrictEqual(await dns.reverse('172.217.0.46'), [ + 'lga15s43-in-f46.1e100.net', + 'sfo07s26-in-f14.1e100.net', + 'lga15s43-in-f14.1e100.net', + 'lga15s43-in-f46.1e100.net', + 'sfo07s26-in-f14.1e100.net', + 'lga15s43-in-f14.1e100.net' + ]); + }); + }); - const expect = wire.fromZone(nxResponse); - assert.deepStrictEqual(msg.authority, expect); - } + describe('Extended DNS Server', function () { + const ports = { + a: 5301, + r: 5302 + }; - await stub.close(); - }); + let authQueries = 0; + let recQueries = 0; + let authServer, recServer; - it('should query recursive server (stub)', async () => { - const stub = new StubResolver({ - rd: true, - cd: false, - edns: true, - dnssec: true, - hosts: [ - ['localhost.', '127.0.0.1'], - ['localhost.', '::1'] - ], - servers: ['127.0.0.1:5302'] + before(() => { + authServer = new AuthServer({ + tcp: true, + edns: true, + dnssec: true + }); + authServer.on('query', () => { + authQueries += 1; + }); + authServer.setOrigin('.'); + authServer.setFile(ROOT_ZONE); + + recServer = new RecursiveServer({ + tcp: true, + inet6, + edns: true, + dnssec: true + }); + recServer.on('query', () => { + recQueries += 1; + }); + recServer.resolver.setStub( + '127.0.0.1', + ports.a, + Record.fromString(KSK_2010) + ); }); - stub.on('error', (err) => { - throw err; + after(async () => { + await recServer.close(); + await authServer.close(); + + assert.strictEqual(authQueries, 21); + assert.strictEqual(recQueries, 1); }); - await stub.open(); + describe('Authoritative', function () { + it('should open authoritative server', async () => { + await authServer.bind(ports.a, '127.0.0.1'); + }); - const msg = await stub.lookup('google.com.', types.A); - assert(msg.code === codes.NOERROR); - assert(msg.answer.length > 0); - assert(msg.answer[0].name === 'google.com.'); - assert(msg.answer[0].type === types.A); + it('should query authoritative server (stub)', async () => { + const stub = new StubResolver({ + rd: false, + cd: false, + edns: true, + dnssec: true, + hosts: [ + ['localhost.', '127.0.0.1'], + ['localhost.', '::1'] + ], + servers: [`127.0.0.1:${ports.a}`] + }); + + stub.on('error', (err) => { + throw err; + }); + + await stub.open(); + + { + const msg = await stub.lookup('com.', types.NS); + assert(msg.code === codes.NOERROR); + assert(!msg.aa); + + const expect = wire.fromZone(comResponse); + assert.deepStrictEqual(msg.authority, expect); + + const glue = wire.fromZone(comGlue); + assert.deepStrictEqual(msg.additional, glue); + } + + { + const msg = await stub.lookup('idontexist.', types.A); + assert(!msg.aa); + assert(msg.code === codes.NXDOMAIN); + assert(msg.answer.length === 0); + + const expect = wire.fromZone(nxResponse); + assert.deepStrictEqual(msg.authority, expect); + } + + await stub.close(); + }); - await stub.close(); - }); + it('should do a root resolution', async () => { + const res = new RootResolver({ + tcp: true, + inet6, + edns: true, + dnssec: true + }); - it('should do a recursive resolution', async () => { - const res = new RecursiveResolver({ - tcp: true, - inet6, - edns: true, - dnssec: true - }); + res.on('error', (err) => { + throw err; + }); - res.setStub('127.0.0.1', 5301, Record.fromString(KSK_2010)); + res.servers = [{ + host: '127.0.0.1', + port: ports.a + }]; - res.on('error', (err) => { - throw err; - }); + await res.open(); - await res.open(); + util.fakeTime('2018-08-05:00:00.000Z'); - const msg = await res.lookup('google.com.', types.A); - assert(msg.code === codes.NOERROR); - assert(msg.answer.length > 0); - assert(msg.answer[0].name === 'google.com.'); - assert(msg.answer[0].type === types.A); + const msg = await res.lookup('com.'); + assert(msg.code === codes.NOERROR); + assert(!msg.aa); + assert(msg.ad); - await res.close(); - }); + const expect = wire.fromZone(comResponse); + expect.pop(); // pop signature + assert.deepStrictEqual(msg.authority, expect); - it('should do a recursive resolution (unbound)', async () => { - const res = new UnboundResolver({ - tcp: true, - inet6, - edns: true, - dnssec: true - }); + const glue = wire.fromZone(comGlue); + assert.deepStrictEqual(msg.additional, glue); - res.setStub('127.0.0.1', 5301, Record.fromString(KSK_2010)); + util.fakeTime(); - res.on('error', (err) => { - throw err; - }); + await res.close(); + }); - await res.open(); + it('should do a recursive resolution', async () => { + const res = new RecursiveResolver({ + tcp: true, + inet6, + edns: true, + dnssec: true + }); - const msg = await res.lookup('google.com.', types.A); - assert(msg.code === codes.NOERROR); - assert(msg.answer.length > 0); - assert(msg.answer[0].name === 'google.com.'); - assert(msg.answer[0].type === types.A); + res.setStub('127.0.0.1', ports.a, Record.fromString(KSK_2010)); - await res.close(); - }); + await res.open(); - it('should do a root resolution', async () => { - const res = new RootResolver({ - tcp: true, - inet6, - edns: true, - dnssec: true - }); + const msg = await res.lookup('google.com.', types.A); + assert(msg.code === codes.NOERROR); + assert(msg.answer.length > 0); + assert(msg.answer[0].name === 'google.com.'); + assert(msg.answer[0].type === types.A); - res.on('error', (err) => { - throw err; - }); - - res.servers = [{ - host: '127.0.0.1', - port: 5301 - }]; + await res.close(); + }); - await res.open(); + it('should do a recursive resolution (unbound)', async () => { + const res = new UnboundResolver({ + tcp: true, + inet6, + edns: true, + dnssec: true + }); - util.fakeTime('2018-08-05:00:00.000Z'); + res.setStub('127.0.0.1', ports.a, Record.fromString(KSK_2010)); - const msg = await res.lookup('com.'); - assert(msg.code === codes.NOERROR); - assert(!msg.aa); - assert(msg.ad); + res.on('error', (err) => { + throw err; + }); - const expect = wire.fromZone(comResponse); - expect.pop(); // pop signature - assert.deepStrictEqual(msg.authority, expect); + await res.open(); - const glue = wire.fromZone(comGlue); - assert.deepStrictEqual(msg.additional, glue); + const msg = await res.lookup('google.com.', types.A); + assert(msg.code === codes.NOERROR); + assert(msg.answer.length > 0); + assert(msg.answer[0].name === 'google.com.'); + assert(msg.answer[0].type === types.A); - util.fakeTime(); + await res.close(); + }); - await res.close(); - }); + describe('EDNS', function () { + const stub = new StubResolver({ + rd: true, + cd: false, + edns: true, + dnssec: true, + hosts: [ + ['localhost.', '127.0.0.1'], + ['localhost.', '::1'] + ], + servers: [`127.0.0.1:${ports.a}`] + }); + + // StubResolver automatically retries queries without EDNS + // if it gets an error. We actually want those errors during + // testing, so this function always returns whatever we get first. + async function getFirstResponse(name, type) { + return new Promise(async (resolve) => { + let msg; + stub.socket.once('message', (data) => { + msg = Message.decode(data); + }); + + // Wait until the request is completely fulfilled + await stub.lookup(name, type); + resolve(msg); + }); + } + + before(async () => { + await stub.open(); + }); + + after(async () => { + await stub.close(); + }); + + const randomCookie = util.cookie; + afterEach(() => { + util.cookie = randomCookie; + }); + + it('should query without edns', async () => { + stub.edns = false; + const res = await getFirstResponse('.', types.SOA); + + assert(!res.edns.enabled); + assert.strictEqual(res.edns.size, constants.MAX_UDP_SIZE); + }); + + it('should query with edns', async () => { + stub.edns = true; + const res = await getFirstResponse('.', types.SOA); + + assert(res.edns.enabled); + assert.strictEqual(res.edns.size, constants.MAX_EDNS_SIZE); + }); + + it('should throw if edns is malformed (client cookie)', async () => { + stub.edns = true; + util.cookie = () => { + return Buffer.from([1, 2, 3, 4, 5, 6, 7]); // too short clientCookie + }; + const res = await getFirstResponse('applepie.', types.SOA); + + assert.strictEqual(res.code, codes.FORMERR); + assert(res.question.length); + assert.deepStrictEqual( + res.question[0].getJSON(), + { + name: 'applepie.', + class: 'IN', + type: 'SOA' + } + ); + }); + + it('should throw if edns is malformed (server cookie)', async () => { + stub.edns = true; + util.cookie = () => { + return Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]); // too short serverCookie + }; + const res = await getFirstResponse('cheddarcheese.', types.SOA); + + assert.strictEqual(res.code, codes.FORMERR); + assert(res.question.length); + assert.deepStrictEqual( + res.question[0].getJSON(), + { + name: 'cheddarcheese.', + class: 'IN', + type: 'SOA' + } + ); + }); + + it('should echo client cookie', async () => { + stub.edns = true; + util.cookie = () => { + return Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + }; + const res = await getFirstResponse('sideoffries.', types.SOA); + + const opt = res.edns.options[0]; + assert.strictEqual(opt.code, options.COOKIE); + assert.strictEqual( + opt.option.clientCookie.toString('hex'), + '0102030405060708' + ); + assert.strictEqual(opt.option.serverCookie.length, 8); + }); + }); + }); - it('should have total requests', () => { - assert.strictEqual(authQueries, 16); - assert.strictEqual(recQueries, 1); - }); + describe('Recursive', function () { + it('should open recursive server', async () => { + await recServer.bind(ports.r, '127.0.0.1'); + }); - it('should close servers', async () => { - await recServer.close(); - await authServer.close(); + it('should query recursive server (stub)', async () => { + const stub = new StubResolver({ + rd: true, + cd: false, + edns: true, + dnssec: true, + hosts: [ + ['localhost.', '127.0.0.1'], + ['localhost.', '::1'] + ], + servers: [`127.0.0.1:${ports.r}`] + }); + + await stub.open(); + + const msg = await stub.lookup('google.com.', types.A); + assert(msg.code === codes.NOERROR); + assert(msg.answer.length > 0); + assert(msg.answer[0].name === 'google.com.'); + assert(msg.answer[0].type === types.A); + + await stub.close(); + }); + }); }); }); diff --git a/test/wire-test.js b/test/wire-test.js index e6d2d5c..81e1e4b 100644 --- a/test/wire-test.js +++ b/test/wire-test.js @@ -138,5 +138,21 @@ describe('Wire', function() { deepStrictEqual(Message.fromJSON(msg.toJSON()), msg); } }); + + it('should set EDNS extended error', () => { + const msg = new Message(); + msg.edns.setEDE(7, 'Signature Expired'); + msg.setEDNS(4096, true); + assert.deepStrictEqual( + msg.getJSON().edns.options, + [{ + code: 'EDE', + option: { + infoCode: 7, + extraText: 'Signature Expired' + } + }] + ); + }); }); });