diff --git a/TESTS.md b/TESTS.md index 8cffd36..1aec613 100644 --- a/TESTS.md +++ b/TESTS.md @@ -33,46 +33,56 @@ Test that A queries for ANAME-enabled records (but no address) recurse (regardle Generally speaking test the cases of 0, 1, and 2 records of any given type (null case, single case, multi case) +``` +# Serve: +node bin/digd.js +norecurse -p 65053 --input sample/db.json +``` ``` # Sample Data: -# no A records for delegated.daplie.me -# two external NS records for delegted.daplie.me -# zone daplie.me exists +# no A records for out-delegated.example.com +# two external NS records for delegted.example.com +# zone example.com exists # Test: # should return NS records in AUTHORITY section, nothing else -node bin/dig.js @localhost -p 65053 A delegated.daplie.me +node bin/dig.js @localhost -p 65053 A out-delegated.example.com +node bin/dig.js @localhost -p 65053 ANY out-delegated.example.com + +# should return SOA records in AUTHORITY section, nothing else +node bin/dig.js @localhost -p 65053 A in-delegated.example.com +node bin/dig.js @localhost -p 65053 ANY in-delegated.example.com # should return NS records in ANSWER section, nothing else -node bin/dig.js @localhost -p 65053 NS delegated.daplie.me +node bin/dig.js @localhost -p 65053 NS out-delegated.example.com +node bin/dig.js @localhost -p 65053 NS in-delegated.example.com # Sample Data: -# two A records for daplie.me +# two A records for example.com # no NS records # Test: # should return A records in ANSWER section, nothing else -node bin/dig.js @localhost -p 65053 A daplie.me +node bin/dig.js @localhost -p 65053 A example.com # should return SOA records in AUTHORITY section, nothing else -node bin/dig.js @localhost -p 65053 A doesntexist.daplie.me -node bin/dig.js @localhost -p 65053 NS doesntexist.daplie.me +node bin/dig.js @localhost -p 65053 A doesntexist.example.com +node bin/dig.js @localhost -p 65053 NS doesntexist.example.com # Sample Data: -# two A records for a.daplie.me +# two A records for a.example.com # has **internal** NS records # Test: # should return A record in ANSWER section, nothing else -node bin/dig.js @localhost -p 65053 A a.daplie.me +node bin/dig.js @localhost -p 65053 A a.example.com # should return SOA record in AUTHORITY section, nothing else -node bin/dig.js @localhost -p 65053 A doesntexist.a.daplie.me +node bin/dig.js @localhost -p 65053 A doesntexist.a.example.com # should return NS records in ANSWER section, nothing else -node bin/dig.js @localhost -p 65053 NS a.daplie.me +node bin/dig.js @localhost -p 65053 NS a.example.com ``` diff --git a/lib/dns-store.js b/lib/dns-store.js index b42b30a..b60b8f4 100644 --- a/lib/dns-store.js +++ b/lib/dns-store.js @@ -210,10 +210,14 @@ function domainToSoa(db, domain) { }; } -function getSoa(db, domain, results, cb) { +function getSoa(db, domain, results, cb, answerSoa) { console.log('[DEV] getSoa entered'); - results.authority.push(domainToSoa(db, domain)); + if (!answerSoa) { + results.authority.push(domainToSoa(db, domain)); + } else { + results.answer.push(domainToSoa(db, domain)); + } cb(null, results); return; @@ -270,11 +274,110 @@ module.exports.query = function (input, query, cb) { , question: [ query.question[0] ], answer: [], authority: [], additional: [] }; - return getRecords(db, qname, function (err, myRecords) { + function getNsAndSoa(getNsAlso, answerSoa) { + // If the query is www.foo.delegated.example.com + // and we have been delegated delegated.example.com + // and delegated.example.com exists + // but foo.delegated.example.com does not exist + // what's the best strategy for returning the record? + // + // What does PowerDNS do in these situations? + // https://doc.powerdns.com/md/authoritative/backend-generic-mysql/ + + // How to optimize: + // Assume that if a record is being requested, it probably exists + // (someone has probably published it somewhere) + // If the record doesn't exist, then see if any of the domains are managed + // [ 'www.john.smithfam.net', 'john.smithfam.net', 'smithfam.net', 'net' ] + // Then if one of those exists, return the SOA record with NXDOMAIN + + var qarr = qname.split('.'); + var qnames = []; + while (qarr.length) { + qnames.push(qarr.join('.').toLowerCase()); + qarr.shift(); // first + } + + var myDomains = db.domains.filter(function (d) { + return -1 !== qnames.indexOf(d.id.toLowerCase()); + }); + + // this should result in a REFUSED status + if (!myDomains.length) { + // REFUSED will have no records, so we could still recursion, if enabled + results.header.rcode = REFUSED; + cb(null, results); + return; + } + + myDomains.sort(function (d1, d2) { + if (d1.id.length > d2.id.length) { + return -1; + } + if (d1.id.length < d2.id.length) { + return 1; + } + return 0; + }); + //console.log('sorted domains', myDomains); + + if (!getNsAlso) { + return getSoa(db, myDomains[0], results, cb, answerSoa); + } + + return getNs(db, myDomains.slice(0), results, function (err, results) { + //console.log('[DEV] getNs complete'); + + if (err) { cb(err, results); return; } + + // has NS records (or SOA record if NS records match the server itself) + if (results.authority.length) { + console.log(results); cb(null, results); return; + } + + // myDomains was sorted such that the longest was first + return getSoa(db, myDomains[0], results, cb); + + }); + } + + if ('SOA' === query.question[0].typeName) { + return getNsAndSoa(false, true); + } + + //console.log('[DEV] QUERY NAME', qname); + return getRecords(db, qname, function (err, someRecords) { + var myRecords; + var nsRecords = []; + if (err) { cb(err); return; } + // filter out NS (delegation) records, unless that is what is intended + someRecords = someRecords.filter(function (r) { + // If it's not an NS record, it's a potential result + if ('NS' !== r.type && 'NS' !== r.typeName) { + return true; + } + + // If the query was for NS, it's a potential result + if ('NS' === query.question[0].typeName) { + return true; + } + + // If it's a vanity NS, it's not a valid NS for lookup + if (-1 !== db.primaryNameservers.indexOf(r.data.toLowerCase())) { + return false; + } + + nsRecords.push(r); + return false; + }); + + // TODO should NS be returned as ANSWER or AUTHORITY in ANY? + myRecords = someRecords; if (255 !== query.question[0].type && 'ANY' !== query.question[0].typeName) { myRecords = myRecords.filter(function (r) { + return ((r.type && r.type === query.question[0].type) || (r.type && r.type === query.question[0].typeName) || (r.typeName && r.typeName === query.question[0].typeName) @@ -287,72 +390,22 @@ module.exports.query = function (input, query, cb) { results.answer.push(dbToResourceRecord(r)); }); results.header.rcode = NOERROR; - console.log('[DEV] results', results); + //console.log('[DEV] ANSWER results', results); + cb(null, results); + return; + } + else if (nsRecords.length) { + nsRecords.forEach(function (r) { + results.authority.push(dbToResourceRecord(r)); + }); + results.header.rcode = NOERROR; + //console.log('[DEV] AUTHORITY results', results); cb(null, results); return; } if (!myRecords.length) { - // If the query is www.foo.delegated.example.com - // and we have been delegated delegated.example.com - // and delegated.example.com exists - // but foo.delegated.example.com does not exist - // what's the best strategy for returning the record? - // - // What does PowerDNS do in these situations? - // https://doc.powerdns.com/md/authoritative/backend-generic-mysql/ - - // How to optimize: - // Assume that if a record is being requested, it probably exists - // (someone has probably published it somewhere) - // If the record doesn't exist, then see if any of the domains are managed - // [ 'www.john.smithfam.net', 'john.smithfam.net', 'smithfam.net', 'net' ] - // Then if one of those exists, return the SOA record with NXDOMAIN - - var qarr = qname.split('.'); - var qnames = []; - while (qarr.length) { - qnames.push(qarr.join('.').toLowerCase()); - qarr.shift(); // first - } - - var myDomains = db.domains.filter(function (d) { - return -1 !== qnames.indexOf(d.id.toLowerCase()); - }); - - // this should result in a REFUSED status - if (!myDomains.length) { - // REFUSED will have no records, so we could still recursion, if enabled - results.header.rcode = REFUSED; - cb(null, results); - return; - } - - myDomains.sort(function (d1, d2) { - if (d1.id.length > d2.id.length) { - return -1; - } - if (d1.id.length < d2.id.length) { - return 1; - } - return 0; - }); - console.log('sorted domains', myDomains); - - return getNs(db, myDomains.slice(0), results, function (err, results) { - console.log('[DEV] getNs complete'); - - if (err) { cb(err, results); return; } - - // has NS records (or SOA record if NS records match the server itself) - if (results.authority.length) { - console.log(results); cb(null, results); return; - } - - // myDomains was sorted such that the longest was first - getSoa(db, myDomains[0], results, cb); - - }); + getNsAndSoa(true); } }); }; diff --git a/samples/db.js b/samples/db.js index 9b328ea..a205580 100644 --- a/samples/db.js +++ b/samples/db.js @@ -1,69 +1,112 @@ 'use strict'; module.exports = { - "primaryNameservers": [ 'localhost' ] // 'ns1.redirect-www.org' + "primaryNameservers": [ 'localhost' ] // 'ns1.vanity-dns.org' , "domains": [ - { "id": "daplie.me", "revokedAt": 0 } - , { "id": "oneal.daplie.me", "revokedAt": 0 } - , { "id": "aj.oneal.daplie.me", "revokedAt": 0, "vanityNs": [ 'ns1.daplie.me', 'ns2.daplie.me' ] } + { "id": "example.com", "revokedAt": 0 } + , { "id": "smith.example.com", "revokedAt": 0 } + , { "id": "in-delegated.example.com", "revokedAt": 0 } + , { "id": "john.smith.example.com", "revokedAt": 0, "vanityNs": [ 'ns1.dns-server.net', 'ns2.dns-server.net' ] } + // test and probably remove + //, { "id": "out-delegated.example.com", "revokedAt": 0 } ] , "records": [ - // zone daplie.me should be able to have some records on its own - { "zone": "daplie.me", "name": "daplie.me", "tld": "me", "sld": "daplie", "sub": "" - , "type": "A", "address": "23.228.168.108", "aname": "tardigrade.devices.daplie.me" } + // zone example.com should be able to have some records on its own + { "zone": "example.com", "name": "example.com", "tld": "com", "sld": "example", "sub": "" + , "type": "A", "address": "1.2.3.4", "aname": "fido.devices.example.com" } + , { "zone": "example.com", "name": "example.com", "tld": "com", "sld": "example", "sub": "" + , "type": "MX", "priority": 10, "exchange": "mxa.example.org" } + , { "zone": "example.com", "name": "example.com", "tld": "com", "sld": "example", "sub": "" + , "type": "MX", "priority": 10, "exchange": "mxb.example.org" } + , { "zone": "example.com", "name": "example.com", "tld": "com", "sld": "example", "sub": "" + , "type": "SRV", "priority": 10, "weight": 20, "port": 65065, "target": "spot.devices.example.com" } + , { "zone": "example.com", "name": "example.com", "tld": "com", "sld": "example", "sub": "" + , "type": "TXT", "data": [ "foo bar baz" ] } + , { "zone": "example.com", "name": "example.com", "tld": "com", "sld": "example", "sub": "" + , "type": "TXT", "data": [ "foo", "bar", "baz" ] } - , { "zone": "daplie.me", "name": "www.daplie.me", "tld": "me", "sld": "daplie", "sub": "www" - , "type": "A", "address": "23.228.168.108", "aname": "tardigrade.devices.daplie.me" } + // A, CNAME, ANAME, MX, SRV, TXT + , { "zone": "example.com", "name": "a.example.com", "tld": "com", "sld": "example", "sub": "a" + , "type": "A", "address": "4.3.2.1" } + , { "zone": "example.com", "name": "aaaa.example.com", "tld": "com", "sld": "example", "sub": "aaaa" + , "type": "A", "address": "::1" } + , { "zone": "example.com", "name": "aname.example.com", "tld": "com", "sld": "example", "sub": "aname" + , "type": "A", "aname": "amazon.com" } + , { "zone": "example.com", "name": "devname.example.com", "tld": "com", "sld": "example", "sub": "devname" + , "type": "A", "address": "1.2.3.4", "aname": "fido.devices.example.com" } + , { "zone": "example.com", "name": "cname.example.com", "tld": "com", "sld": "example", "sub": "cname" + , "type": "CNAME", "data": "example.com" } // TODO should return additional + , { "zone": "example.com", "name": "mx.example.com", "tld": "com", "sld": "example", "sub": "mx" + , "type": "MX", "priority": 10, "exchange": "mxa.example.org" } + , { "zone": "example.com", "name": "mx.example.com", "tld": "com", "sld": "example", "sub": "mx" + , "type": "MX", "priority": 10, "exchange": "mxb.example.org" } + , { "zone": "example.com", "name": "srv.example.com", "tld": "com", "sld": "example", "sub": "srv" + , "type": "SRV", "priority": 10, "weight": 20, "port": 65065, "target": "spot.devices.example.com" } + , { "zone": "example.com", "name": "txt.example.com", "tld": "com", "sld": "example", "sub": "txt" + , "type": "TXT", "data": [ "foo bar baz" ] } + , { "zone": "example.com", "name": "mtxt.example.com", "tld": "com", "sld": "example", "sub": "mtxt" + , "type": "TXT", "data": [ "foo", "bar", "baz" ] } - , { "zone": "daplie.me", "name": "aname.daplie.me", "tld": "me", "sld": "daplie", "sub": "aname" - , "type": "A", "aname": "google.com" } - - , { "zone": "daplie.me", "name": "email.daplie.me", "tld": "me", "sld": "daplie", "sub": "email" + // www., email., etc just for fun + , { "zone": "example.com", "name": "www.example.com", "tld": "com", "sld": "example", "sub": "www" + , "type": "A", "address": "1.2.3.4", "aname": "fido.devices.example.com" } + , { "zone": "example.com", "name": "email.example.com", "tld": "com", "sld": "example", "sub": "email" , "type": "CNAME", "data": "mailgun.org" } - , { "zone": "daplie.me", "name": "tardigrade.devices.daplie.me", "tld": "me", "sld": "daplie", "sub": "tardigrade.devices" + + // Out-delegated Domains + , { "zone": "example.com", "type": "NS", "name": "out-delegated.example.com" + , "tld": "com", "sld": "example", "sub": "out-delegated", "data": "ns1.vanity-dns.org" } + , { "zone": "example.com", "type": "NS", "name": "out-delegated.example.com" + , "tld": "com", "sld": "example", "sub": "out-delegated", "data": "ns2.vanity-dns.org" } + + // In-delegated Domains + , { "zone": "example.com", "type": "NS", "name": "in-delegated.example.com" + , "tld": "com", "sld": "example", "sub": "in-delegated", "data": "localhost" } + + , { "zone": "example.com", "name": "fido.devices.example.com", "tld": "com", "sld": "example", "sub": "fido.devices" , "device": "abcdef123" - , "type": "ANAME", "address": "23.228.168.108" } + , "type": "ANAME", "address": "1.2.3.4" } - // zone daplie.me can delegate oneal.daplie.me to the same nameserver + // zone example.com can delegate smith.example.com to the same nameserver // (it's probably programmatically and politically simplest to always delegate from a parent zone) - // Thought Experiment: could we delegate the root to a child? i.e. daplie.me -> www.daplie.me + // Thought Experiment: could we delegate the root to a child? i.e. example.com -> www.example.com // to let someone exclusively "own" the root domain, but none of the children? - , { "zone": "daplie.me", "type": "NS", "name": "oneal.daplie.me" - , "tld": "me", "sld": "daplie", "sub": "oneal", "data": "ns1.redirect-www.org" } + , { "zone": "example.com", "type": "NS", "name": "smith.example.com" + , "tld": "com", "sld": "example", "sub": "smith", "data": "ns1.vanity-dns.org" } - , { "zone": "daplie.me", "name": "oneal.daplie.me", "tld": "me", "sld": "daplie", "sub": "oneal" - , "type": "NS", "data": "ns2.redirect-www.org" } + , { "zone": "example.com", "name": "smith.example.com", "tld": "com", "sld": "example", "sub": "smith" + , "type": "NS", "data": "ns2.vanity-dns.org" } // - // now the zone "oneal.daplie.me" can be independently owned (and delegated) - // ... but what about email for aj@daplie.me with aj@daplie.me? - , { "zone": "oneal.daplie.me", "name": "oneal.daplie.me", "tld": "daplie.me", "sld": "oneal", "sub": "" - , "type": "A", "address": "45.56.59.142", "aname": "leo.devices.oneal.daplie.me" } + // now the zone "smith.example.com" can be independently owned (and delegated) + // ... but what about email for john@example.com with john@example.com? + , { "zone": "smith.example.com", "name": "smith.example.com", "tld": "example.com", "sld": "smith", "sub": "" + , "type": "A", "address": "45.56.59.142", "aname": "rex.devices.smith.example.com" } - , { "zone": "oneal.daplie.me", "name": "www.oneal.daplie.me", "tld": "daplie.me", "sld": "oneal", "sub": "www" - , "type": "CNAME", "data": "oneal.daplie.me" } + , { "zone": "smith.example.com", "name": "www.smith.example.com", "tld": "example.com", "sld": "smith", "sub": "www" + , "type": "CNAME", "data": "smith.example.com" } - , { "zone": "oneal.daplie.me", "name": "aj.oneal.daplie.me", "tld": "daplie.me", "sld": "oneal", "sub": "aj" - , "type": "NS", "data": "ns1.redirect-www.org" } + , { "zone": "smith.example.com", "name": "john.smith.example.com", "tld": "example.com", "sld": "smith", "sub": "john" + , "type": "NS", "data": "ns1.vanity-dns.org" } - , { "zone": "oneal.daplie.me", "name": "aj.oneal.daplie.me", "tld": "daplie.me", "sld": "oneal", "sub": "aj" - , "type": "NS", "data": "ns2.redirect-www.org" } + , { "zone": "smith.example.com", "name": "john.smith.example.com", "tld": "example.com", "sld": "smith", "sub": "john" + , "type": "NS", "data": "ns2.vanity-dns.org" } // there can be a wildcard, to which a delegation is the exception - , { "zone": "oneal.daplie.me", "name": "*.oneal.daplie.me", "tld": "daplie.me", "sld": "oneal", "sub": "*" - , "type": "A", "address": "45.56.59.142", "aname": "leo.devices.oneal.daplie.me" } + , { "zone": "smith.example.com", "name": "*.smith.example.com", "tld": "example.com", "sld": "smith", "sub": "*" + , "type": "A", "address": "45.56.59.142", "aname": "rex.devices.smith.example.com" } // there can be an exception to the delegation - , { "zone": "oneal.daplie.me", "name": "exception.aj.oneal.daplie.me", "tld": "daplie.me", "sld": "oneal", "sub": "exception.aj" - , "type": "A", "address": "45.56.59.142", "aname": "leo.devices.oneal.daplie.me" } + , { "zone": "smith.example.com", "name": "exception.john.smith.example.com", "tld": "example.com", "sld": "smith", "sub": "exception.john" + , "type": "A", "address": "45.56.59.142", "aname": "rex.devices.smith.example.com" } // - // aj.oneal.daplie.me + // john.smith.example.com // - , { "zone": "aj.oneal.daplie.me", "name": "aj.oneal.daplie.me", "tld": "oneal.daplie.me", "sld": "aj", "sub": "" - , "type": "A", "address": "45.56.59.142", "aname": "leo.devices.oneal.daplie.me" } + , { "zone": "john.smith.example.com", "name": "john.smith.example.com", "tld": "smith.example.com", "sld": "john", "sub": "" + , "type": "A", "address": "45.56.59.142", "aname": "rex.devices.smith.example.com" } ] } ;