UNPKG

dnschain

Version:

A blockchain-based DNS + HTTPS server that fixes HTTPS security, and more!

275 lines (243 loc) 13.4 kB
### dnschain http://dnschain.net Copyright (c) 2014 okTurtles Foundation This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ### # TODO: go through 'TODO's! Packet = require('native-dns-packet') module.exports = (dnschain) -> # expose these into our namespace for k of dnschain.globals eval "var #{k} = dnschain.globals.#{k};" QTYPE_NAME = dns2.consts.QTYPE_TO_NAME NAME_QTYPE = dns2.consts.NAME_TO_QTYPE NAME_RCODE = dns2.consts.NAME_TO_RCODE RCODE_NAME = dns2.consts.RCODE_TO_NAME class DNSServer constructor: (@dnschain) -> @log = gNewLogger 'DNS' @log.debug "Loading DNSServer..." @method = gConf.get 'dns:oldDNSMethod' @rateLimiting = gConf.get 'rateLimiting:dns' # this is just for development testing of NODE_DNS method # dns.setServers ['8.8.8.8'] if @method is gConsts.oldDNS.NODE_DNS @log.warn "Using".bold.red, "oldDNSMethod = NODE_DNS".bold, "method is strongly discouraged!".bold.red if dns.getServers? blacklist = _.intersection ['127.0.0.1', '::1', 'localhost'], dns.getServers() if blacklist.length > 0 gErr "Cannot use NODE_DNS method when system DNS lists %j as a resolver! Would lead to infinite loop!", blacklist else gErr "Node's DNS module doesn't have 'getServers'. Please upgrade NodeJS." else if @method is gConsts.oldDNS.NO_OLD_DNS @log.warn "oldDNSMethod is set to refuse queries for traditional DNS!".bold else if @method is gConsts.oldDNS.NO_OLD_DNS_EVER @log.warn "oldDNSMethod is set to refuse *ALL* queries for traditional DNS (even if the blockchain wants us to)!".bold.red else if @method isnt gConsts.oldDNS.NATIVE_DNS gErr "No such oldDNSMethod: #{@method}" gFillWithRunningChecks @ start: -> @startCheck (cb) => @server = dns2.createServer() or gErr "dns2 create" @server.on 'socketError', (err) -> gErr err @server.on 'request', (req, res) => domain = req.question[0]?.name if domain domain = domain.split(".") if domain.length > 3 # if there are more than 3 parts to the domain, we use the last # letter of the fourth, and the full parts of the last three # This isn't perfect, especially because of: # https://publicsuffix.org/list/effective_tld_names.dat # # See: https://github.com/okTurtles/dnschain/issues/107 ! domain = [domain[-4..][0][-1..]].concat(domain[-3..]).join '.' else domain = domain[-3..].join '.' key = "dns-#{req.address.address}-#{domain}" @log.debug gLineInfo("creating bottleneck on: #{key}") limiter = gThrottle key, => new Bottleneck _.at(@rateLimiting, ['maxConcurrent', 'minTime', 'highWater', 'strategy'])... limiter.changePenalty(@rateLimiting.penalty).submit (@callback.bind @), req, res, null else @log.warn gLineInfo('received empty request!'), {req:req} # // end on 'request' @server.on 'listening', => @log.info 'started DNS', gConf.get 'dns' cb() @server.serve gConf.get('dns:port'), gConf.get('dns:host') shutdown: -> @shutdownCheck (cb) => if @server @server.on 'close', cb @server.close() else @log.warn gLineInfo '@server not defined!' cb() # (Notes on 'native-dns' version <=0.6.x, which I'd like to see changed.) # # Both `req` and `res` are of type `Packet` (the subclass, as explained next). # # The packet that's inside of 'native-dns' inherits from the one inside of 'native-dns-packet'. # It adds two extra fields (at this time of writing): # # - address: added by Server.prototype.handleMessage and the Packet subclass constructor # - _socket: added by the Packet subclass constructor in lib/packet.js # # `req` and `res` are both instances of this subclass of 'Packet'. # They also have the same 'question' field. # # See also: # - native-dns/lib/server.js # - native-dns/lib/packet.js # - native-dns-packet/packet.js # # Separately, there is a 'Request' class defined in 'native-dns/lib/client.js'. # Like 'Packet', it has a 'send' method. # To understand it see these functions in 'lib/pending.js': # # - SocketQueue.prototype._dequeue (sending) # - SocketQueue.prototype._onmessage (receiving) # # When you create a 'new Request' and send it, it will first create a 'new Packet' and copy # some of the values from the request into it, and then call 'send' on that. # Similarly, in receiving a reply from a Request instance, handle the 'message' event, which # will create a new Packet (the subclass, with the _socket field) from the received data. # # See also: # - native-dns/lib/client.js # - native-dns/lib/pending.js # # Ideally we want to be able to reuse the 'req' received here and pass it along # to oldDNSLookup without having to recreate or copy any information. # See: https://github.com/tjfontaine/node-dns/issues/69 # # Even more ideally we want to be able to simply pass along the raw data without having to parse it. # See: https://github.com/okTurtles/dnschain/issues/6 # callback: (req, res, cb) -> # answering multiple questions in a query appears to be problematic, # and few servers do it, so we only answer the first question: # https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query # At some point we may still want to support this though. q = req.question[qIdx=0] q.name = q.name.toLowerCase() ttl = Math.floor(Math.random() * 3600) + 30 # TODO: pick an appropriate TTL value! @log.debug "received question", q if (datastore = @dnschain.chainsTLDs[q.name.split('.').pop()]) @log.debug gLineInfo("resolving via #{datastore.name}..."), {domain:q.name, q:q} if not datastore.resources.key? @log.error gLineInfo "#{datastore.name} does not implement `key` resource!".bold return @sendErr(res, NAME_RCODE.SERVFAIL, cb) args = [datastore.name , "key", q.name, null, null, {}] # args conform to the datastore API resourceRequest = (cb) => datastore.resources.key.call datastore, args[2..]..., cb @dnschain.cache.resolveResource datastore, resourceRequest, JSON.stringify(args), (err, result) => if err? or !result @log.error gLineInfo("#{datastore.name} failed to resolve"), {err:err?.message, result:result, q:q} @sendErr res, null, cb else @log.debug gLineInfo("#{datastore.name} resolved query"), {q:q, d:q.name, result:result} if not (handler = datastore.dnsHandler[QTYPE_NAME[q.type]]) @log.warn gLineInfo("no such DNS handler!"), {datastore: datastore.name, q:q, type: QTYPE_NAME[q.type]} return @sendErr res, NAME_RCODE.NOTIMP, cb handler.call datastore, req, res, qIdx, result.data, (errCode) => try if errCode @sendErr res, errCode, cb else @sendRes res, cb catch e @log.error e.stack @log.error gLineInfo("exception in handler"), {q:q, result:result} return @sendErr res, NAME_RCODE.SERVFAIL, cb else if S(q.name).endsWith '.dns' res.answer.push gIP2type(q.name,ttl,QTYPE_NAME[q.type])(gConf.get 'dns:externalIP') @log.debug gLineInfo('cb|.dns'), {q:q, answer:res.answer} @sendRes res, cb else @log.debug gLineInfo("resolving #{q.name} via oldDNS"), {q:q} @dnschain.cache.resolveOldDNS req, (code, packet) => _.assign res, packet if code @sendErr res, code, cb else @sendRes res, cb # / end callback oldDNSLookup: (req, cb) -> res = new Packet() sig = "oldDNS{#{@method}}" q = req.question[0] filterRes = (p) -> _.pick p, ['edns_version', 'edns_options', 'edns', 'answer', 'authority', 'additional'] @log.debug {fn:sig+':start', q:q} if @method is gConsts.oldDNS.NATIVE_DNS success = false # TODO: retry in TCP-mode on truncated response (like `dig`) # See: https://github.com/tjfontaine/node-dns/issues/70 req2 = new dns2.Request question: q server : gConf.get 'dns:oldDNS' try_edns: q.type is NAME_QTYPE.ANY or req.edns? # 'answer' is a Packet subclass with the .address and ._socket fields req2.on 'message', (err, answer) => if err? @log.error gLineInfo("should not have an error here!"), {err:err?.message, answer:answer} req2.DNSErr ?= err else @log.debug gLineInfo('message'), {answer:answer} success = true res = answer req2.on 'error', (err={message:'unknown error'}) => @log.error gLineInfo('oldDNS lookup error'), {err:err?.message} req2.DNSErr = err req2.on 'timeout', (err={message:'timeout'}) => @log.warn gLineInfo('oldDNS timeout'), {err:err} req2.DNSErr = err req2.on 'end', => if success @log.debug gLineInfo('success!'), {q:q, res: _.omit(res, '_socket')} cb null, filterRes(res) else # TODO: this is noisy. # also make log output look good in journalctl # you can log IP with: res._socket.remote.address @log.warn gLineInfo('oldDNS lookup failed'), {q:q, err:req2.DNSErr} cb NAME_RCODE.SERVFAIL, filterRes(res) # @log.debug {fn:"beforesend", req:req2} req2.send() else if @method is gConsts.oldDNS.NODE_DNS dns.resolve q.name, QTYPE_NAME[q.type], (err, addrs) => if err @log.debug {fn:sig+':fail', q:q, err:err?.message} cb NAME_RCODE.SERVFAIL, filterRes(res) else # USING THIS METHOD IS DISCOURAGED BECAUSE IT DOESN'T # PROVIDE US WITH CORRECT TTL VALUES!! # TODO: pick an appropriate TTL value! ttl = Math.floor(Math.random() * 3600) + 30 res.answer.push (addrs.map gIP2type(q.name, ttl, QTYPE_NAME[q.type]))... @log.debug {fn:sig+':success', answer:res.answer, q:q.name} cb null, filterRes(res) else # refuse all such queries cb NAME_RCODE.REFUSED, res sendRes: (res, cb) -> try @log.debug gLineInfo("sending response!"), {res:_.omit(res, '_socket')} res.send() cb() catch e @log.error gLineInfo('error trying send response back!'), {msg:e.message, res:_.omit(res, '_socket'), stack:e.stack} cb e sendErr: (res, code=NAME_RCODE.SERVFAIL, cb) -> try res.header.rcode = code @log.debug gLineInfo(), {code:code, name:RCODE_NAME[code]} res.send() catch e @log.error gLineInfo('exception sending error back!'), e.stack cb() false # helps other functions pass back an error value