From 4734b5ac5787b82c729937dc822429a57038d189 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 6 Oct 2017 15:34:36 -0600 Subject: [PATCH] WIP dns store --- HOW_DELEGATION_WORKS.md | 59 ++++++++++ bin/digd.js | 4 + lib/dns-store.js | 253 +++++++++++++++++++++++++++++++++++++++- samples/zones.js | 61 ++++++++++ 4 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 samples/zones.js diff --git a/HOW_DELEGATION_WORKS.md b/HOW_DELEGATION_WORKS.md index ea7ee22..5919126 100644 --- a/HOW_DELEGATION_WORKS.md +++ b/HOW_DELEGATION_WORKS.md @@ -166,3 +166,62 @@ but I but you could do something weird like host `whatever.john.daplie.me` on th nameserver by A) answering to it directly on the main nameserver and B) delegating from `whatever.john.daplie.me` back to the original nameserver in case the resolving client makes intelligent assumptions and caching. + +When a domain doesn't exist +--------------------------- + +### NXDOMAIN + +This nameserver can respond for that domain, but no record exists + +``` +dig @ns1.google.com doesntexist.google.com +``` + +``` +; <<>> DiG 9.8.3-P1 <<>> @ns1.google.com doesntexist.google.com +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 45549 +;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0 +;; WARNING: recursion requested but not available + +;; QUESTION SECTION: +;doesntexist.google.com. IN A + +;; AUTHORITY SECTION: +google.com. 60 IN SOA ns2.google.com. dns-admin.google.com. 170961396 900 900 1800 60 + +;; Query time: 50 msec +;; SERVER: 216.239.32.10#53(216.239.32.10) +;; WHEN: Wed Oct 4 01:14:09 2017 +;; MSG SIZE rcvd: 90 +``` + +### REFUSED + +This nameserver does not store records for that domain +(and would appreciated it if you didn't ask) + +```bash +dig @ns1.google.com daplie.com +``` + +``` +; <<>> DiG 9.8.3-P1 <<>> @ns1.google.com daplie.com +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 47317 +;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 +;; WARNING: recursion requested but not available + +;; QUESTION SECTION: +;daplie.com. IN A + +;; Query time: 52 msec +;; SERVER: 216.239.32.10#53(216.239.32.10) +;; WHEN: Wed Oct 4 01:14:20 2017 +;; MSG SIZE rcvd: 28 +``` diff --git a/bin/digd.js b/bin/digd.js index 5c0f65b..fd10846 100755 --- a/bin/digd.js +++ b/bin/digd.js @@ -169,6 +169,10 @@ cli.main(function (args, cli) { } function sendEmptyResponse(query, nx) { + // rcode + // 0 SUCCESS // manages this domain and found a record + // 3 NXDOMAIN // manages this domain, but doesn't have a record + // 5 REFUSED // doesn't manage this domain var newAb; var emptyResp = { header: { diff --git a/lib/dns-store.js b/lib/dns-store.js index e0b9050..4df187f 100644 --- a/lib/dns-store.js +++ b/lib/dns-store.js @@ -2,16 +2,259 @@ 'use strict'; /* -var fs = require('fs'); - module.exports.ask = function (query, cb) { }; */ -module.exports.query = function (input, query, cb) { - process.nextTick(function () { - cb(new Error('No local lookup method for DNS records defined.')); +var SUCCESS = 0; +var NXDOMAIN = 3; +var REFUSED = 5; + +function getRecords(db, qname) { + var myRecords = db.records.filter(function (r) { + if ('string' !== typeof r.domain) { + return false; + } + + // TODO use IN in masterquest (or implement OR) + // Only return single-level wildcard? + if (qname === r.domain || ('*.' + qname.split('.').slice(1).join('.')) === r.domain) { + return true; + } }); + + return myRecords; +} + +function dbToResourceRecord(r) { + return { + name: r.domain + , typeName: r.type // NS + , className: 'IN' + , ttl: r.ttl || 300 + + // SOA + /* + , "primary": "ns1.yahoo.com" + , "admin": "hostmaster.yahoo-inc.com" + , "serial": 2017092539 + , "refresh": 3600 + , "retry": 300 + , "expiration": 1814400 + , "minimum": 600 + */ + + // A, AAAA + , address: -1 !== [ 'A', 'AAAA' ].indexOf(r.type) ? (r.address || r.value) : undefined + + // CNAME, NS, PTR || TXT + , data: -1 !== [ 'CNAME', 'NS', 'PTR', 'TXT' ].indexOf(r.type) ? (r.value || r.values) : undefined + + // MX, SRV + , priority: r.priority + + // MX + , exchange: r.exchange + + // SRV + , weight: r.weight + , port: r.port + , target: r.target + }; +} + +function getNs(db, ds, results, cb) { + var d = ds.shift(); + + if (!d) { + results.rcode = NXDOMAIN; + cb(null, results); + return; + } + + var qn = d.id.toLowerCase(); + getRecords(db, qn).forEach(function (r) { + if ('NS' !== r.type) { + return; + } + + results.authority.push({ + name: r.domain + , typeName: r.type // NS + , className: 'IN' + , ttl: r.ttl || 300 + , data: r.value + }); + }); + + if (results.authority.length) { + cb(null, results); + return; + } +} + +function getSoa(db, domain, results, cb) { + var index = Math.floor(Math.random() * domain.nameservers.length) % domain.nameservers.length; + var nameserver = domain.nameservers[index]; + + results.authority.push({ + name: domain.id + , typeName: 'SOA' + , className: 'IN' + , ttl: domain.ttl || 60 + + // nameserver -- select an NS at random if they're all in sync + , primary: nameserver + , name_server: nameserver + + // admin -- email address or domain for admin + , admin: domain.admin + , email_addr: domain.admin + + // serial -- the version, for cache-busting of secondary nameservers. suggested format: YYYYMMDDnn + , serial: domain.serial || Math.round((domain.updatedAt || domain.createdAt) / 1000) + , sn: domain.serial || Math.round((domain.updatedAt || domain.createdAt) / 1000) + + // refresh -- only used when nameservers following the DNS NOTIFY spec talk + , refresh: domain.refresh || 1800 + , ref: domain.refresh || 1800 + + // retry -- only used when nameservers following the DNS NOTIFY spec talk + , retry: domain.retry || 600 + , ret: domain.retry || 600 + + // expiration -- how long other nameservers should continue when the primary goes down + , expiration: domain.expiration || 2419200 + , ex: domain.expiration || 2419200 + + // minimum -- how long to cache a non-existent domain (also the default ttl for BIND) + , minimum: domain.minimum || 5 + , nx: domain.minimum || 5 + }); + + cb(null, results); + return; +} + +module.exports.query = function (input, query, cb) { + /* + var fs = require('fs'); + + fs.readFile(input, 'utf8', function (err, text) { + if (err) { cb(err); return; } + var records; + try { + records = JSON.parse(text); + } catch(e) { cb(e); return; } + }); + */ + + var db; + var qname; + try { + db = require(input); + } catch(e) { cb(e); return; } + + if (!Array.isArray(query.question) || query.question.length < 1) { + cb(new Error("query is missing question section")); + return; + } + + if (1 !== query.question.length) { + cb(new Error("query should have exactly one question (for now)")); + return; + } + + if (!query.question[0] || 'string' !== typeof query.question[0].name) { + cb(new Error("query's question section should exist and have a String name property")); + return; + } + + qname = query.question[0].name.toLowerCase(); + + var results = { + header: { + id: query.header.id // same as request + , qr: 1 + , opcode: 0 // pretty much always 0 QUERY + , aa: 1 // TODO right now we assume that if we have the record, we're authoritative + // but in reality we could be hitting a cache and then recursing on a cache miss + , tc: 0 + , rd: query.header.rd // duh + , ra: 0 // will be changed by cli.norecurse + , rcode: SUCCESS // 0 SUCCESS, 3 NXDOMAIN, 5 REFUSED + } + , question: [], answer: [], authority: [], additional: [] + }; + + var myRecords = getRecords(db, qname); + + if (myRecords.length) { + myRecords.forEach(function (r) { + results.answer.push(dbToResourceRecord(r)); + }); + 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) { + if (err) { cb(err, results); return; } + + // has NS records + if (NXDOMAIN !== results.header.rcode) { cb(null, results); return; } + + // myDomains was sorted such that the longest was first + getSoa(db, myDomains[0], results, cb); + + }); + } + /* query.question.forEach(function (q) { module.exports.ask(q); diff --git a/samples/zones.js b/samples/zones.js new file mode 100644 index 0000000..7953a55 --- /dev/null +++ b/samples/zones.js @@ -0,0 +1,61 @@ +'use strict'; + +module.exports = { + "domain": [ + { "id": "daplie.me", "revokedAt": 0 } + , { "id": "oneal.daplie.me", "revokedAt": 0 } + , { "id": "aj.oneal.daplie.me", "revokedAt": 0 } + ] +, "records": [ + // zone daplie.me should be able to have some records on its own + { "zone": "daplie.me", "type": "A", "domain": "www.daplie.me" + , "tld": "me", "sld": "daplie", "sub": "www", "value": "23.228.168.108", "aname": "tardigrade.devices.daplie.me" } + + , { "zone": "daplie.me", "type": "CNAME", "domain": "email.daplie.me" + , "tld": "me", "sld": "daplie", "sub": "email", "value": "mailgun.org" } + + , { "zone": "daplie.me", "type": "ANAME", "domain": "tardigrade.devices.daplie.me", "device": "abcdef123" + , "tld": "me", "sld": "daplie", "sub": "tardigrade.devices", "value": "23.228.168.108" } + + // zone daplie.me can delegate oneal.daplie.me 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 + // to let someone exclusively "own" the root domain, but none of the children? + , { "zone": "daplie.me", "type": "NS", "domain": "oneal.daplie.me" + , "tld": "me", "sld": "daplie", "sub": "oneal", "value": "ns1.redirect-www.org" } + + , { "zone": "daplie.me", "type": "NS", "domain": "oneal.daplie.me" + , "tld": "me", "sld": "daplie", "sub": "oneal", "value": "ns2.redirect-www.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", "type": "A", "domain": "oneal.daplie.me" + , "tld": "daplie.me", "sld": "oneal", "sub": "", "value": "45.56.59.142", "aname": "leo.devices.oneal.daplie.me" } + + , { "zone": "oneal.daplie.me", "type": "CNAME", "domain": "www.oneal.daplie.me" + , "tld": "daplie.me", "sld": "oneal", "sub": "www", "value": "oneal.daplie.me" } + + , { "zone": "oneal.daplie.me", "type": "NS", "domain": "aj.oneal.daplie.me" + , "tld": "daplie.me", "sld": "oneal", "sub": "aj", "value": "ns1.redirect-www.org" } + + , { "zone": "oneal.daplie.me", "type": "NS", "domain": "aj.oneal.daplie.me" + , "tld": "daplie.me", "sld": "oneal", "sub": "aj", "value": "ns2.redirect-www.org" } + + // there can be a wildcard, to which a delegation is the exception + , { "zone": "oneal.daplie.me", "type": "A", "domain": "*.oneal.daplie.me" + , "tld": "daplie.me", "sld": "oneal", "sub": "*", "value": "45.56.59.142", "aname": "leo.devices.oneal.daplie.me" } + + // there can be an exception to the delegation + , { "zone": "oneal.daplie.me", "type": "A", "domain": "exception.aj.oneal.daplie.me" + , "tld": "daplie.me", "sld": "oneal", "sub": "exception.aj", "value": "45.56.59.142", "aname": "leo.devices.oneal.daplie.me" } + + + // + // aj.oneal.daplie.me + // + , { "zone": "aj.oneal.daplie.me", "type": "A", "domain": "aj.oneal.daplie.me" + , "tld": "oneal.daplie.me", "sld": "aj", "sub": "", "value": "45.56.59.142", "aname": "leo.devices.oneal.daplie.me" } + ] +} +;