WIP dns store
This commit is contained in:
parent
8131de4a08
commit
4734b5ac57
|
@ -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
|
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
|
from `whatever.john.daplie.me` back to the original nameserver in case the resolving
|
||||||
client makes intelligent assumptions and caching.
|
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
|
||||||
|
```
|
||||||
|
|
|
@ -169,6 +169,10 @@ cli.main(function (args, cli) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendEmptyResponse(query, nx) {
|
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 newAb;
|
||||||
var emptyResp = {
|
var emptyResp = {
|
||||||
header: {
|
header: {
|
||||||
|
|
253
lib/dns-store.js
253
lib/dns-store.js
|
@ -2,16 +2,259 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
var fs = require('fs');
|
|
||||||
|
|
||||||
module.exports.ask = function (query, cb) {
|
module.exports.ask = function (query, cb) {
|
||||||
};
|
};
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports.query = function (input, query, cb) {
|
var SUCCESS = 0;
|
||||||
process.nextTick(function () {
|
var NXDOMAIN = 3;
|
||||||
cb(new Error('No local lookup method for DNS records defined.'));
|
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) {
|
query.question.forEach(function (q) {
|
||||||
module.exports.ask(q);
|
module.exports.ask(q);
|
||||||
|
|
|
@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
;
|
Loading…
Reference in New Issue