njord-ns-server 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Makefile CHANGED
@@ -1,5 +1,3 @@
1
- NODEUNIT := ./node_modules/.bin/nodeunit
2
- BUNYAN := ./node_modules/.bin/bunyan
3
1
  NPM := $(shell which npm)
4
2
 
5
3
  .PHONY: setup
@@ -7,7 +5,5 @@ setup: $(NPM)
7
5
  $(NPM) install
8
6
 
9
7
  .PHONY: test
10
- test: $(NODEUNIT)
11
- $(NODEUNIT) test/*.test.js $(BUNYAN)
12
-
13
-
8
+ test: $(NPM)
9
+ $(NPM) test
package/README.md CHANGED
@@ -8,6 +8,20 @@ functionality that is in use today.
8
8
 
9
9
 
10
10
 
11
+ ## Requirements
12
+
13
+ Node.js 20 or newer.
14
+
15
+ ## Testing
16
+
17
+ ```
18
+ npm install
19
+ npm test
20
+ ```
21
+
22
+ The tests use the test runner that is built into Node.js, and query a running
23
+ server with the `dns` resolver of Node.js, so no other tools are needed.
24
+
11
25
  ## Creating a DNS Server
12
26
  ```javascript
13
27
  var named = require('./lib/index');
@@ -30,7 +44,7 @@ server.on('query', function(query) {
30
44
 
31
45
  node-named provides helper functions for creating DNS records.
32
46
  The records are available under 'named.record.NAME' where NAME is one
33
- of ['A', 'AAAA', 'CNAME', 'SOA', 'MX', 'NS', 'TXT, 'SRV']. It is important to
47
+ of ['A', 'AAAA', 'CNAME', 'SOA', 'MX', 'NS', 'PTR', 'TXT, 'SRV']. It is important to
34
48
  remember that these DNS records are not permanently added to the server.
35
49
  They only exist for the length of the particular request. After that, they are
36
50
  destroyed. This means you have to create your own lookup mechanism.
@@ -50,6 +64,7 @@ The following record types are supported
50
64
  * SOA (start of authority)
51
65
  * MX (mail server records)
52
66
  * NS (nameserver entries)
67
+ * PTR (reverse entries)
53
68
  * TXT (arbitrary text entries)
54
69
  * SRV (service discovery)
55
70
 
@@ -65,12 +80,69 @@ You can pass in an alternate logger if you wish. If you do not, then it will use
65
80
  bunyan by default. Your logger must expose the functions 'info', 'debug',
66
81
  'warn', 'trace', 'error', and 'notice'.
67
82
 
68
- ### TODO
83
+ ### Validating records
84
+
85
+ `record.valid()` tells you whether a record can be sent to a client, and
86
+ `record.invalid()` which of its properties can not:
87
+ ```javascript
88
+ var srv = new named.SRVRecord('sip example', 70000);
89
+ srv.valid(); // false
90
+ srv.invalid(); // [ 'target', 'port' ]
91
+ ```
92
+
93
+ ## TCP and zone transfers (AXFR)
94
+
95
+ A server listens on UDP with `listen`, and on TCP with `listenTcp`. Queries
96
+ that arrive over TCP are emitted and answered in the same way as the ones that
97
+ arrive over UDP, and `query.transport()` tells them apart.
98
+
99
+ A zone transfer is a query of the type `AXFR`, that is answered with all of
100
+ the records of the zone, between two copies of its SOA record. A transfer that
101
+ is too large for one DNS message is split over as many messages as it takes.
102
+ ```javascript
103
+ server.listen(9999, '127.0.0.1');
104
+ server.listenTcp(9999, '127.0.0.1');
105
+
106
+ server.on('query', function(query) {
107
+ if (query.type() === 'AXFR') {
108
+ if (query.transport() !== 'tcp' || query.name() !== 'example.com') {
109
+ query.setError('erefuse');
110
+ } else {
111
+ query.addAnswer('example.com', soa, 300);
112
+ // ... one addAnswer for each record of the zone ...
113
+ query.addAnswer('example.com', soa, 300);
114
+ }
115
+ }
116
+ server.send(query);
117
+ });
118
+ ```
119
+ See `examples/axfr.js` for a complete server. Anyone who can reach the TCP
120
+ port can ask for a transfer, so check `query._client.address` before you
121
+ answer one.
69
122
 
70
- * Better record validation
71
- * Create DNS client for query recursor
72
- * Add support for PTR records
73
- * Add support for TCP AXFR requests
123
+ ## Recursor
124
+
125
+ A recursor is a DNS client that answers a query by asking other DNS servers,
126
+ which are the ones of the system unless you name them. A lookup that fails
127
+ sets the response code of the query (`query.setError`), so the query can be
128
+ sent either way.
129
+ ```javascript
130
+ var recursor = named.createRecursor({servers: ['1.1.1.1', '8.8.8.8']});
131
+
132
+ server.on('query', function(query) {
133
+ recursor.resolve(query, function() {
134
+ server.send(query);
135
+ });
136
+ });
137
+ ```
138
+ The client can be used by itself as well:
139
+ ```javascript
140
+ recursor.lookup('example.com', 'MX').then(function(answers) {
141
+ // [ { record: MX { exchange: ..., priority: ... }, ttl: 60 }, ... ]
142
+ });
143
+ ```
144
+ The A, AAAA, CNAME, MX, NS, PTR, SOA, SRV and TXT records can be looked up.
145
+ See `examples/recursor.js`.
74
146
 
75
147
  ## Tell me even more...
76
148
 
@@ -85,3 +157,7 @@ basis).
85
157
 
86
158
  There are a few handy ways to lookup DNS records in node.
87
159
  https://github.com/LCMApps/dns-lookup-cache
160
+
161
+ ## Release notes
162
+
163
+ See [RELEASE.md](RELEASE.md) for what changed in each release.
package/RELEASE.md ADDED
@@ -0,0 +1,177 @@
1
+ # Njord NS Server 2.1.0
2
+
3
+ This release completes the TODO list of the project: PTR records, better
4
+ validation of records, queries over TCP including zone transfers (AXFR), and a
5
+ recursor that answers queries by asking other DNS servers.
6
+
7
+ Everything is an addition to 2.0.0. Existing servers keep working without
8
+ changes, with the exceptions listed under "Behaviour changes".
9
+
10
+ ## New features
11
+
12
+ ### PTR records
13
+
14
+ `named.PTRRecord(target)` is the answer to a reverse lookup, such as a query
15
+ for `1.2.0.192.in-addr.arpa`.
16
+
17
+ ### Record validation
18
+
19
+ - `record.valid()` checks that names are valid host or domain names, that
20
+ addresses are valid addresses, and that numbers are integers within the
21
+ range of their field.
22
+ - `record.invalid()` returns the names of the properties that hold invalid
23
+ data, for example `['target', 'port']`.
24
+ - `query.addAnswer` throws a `RangeError` for a TTL that is not an integer
25
+ from 0 to 2147483647.
26
+
27
+ ### TCP and zone transfers (AXFR)
28
+
29
+ - `server.listenTcp(port, [host], [onListen])` accepts queries over TCP, next
30
+ to or instead of the UDP socket of `server.listen`. `server.close()` closes
31
+ both.
32
+ - Queries that arrive over TCP are emitted with the same `query` event and
33
+ answered with the same `server.send`. `query.transport()` returns `'udp'` or
34
+ `'tcp'`.
35
+ - A zone transfer is a query of the type `AXFR`, answered with the SOA record
36
+ of the zone, its records, and the SOA record again. A response of more than
37
+ 65535 bytes is split over several DNS messages.
38
+ - Idle connections are closed after 30 seconds, which the `tcpTimeout` option
39
+ of `createServer` changes.
40
+
41
+ There is no access control for zone transfers: check `query._client.address`
42
+ before you answer one. See `examples/axfr.js`.
43
+
44
+ ### Recursor
45
+
46
+ - `named.createRecursor({servers, timeout, tries, ttl})` creates a DNS client
47
+ on top of the resolver of Node.js (c-ares). Without `servers` it asks the
48
+ DNS servers of the system.
49
+ - `recursor.resolve(query, [callback])` adds the answers that are found to a
50
+ query, or sets its response code when the lookup fails, so that the query
51
+ can be sent either way.
52
+ - `recursor.lookup(name, type)` is the client by itself, and returns a promise
53
+ for a list of records with their TTL.
54
+ - The A, AAAA, CNAME, MX, NS, PTR, SOA, SRV and TXT records can be looked up.
55
+
56
+ The resolver only reports the TTL of A and AAAA records. The other records get
57
+ the `ttl` of the recursor, which is 60 seconds by default. See
58
+ `examples/recursor.js`.
59
+
60
+ ### Response codes
61
+
62
+ `query.setError(name)` sets the response code of a query, with `'enoerr'`,
63
+ `'eformat'`, `'eserver'`, `'enoname'`, `'enotimp'` or `'erefuse'`.
64
+
65
+ ## Behaviour changes
66
+
67
+ - The target of a SRV record must be a domain name to be valid. It used to be
68
+ any text of up to 255 characters.
69
+ - The port of a SRV record must be from 1 to 65535 to be valid.
70
+ - An IPv6 address with a zone (`fe80::1%eth0`) is not valid.
71
+ - The TTL of a MX record must be at most 2147483647 to be valid.
72
+ - A TXT record of more than 255 bytes is valid, as it is sent as several
73
+ strings.
74
+ - Host names may end with a dot, and may be the root (`.`), which is what a
75
+ null MX record and a SRV record of an unavailable service use.
76
+ - `query.addAnswer` rejects a TTL that is negative, fractional or too large
77
+ when the answer is added. It used to fail when the response was sent.
78
+
79
+ ## Known limitations
80
+
81
+ - A response that is too large for UDP is sent as it is. It is not truncated
82
+ to make the client retry over TCP.
83
+ - `examples/cluster.js` does not run on Windows.
84
+
85
+ ## Testing
86
+
87
+ ```
88
+ npm install
89
+ npm test
90
+ ```
91
+
92
+ The 105 tests cover the new features with a live server: queries over TCP, a
93
+ zone transfer of 6000 records, and a recursor that asks a second server.
94
+
95
+ ---
96
+
97
+ # Njord NS Server 2.0.0
98
+
99
+ This release modernizes the codebase, updates every dependency, and makes
100
+ encoding a response several times faster. The exported API is unchanged, but
101
+ the minimum Node.js version is raised and a number of bugs are fixed in ways
102
+ that callers can observe, hence the new major version.
103
+
104
+ ## Requirements
105
+
106
+ Node.js 20 or newer (was 0.6).
107
+
108
+ ## Upgrading from 1.x
109
+
110
+ No code changes are needed for most users: the exports, method signatures and
111
+ events are the same, and `error.message()` is still a function. Check the list
112
+ of behaviour changes below if you relied on any of the old behaviour.
113
+
114
+ ## Dependencies
115
+
116
+ | Package | 1.0.1 | 2.0.0 |
117
+ |-----------|-------|---------|
118
+ | bunyan | 0.7.0 | 1.8.15 |
119
+ | ipaddr.js | 0.1.1 | 2.5.0 |
120
+ | nodeunit | 0.9.0 | removed |
121
+
122
+ The tests now use the test runner that is built into Node.js, so there are no
123
+ development dependencies, and `dig` is no longer needed to run them.
124
+
125
+ ## Performance
126
+
127
+ A response is written in a single pass into one buffer, instead of one small
128
+ buffer per field. Names are compressed with DNS pointers (RFC 1035 4.1.4), so
129
+ responses are smaller as well.
130
+
131
+ Parsing a query and encoding its response, measured on one machine:
132
+
133
+ | Response | 1.0.1 (ops/s) | 2.0.0 (ops/s) | Size (bytes) |
134
+ |----------|---------------|---------------|--------------|
135
+ | A | 53k | 236k | 62 → 48 |
136
+ | AAAA | 41k | 144k | 74 → 60 |
137
+ | SOA | 44k | 234k | 113 → 79 |
138
+ | 4 × MX | 21k | 118k | 208 → 112 |
139
+
140
+ This is a micro-benchmark of the protocol code, not of network throughput.
141
+
142
+ ## Behaviour changes
143
+
144
+ - `record.valid()` validates the record. It used to return `true` for
145
+ everything.
146
+ - `query.addAnswer(name, record, 0)` sends a TTL of 0. It used to be replaced
147
+ by the default of 5.
148
+ - `server.listen(port, '::1')` listens on IPv6. A UDP4 socket used to be
149
+ opened whatever the address was.
150
+ - The address family of the client of a query is the real one. It used to be
151
+ reported as `udp6` for every client.
152
+ - The counts in the header of a response describe what is sent. A query with
153
+ EDNS options used to get a response that announced an additional record
154
+ which was not there.
155
+ - The opcode of a query is decoded correctly, so `query.operation()` works for
156
+ status, notify and update messages.
157
+ - Names with a trailing dot, and the root name, are encoded correctly.
158
+ - TXT records longer than 255 bytes are split into several strings.
159
+ - The constructors of the records leave the options that are passed to them
160
+ alone.
161
+ - Invalid addresses and names are rejected when the response is encoded, which
162
+ is reported with the `uncaughtException` event of the server.
163
+
164
+ ## Other changes
165
+
166
+ - `lib/` is written with classes and in strict mode, and no longer leaks
167
+ global variables.
168
+ - The examples listen on port 9999, as their output always claimed.
169
+ - `examples/cluster.js` documents that it does not run on Windows, where
170
+ Node.js can not share a UDP socket between the workers of a cluster.
171
+
172
+ ## Testing
173
+
174
+ ```
175
+ npm install
176
+ npm test
177
+ ```
package/docs/index.md CHANGED
@@ -77,6 +77,8 @@ options is an object which may specify:
77
77
 
78
78
  - log: an optional bunyan logger
79
79
  - name: an optional name used to identify the server
80
+ - tcpTimeout: the number of milliseconds after which an idle TCP connection is
81
+ closed. Defaults to 30000, and 0 disables the timeout.
80
82
 
81
83
  Here is an example a named server listening on port 53
82
84
 
@@ -99,6 +101,17 @@ This function is asyncronous. Whenever the server is litening a `listen` event
99
101
  will be emitted. The last parameter `onListen` will be executed but not attached
100
102
  to the listen event, when the server is listening.
101
103
 
104
+ ### server.listenTcp(port, [host], [onListen])
105
+
106
+ Start accepting TCP connections on the specified `port` and `host`, next to
107
+ or instead of the UDP socket of `server.listen`. TCP is what clients use for
108
+ zone transfers (AXFR), and for answers that are too large for UDP.
109
+
110
+ Queries that arrive over TCP are emitted with the same `query` event, and are
111
+ answered with the same `server.send`. A client may send several queries over
112
+ one connection. A response of more than 65535 bytes is sent as several DNS
113
+ messages, which is how a large zone is transferred.
114
+
102
115
  ### server.send(queryResponse)
103
116
 
104
117
  Sends a `queryResponse` message. The queryResponse includes information about
@@ -111,8 +124,8 @@ will send a 'null-response' DNS message. This is the equivalent of an HTTP 404.
111
124
 
112
125
  ### server.close(onClose)
113
126
 
114
- Stops listening and closes the socket. `onClose` is an optional callback that
115
- will be attached to the underlying socket `close` event.
127
+ Stops listening, on UDP as well as on TCP, and closes the open TCP connections.
128
+ `onClose` is an optional callback that is called once everything is closed.
116
129
 
117
130
  ### Event: 'listening'
118
131
 
@@ -190,6 +203,22 @@ are responding with. In most cases this will be what the query.type() returns,
190
203
  but for instances like an 'A' or 'AAAA' request you may elect to respond with a
191
204
  CNAME record.
192
205
 
206
+ ### query.transport()
207
+
208
+ Returns `'udp'` or `'tcp'`, depending on how the query was received. A zone
209
+ transfer (a query of the type 'AXFR') should only be answered over TCP.
210
+
211
+ ### query.setError(name)
212
+
213
+ Sets the response code of the query. `name` is one of:
214
+
215
+ - `'enoerr'`: no error, which is the default
216
+ - `'eformat'`: the query could not be interpreted
217
+ - `'eserver'`: the server failed to process the query
218
+ - `'enoname'`: the name does not exist
219
+ - `'enotimp'`: the kind of query is not implemented
220
+ - `'erefuse'`: the server refuses to answer, for policy reasons
221
+
193
222
  ### query.operation()
194
223
 
195
224
  Returns the type of operation that the client is requesting. In almost all cases
@@ -202,6 +231,68 @@ Encodes the query and stores the results as a `buffer` in the query itself.
202
231
  This function should never need to be invoked, as the `server.send` function
203
232
  will automatically encode a query prior to being sent to the client.
204
233
 
234
+ ### query.encodeMessages(maxSize)
235
+
236
+ Encodes the query as an array of buffers, each of which is a DNS message of at
237
+ most `maxSize` bytes with as many of the answers as fit. This is what
238
+ `server.send` uses for a query that was received over TCP.
239
+
240
+ ## Class: named.Recursor
241
+
242
+ A DNS client that answers queries by asking other DNS servers. The lookups are
243
+ made with the resolver of Node.js, which is built on c-ares.
244
+
245
+ ### named.createRecursor([options])
246
+
247
+ options is an object which may specify:
248
+
249
+ - servers: an array with the addresses of the servers to ask, each with an
250
+ optional port (`'192.0.2.1'`, `'192.0.2.1:5353'`, `'[2001:db8::1]:53'`).
251
+ Defaults to the DNS servers of the system.
252
+ - timeout: the number of milliseconds to wait for an answer to each attempt
253
+ - tries: the number of attempts that are made for each lookup
254
+ - ttl: the ttl of the answers for which the server that was asked does not
255
+ report one, which it only does for A and AAAA records. Defaults to 60.
256
+
257
+ ### recursor.resolve(query, [callback])
258
+
259
+ Looks up the question of a `query`, and adds what is found to it as answers.
260
+ When the lookup fails the response code of the query is set instead: to
261
+ 'enoname' for a name that does not exist, to 'enotimp' for a type that can not
262
+ be looked up, to 'erefuse' when the other server refuses, and to 'eserver' for
263
+ everything else. In either case the query is ready to be sent.
264
+
265
+ server.on('query', function(query) {
266
+ recursor.resolve(query, function() {
267
+ server.send(query);
268
+ });
269
+ });
270
+
271
+ `callback` is called with an error (which is `null`, as failed lookups are
272
+ reported to the client) and the query. Without a callback a promise for the
273
+ query is returned.
274
+
275
+ ### recursor.lookup(name, type)
276
+
277
+ Looks up the records of a `type` for a `name`. Returns a promise for an array
278
+ of objects with a `record` (an instance of `named.Record`) and its `ttl`.
279
+ The promise is rejected with the error of the resolver, of which the `code` is
280
+ one of the error codes of the `dns` module of Node.js, such as 'ENOTFOUND' and
281
+ 'ENODATA'.
282
+
283
+ ### recursor.types()
284
+
285
+ Returns the types of record that can be looked up: A, AAAA, CNAME, MX, NS, PTR,
286
+ SOA, SRV and TXT.
287
+
288
+ ### recursor.servers()
289
+
290
+ Returns the addresses of the servers that are asked.
291
+
292
+ ### recursor.cancel()
293
+
294
+ Cancels the lookups that are in progress, which then fail.
295
+
205
296
 
206
297
  ## Records
207
298
 
@@ -243,6 +334,17 @@ Create an IPv6 resource record.
243
334
  Create an Alias record. When these records are sent to the client, the client
244
335
  will often make an additional request for the alias itself.
245
336
 
337
+ ### named.NSRecord(target)
338
+
339
+ Create a Name Server record. `target` is the name of a server that is
340
+ authoritative for the domain.
341
+
342
+ ### named.PTRRecord(target)
343
+
344
+ Create a Pointer record, which is the answer to a reverse lookup: a query for a
345
+ name such as `1.2.0.192.in-addr.arpa`. `target` is the name of the host that
346
+ has the address.
347
+
246
348
  ### named.MXRecord(exchange, options)
247
349
 
248
350
  Create a Mail Server record. A client making this request will often make an
@@ -269,14 +371,21 @@ Options:
269
371
  ### named.TXTRecord(target)
270
372
 
271
373
  Create a text resource record.
272
- `target` can be any text up to 500 bytes in length
374
+ `target` can be any text. Text of more than 255 bytes is sent as several
375
+ strings, which clients join again.
273
376
 
274
377
  ## Class: named.Record
275
378
 
276
379
  ### record.valid()
277
380
 
278
381
  This function will ensure that the data you used to create the record is in fact
279
- valid.
382
+ valid. Names must be valid host or domain names, addresses must be valid
383
+ addresses, and numbers must be integers within the range of their field.
384
+
385
+ ### record.invalid()
386
+
387
+ Returns an array with the names of the properties of the record that hold
388
+ invalid data, which is empty for a valid record.
280
389
 
281
390
  ## DnsError
282
391
 
package/examples/app.js CHANGED
@@ -1,15 +1,17 @@
1
- var named = require('../lib');
2
- var server = named.createServer();
1
+ 'use strict';
3
2
 
4
- server.listen(9999, '127.0.0.1', function() {
5
- console.log('DNS server started on port 9999');
6
- });
3
+ const named = require('../lib');
4
+
5
+ const PORT = 9999;
6
+ const server = named.createServer();
7
7
 
8
- console.log(named.SoaRecord);
8
+ server.listen(PORT, '127.0.0.1', () => {
9
+ console.log('DNS server started on port %d', PORT);
10
+ });
9
11
 
10
- server.on('query', function(query) {
11
- var domain = query.name();
12
- var record = new named.SOARecord(domain, {serial: 12345, ttl: 300});
12
+ server.on('query', (query) => {
13
+ const domain = query.name();
14
+ const record = new named.SOARecord(domain, {serial: 12345, ttl: 300});
13
15
  query.addAnswer(domain, record, 300);
14
16
  server.send(query);
15
17
  });
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A server that listens on UDP as well as on TCP, and that transfers its zone
5
+ * to the clients that ask for it (AXFR).
6
+ *
7
+ * You can test it like this:
8
+ *
9
+ * $ dig @127.0.0.1 -p 9999 www.example.com
10
+ * $ dig @127.0.0.1 -p 9999 example.com AXFR
11
+ */
12
+
13
+ const named = require('../lib');
14
+
15
+ const PORT = 9999;
16
+ const ZONE = 'example.com';
17
+ const TTL = 300;
18
+
19
+ const soa = new named.SOARecord('ns.' + ZONE, {serial: 2024010101});
20
+
21
+ // name -> records
22
+ const records = {
23
+ 'example.com': [
24
+ new named.NSRecord('ns.example.com'),
25
+ new named.MXRecord('smtp.example.com', {priority: 10})
26
+ ],
27
+ 'ns.example.com': [new named.ARecord('192.0.2.1')],
28
+ 'smtp.example.com': [new named.ARecord('192.0.2.2')],
29
+ 'www.example.com': [new named.CNAMERecord('example.com')],
30
+ '_sip._tcp.example.com': [new named.SRVRecord('sip.example.com', 5060)]
31
+ };
32
+
33
+ const server = named.createServer();
34
+
35
+ server.on('query', (query) => {
36
+ const domain = query.name();
37
+ const type = query.type();
38
+ console.log('DNS Query: (%s) %s over %s', type, domain, query.transport());
39
+
40
+ if (type === 'AXFR') {
41
+ // Zone transfers are only possible over TCP. This is also the place to
42
+ // check query._client.address: a zone should not be given to everyone.
43
+ if (query.transport() !== 'tcp' || domain !== ZONE) {
44
+ query.setError('erefuse');
45
+ } else {
46
+ // a transfer starts and ends with the SOA record of the zone
47
+ query.addAnswer(ZONE, soa, TTL);
48
+ for (const name of Object.keys(records)) {
49
+ for (const record of records[name])
50
+ query.addAnswer(name, record, TTL);
51
+ }
52
+ query.addAnswer(ZONE, soa, TTL);
53
+ }
54
+ } else if (type === 'SOA' && domain === ZONE) {
55
+ query.addAnswer(ZONE, soa, TTL);
56
+ } else if (records[domain] === undefined) {
57
+ query.setError('enoname');
58
+ } else {
59
+ for (const record of records[domain]) {
60
+ if (record._type === type)
61
+ query.addAnswer(domain, record, TTL);
62
+ }
63
+ }
64
+
65
+ server.send(query);
66
+ });
67
+
68
+ server.listen(PORT, '127.0.0.1', () => {
69
+ console.log('DNS server started on udp port %d', PORT);
70
+ });
71
+
72
+ server.listenTcp(PORT, '127.0.0.1', () => {
73
+ console.log('DNS server started on tcp port %d', PORT);
74
+ });