/
dns.coffee
274 lines (243 loc) · 13.1 KB
/
dns.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
###
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