forked from coolaj86/digd.js
		
	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 | ||||
| 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 | ||||
| ``` | ||||
|  | ||||
| @ -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: { | ||||
|  | ||||
							
								
								
									
										253
									
								
								lib/dns-store.js
									
									
									
									
									
								
							
							
						
						
									
										253
									
								
								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); | ||||
|  | ||||
							
								
								
									
										61
									
								
								samples/zones.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								samples/zones.js
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user