├── package.json
├── README.md
└── index.js
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dns-host",
3 | "version": "1.0.5",
4 | "description": "This is a simple, lightweight DNS server written in pure JavaScript with no external dependencies",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [
10 | "dns",
11 | "node",
12 | "javascript",
13 | "server"
14 | ],
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/O1dMate/dns-host"
18 | },
19 | "homepage": "https://github.com/O1dMate/dns-host#readme",
20 | "author": "o1dmate",
21 | "license": "MIT"
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # What is this?
2 |
3 | This is a simple, lightweight DNS server written in pure JavaScript with no external dependencies. This server allows you to receive and respond to DNS queries.
4 |
5 | The server supports capturing of requests for all common DNS record types (and more):
6 | - A
7 | - AAAA
8 | - NS
9 | - TXT
10 | - MX
11 | - CNAME
12 |
13 | The server supports sending responses for following record types:
14 | - A
15 | - AAAA
16 | - TXT
17 | - NS
18 |
19 |
20 |
21 | # Installation
22 | ```
23 | npm i dns-host
24 | ```
25 |
26 |
27 |
28 | # Usage
29 |
30 | 1. Install & import the library.
31 | 2. Create a new instance of the DNS Server.
32 | 3. Setup desired callbacks listeners (see below).
33 | 4. Start the server.
34 | 5. Respond to desired requests.
35 |
36 | There are 4 callbacks that you can setup for the server (all of which are optional):
37 | * `request` - Called when a DNS request is received. The processed data will be returned as an Object. You can respond to the DNS Query by returning a value at the end of this callback function.
38 | * `error` - Called whenever an error is thrown by the server. The error object is returned.
39 | * `start` - Called when the DNS server is started.
40 | * `stop` - Called when the DNS server is stopped.
41 |
42 | `Note`: Stopping the server then starting it again is totally fine and supported. All your previously setup callbacks will still work after the server has been stopped and started again.
43 |
44 |
45 |
46 | ## Listening for DNS Queries (All Record Types)
47 | ```javascript
48 | const DnsServer = require('dns-host');
49 | const dnsServer = new DnsServer();
50 |
51 | dnsServer.on('request', (data) => {
52 | console.log('Data:', data);
53 | // Examples:
54 | // Data: { domain: 'test.com', recordType: 'A', id: 12, fromIp: '1.2.3.4' }
55 | // Data: { domain: 'test.com', recordType: 'AAAA', id: 13, fromIp: '1.2.3.4' }
56 | // Data: { domain: 'random.com', recordType: 'MX', id: 14, fromIp: '1.2.3.4' }
57 | // Data: { domain: 'example.com', recordType: 'NS', id: 15, fromIp: '1.2.3.4' }
58 |
59 | return '1.1.1.1';
60 | });
61 |
62 | dnsServer.on('error', (err) => {
63 | console.log('An Error Occurred:', err);
64 | });
65 |
66 | dnsServer.on('start', () => {
67 | console.log('DNS server started');
68 | });
69 |
70 | dnsServer.on('stop', () => {
71 | console.log('DNS server stopped');
72 | });
73 |
74 | dnsServer.start();
75 |
76 | // This will stop the server.
77 | // dnsServer.stop();
78 | ```
79 |
80 |
81 |
82 | ## Responding to DNS Queries (A Records)
83 | ```javascript
84 | dnsServer.on('request', (data) => {
85 | console.log('Data:', data);
86 | // Example:
87 | // Data: { domain: 'test.com', recordType: 'A', id: 12, fromIp: '1.2.3.4' }
88 |
89 | // Respond with a single IPv4 Address
90 | return '1.2.3.4';
91 | });
92 | ```
93 |
94 | ```javascript
95 | dnsServer.on('request', (data) => {
96 | console.log('Data:', data);
97 | // Example:
98 | // Data: { domain: 'example.com', recordType: 'A', id: 24, fromIp: '1.2.3.4' }
99 |
100 | // Respond with a list of IPv4 Addresses
101 | return ['1.2.3.4', '5.6.7.8'];
102 | });
103 | ```
104 |
105 |
106 |
107 | ## Responding to DNS Queries (AAAA Records)
108 | ```javascript
109 | dnsServer.on('request', (data) => {
110 | console.log('Data:', data);
111 | // Example:
112 | // Data: { domain: 'example.com', recordType: 'AAAA', id: 12, fromIp: '1.2.3.4' }
113 |
114 | // Respond with a single IPv6 Address
115 | return '2001:db8:1111:2222:3333::51';
116 | });
117 | ```
118 |
119 | ```javascript
120 | dnsServer.on('request', (data) => {
121 | console.log('Data:', data);
122 | // Example:
123 | // Data: { domain: 'example.com', recordType: 'AAAA', id: 24, fromIp: '1.2.3.4' }
124 |
125 | // Respond with a list of IPv6 Addresses
126 | return ['2001:db8:1111:2222:3333::51', '::1', '2001:db8::ff00:42:8329'];
127 | });
128 | ```
129 |
130 |
131 |
132 | ## Responding to DNS Queries (TXT Records)
133 | ```javascript
134 | dnsServer.on('request', (data) => {
135 | console.log('Data:', data);
136 | // Example:
137 | // Data: { domain: 'example.com', recordType: 'TXT', id: 12, fromIp: '1.2.3.4' }
138 |
139 | // Respond with a single string
140 | return 'Test String 1';
141 | });
142 | ```
143 |
144 | ```javascript
145 | dnsServer.on('request', (data) => {
146 | console.log('Data:', data);
147 | // Example:
148 | // Data: { domain: 'example.com', recordType: 'TXT', id: 24, fromIp: '1.2.3.4' }
149 |
150 | // Respond with a list of strings
151 | return ['Test String 1', 'Chars: 1234567890!@#$%^&*()_+-=[]'];
152 | });
153 | ```
154 |
155 |
156 |
157 | ## Responding to DNS Queries (NS Records)
158 | ```javascript
159 | dnsServer.on('request', (data) => {
160 | console.log('Data:', data);
161 | // Example:
162 | // Data: { domain: 'example.com', recordType: 'NS', id: 12, fromIp: '1.2.3.4' }
163 |
164 | // Respond with a single string
165 | return 'ns1.example.com';
166 | });
167 | ```
168 |
169 | ```javascript
170 | dnsServer.on('request', (data) => {
171 | console.log('Data:', data);
172 | // Example:
173 | // Data: { domain: 'example.com', recordType: 'NS', id: 24, fromIp: '1.2.3.4' }
174 |
175 | // Respond with a list of strings
176 | return ['ns1.example.com', 'ns2.example.com'];
177 | });
178 | ```
179 |
180 |
181 |
182 | # Options
183 | * `importantRecordTypes` - The DNS request must be one of these types otherwise it will not call the `request` callback. Good if you only care about certain record types. Default value is that all received queries will call the `request` callback.
184 |
185 | ```javascript
186 | const DnsServer = require('dns-host');
187 |
188 | const dnsServer = new DnsServer({
189 | importantRecordTypes: ['A', 'AAAA']
190 | /*
191 | - Only 'A' and 'AAAA' record types will call the 'request' callback.
192 | - If any other record types are requested, the callback will NOT be called.
193 | */
194 | });
195 |
196 | dnsServer.on('request', (data) => {
197 | console.log('Data:', data);
198 | // Examples:
199 | // Data: { domain: 'test.com', recordType: 'A', id: 12, fromIp: '1.2.3.4' }
200 | // Data: { domain: 'test.com', recordType: 'AAAA', id: 13, fromIp: '1.2.3.4' }
201 |
202 | return '1.2.3.4';
203 | });
204 | ```
205 |
206 |
207 | * `localhostOnly` - The DNS server will listen only on localhost (`127.0.0.1`) instead of on all interfaces (`0.0.0.0`). Default value is to listen on all interfaces (`0.0.0.0`).
208 |
209 | ```javascript
210 | const DnsServer = require('dns-host');
211 |
212 | const dnsServer = new DnsServer({
213 | localhostOnly: true
214 | });
215 | ```
216 |
217 |
218 | * `extendedMode` - Return all the decoded information from the DNS header in the 'request' callback. Default value is non-extended mode.
219 |
220 | ```javascript
221 | const DnsServer = require('dns-host');
222 |
223 | const dnsServer = new DnsServer({
224 | extendedMode: true
225 | });
226 |
227 | dnsServer.on('request', (data) => {
228 | console.log('Data:', data);
229 | // Example:
230 | /*
231 | Data: {
232 | domain: 'example.com',
233 | recordType: 'A',
234 | id: 13,
235 | recordClass: 1,
236 | requestFlags: {
237 | QR: 0,
238 | Opcode: 0,
239 | AA: 0,
240 | TC: 0,
241 | RD: 1,
242 | RA: 0,
243 | Z: 0,
244 | AD: 0,
245 | CD: 0,
246 | Rcode: 0
247 | },
248 | totalQuestions: 1,
249 | totalAnswers: 0,
250 | totalAuthority: 0,
251 | totalAdditional: 0,
252 | fromIp: '127.0.0.1'
253 | }
254 | */
255 |
256 | return '1.2.3.4';
257 | });
258 | ```
259 |
260 |
261 |
262 | # How can I test it?
263 |
264 | You can use the `nslookup` tool that is built into windows to test all of the of the functionality of this package.
265 |
266 | You can perform DNS queries in `nslookup` using the following commands in the command prompt:
267 |
268 | ```bash
269 | nslookup # Open `nslookup`
270 | server 127.0.0.1 # Set the IP of the DNS server
271 | set type= # Specify what record type you want to retrieve
272 | example.com # Enter a domain you want to retrieve the record for.
273 | ```
274 | - Where `` is (a, aaaa, txt, ns, mx, etc...)
275 |
276 |
277 |
278 |
279 | # Issues & TODO
280 | * No support for DNS over HTTPS.
281 | * No length enforcement. If a large payload is given for a TXT record, or too many IP addresses are returned for an A or AAAA record, the response will be received incorrectly.
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const dgram = require('dgram');
2 | const net = require('net');
3 | const isIPv4 = net.isIPv4;
4 | const isIPv6 = net.isIPv6;
5 |
6 | const RECORD_ID_TO_TYPE_LOOKUP = {
7 | 1: 'A',
8 | 2: 'NS',
9 | 3: 'MD',
10 | 4: 'MF',
11 | 5: 'CNAME',
12 | 6: 'SOA',
13 | 7: 'MB',
14 | 8: 'MG',
15 | 9: 'MR',
16 | 10: 'NULL',
17 | 11: 'WKS',
18 | 12: 'PTR',
19 | 13: 'HINFO',
20 | 14: 'MINFO',
21 | 15: 'MX',
22 | 16: 'TXT',
23 | 17: 'RP',
24 | 18: 'AFSDB',
25 | 19: 'X25',
26 | 20: 'ISDN',
27 | 21: 'RT',
28 | 22: 'NSAP',
29 | 23: 'NSAP-PTR',
30 | 24: 'SIG',
31 | 25: 'KEY',
32 | 26: 'PX',
33 | 27: 'GPOS',
34 | 28: 'AAAA',
35 | };
36 |
37 | const RECORD_TYPE_TO_ID_LOOKUP = {};
38 |
39 | const expandIpv6Address = (ipAddress) => {
40 | let pieces = ipAddress.split(':');
41 |
42 | if (pieces.length < 8) {
43 | let newPieces = [];
44 | let handledEmptyPiece = false;
45 | let indexToInsertAt = -1;
46 |
47 | // Determine where the double colon (::) was and stored that location so the zeros can be added back in.
48 | for (let i = 0; i < pieces.length; ++i) {
49 | if (!handledEmptyPiece && pieces[i] === '') {
50 | indexToInsertAt = i;
51 | handledEmptyPiece = true;
52 | newPieces.push('0');
53 | }
54 |
55 | if (pieces[i] !== '') newPieces.push(pieces[i]);
56 | }
57 |
58 | // Add the inbetween zeros that were removed.
59 | while (newPieces.length < 8) {
60 | newPieces.splice(indexToInsertAt, 0, '0');
61 | }
62 |
63 | pieces = newPieces;
64 | }
65 |
66 | // Ensure each piece of the address contains 4 Hex chars
67 | return pieces.map(currentPiece => currentPiece.padStart(4, '0')).join(':');
68 | }
69 |
70 |
71 | const decodesFlagsAndCodes = (flagsAndCodes) => {
72 | if (!Number.isInteger(parseInt(flagsAndCodes))) throw new Error("DNS Decode Failed: Flags & Codes not a valid Integer");
73 |
74 | return {
75 | QR: (flagsAndCodes & 32768) >> 15,
76 | Opcode: (flagsAndCodes & (16384 + 8192 + 4096 + 2048)) >> 11,
77 | AA: (flagsAndCodes & 1024) >> 10,
78 | TC: (flagsAndCodes & 512) >> 9,
79 | RD: (flagsAndCodes & 256) >> 8,
80 | RA: (flagsAndCodes & 128) >> 7,
81 | Z: (flagsAndCodes & 64) >> 6,
82 | AD: (flagsAndCodes & 32) >> 5,
83 | CD: (flagsAndCodes & 16) >> 4,
84 | Rcode: (flagsAndCodes & (8 + 4 + 2 + 1)) >> 0,
85 | };
86 | }
87 |
88 | // rawMessageData = Uint8Array of the DNS request
89 | const decodeRequest = (rawMessageData, extendedMode) => {
90 | if (rawMessageData.length < 12) throw new Error("DNS Decode Failed: Request not long enough < 8 bytes");
91 |
92 | // Convert from Uint8Array to standard Array
93 | rawMessageData = Array.from(rawMessageData);
94 |
95 | // Identification = 2 Bytes
96 | let idNumber = (rawMessageData.shift() << 8) | rawMessageData.shift();
97 |
98 | // Flags & Codes = 2 Bytes
99 | let flagsAndCodes = (rawMessageData.shift() << 8) | rawMessageData.shift();
100 | flagsAndCodes = decodesFlagsAndCodes(flagsAndCodes);
101 |
102 | // Total Questions = 2 Bytes
103 | let totalQuestions = (rawMessageData.shift() << 8) | rawMessageData.shift();
104 |
105 | // Total Answers RRs = 2 Bytes
106 | let totalAnswers = (rawMessageData.shift() << 8) | rawMessageData.shift();
107 |
108 | // Total Authority RRs = 2 Bytes
109 | let totalAuthority = (rawMessageData.shift() << 8) | rawMessageData.shift();
110 |
111 | // Total Additional RRs = 2 Bytes
112 | let totalAdditional = (rawMessageData.shift() << 8) | rawMessageData.shift();
113 |
114 | // List of domains in the question section and the record type.
115 | let domainList = [];
116 |
117 | for (let questionNumber = 0; questionNumber < totalQuestions; ++questionNumber) {
118 | let currentDomain = [];
119 |
120 | // Get the length of the next piece of the domain
121 | let lengthOfNextPiece = rawMessageData.shift();
122 |
123 | while (rawMessageData.length > lengthOfNextPiece) {
124 | for (let i = 0; i < lengthOfNextPiece; ++i) {
125 | currentDomain.push(String.fromCharCode(rawMessageData[i]));
126 | }
127 |
128 | // Remove the processed information
129 | rawMessageData = rawMessageData.slice(lengthOfNextPiece);
130 |
131 | // Get the length of the next piece of the domain
132 | lengthOfNextPiece = rawMessageData.shift();
133 |
134 | // Processing of the domain is done, exit the loop
135 | if (lengthOfNextPiece === 0) break;
136 | // There is more of the domain to process
137 | else currentDomain.push('.');
138 | }
139 |
140 | if (rawMessageData.length < 4) throw new Error(`DNS Decode Failed: Question section not long enough < 4 bytes for "${currentDomain.join('')}`);
141 |
142 | // Record Type = 2 Bytes
143 | let recordType = (rawMessageData.shift() << 8) | rawMessageData.shift();
144 |
145 | // Record Class = 2 Bytes
146 | let recordClass = (rawMessageData.shift() << 8) | rawMessageData.shift();
147 |
148 | let domainData = {
149 | domain: currentDomain.join(''),
150 | recordType: RECORD_ID_TO_TYPE_LOOKUP.hasOwnProperty(recordType) ? RECORD_ID_TO_TYPE_LOOKUP[recordType] : 'N/A',
151 | id: idNumber,
152 | }
153 |
154 | if (extendedMode) {
155 | domainData.recordClass = recordClass;
156 | domainData.requestFlags = flagsAndCodes;
157 | domainData.totalQuestions = totalQuestions;
158 | domainData.totalAnswers = totalAnswers;
159 | domainData.totalAuthority = totalAuthority;
160 | domainData.totalAdditional = totalAdditional;
161 | }
162 |
163 | domainList.push(domainData);
164 | }
165 |
166 | return domainList;
167 | }
168 |
169 | const genericHeader = (id, answers, domain, recordType) => {
170 | let responseBuffer = '';
171 |
172 | // Transaction ID (2 Bytes)
173 | responseBuffer += id.toString(16).padStart(4, '0');
174 |
175 | // Flags (Standard Query Response settings = 2 Bytes [0x8180])
176 | responseBuffer += '8180';
177 |
178 | // Questions (2 Bytes)
179 | responseBuffer += '0001';
180 |
181 | // Answers (2 Bytes)
182 | responseBuffer += '00' + answers.toString(16).padStart(2, '0');
183 |
184 | // Authority RRs (2 Bytes [0x0000]) & Additional RRs (2 Bytes [0x0000])
185 | responseBuffer += '00000000';
186 |
187 | // Queries SECTION (Assuming only 1 since it's most common)
188 | domain.split('.').forEach(partOfDomain => {
189 | // Append the length of the next piece of the domain
190 | responseBuffer += partOfDomain.length.toString(16).padStart(2, '0');
191 |
192 | // Append the domain
193 | partOfDomain.split('').forEach(char => {
194 | responseBuffer += char.charCodeAt().toString(16).padStart(2, '0');
195 | });
196 | });
197 |
198 | // Null Terminator for domain-name
199 | responseBuffer += '00';
200 |
201 | // Record Type
202 | responseBuffer += '00' + RECORD_TYPE_TO_ID_LOOKUP[recordType].toString(16).padStart(2, '0');
203 |
204 | // Class (IN = 0x0001)
205 | responseBuffer += '0001';
206 |
207 | return responseBuffer;
208 | }
209 |
210 | const construct_A_Record_Response = (requestData, responseIpList) => {
211 | let responseBuffer = genericHeader(requestData.id, responseIpList.length, requestData.domain, 'A');
212 |
213 | // Answers SECTION
214 | responseIpList.forEach(responseIp => {
215 | // Name
216 | responseBuffer += 'c00c';
217 |
218 | // Record Type (A)
219 | responseBuffer += '00' + RECORD_TYPE_TO_ID_LOOKUP['A'].toString(16).padStart(2, '0');
220 |
221 | // Class (IN = 0x0001)
222 | responseBuffer += '0001';
223 |
224 | // TTL - Time to Live (4 Bytes)
225 | responseBuffer += '00000015';
226 |
227 | // Data Length
228 | responseBuffer += '0004';
229 |
230 | responseIp.split('.').forEach(x => {
231 | responseBuffer += parseInt(x).toString(16).padStart(2, '0');
232 | });
233 | })
234 |
235 | return Buffer.from(responseBuffer, 'hex');
236 | }
237 |
238 | const construct_AAAA_Record_Response = (requestData, responseIpList) => {
239 | let responseBuffer = genericHeader(requestData.id, responseIpList.length, requestData.domain, 'AAAA');
240 |
241 | // Answers SECTION
242 | responseIpList.forEach(responseIp => {
243 | // Name
244 | responseBuffer += 'c00c';
245 |
246 | // Record Type (AAAA)
247 | responseBuffer += '00' + RECORD_TYPE_TO_ID_LOOKUP['AAAA'].toString(16).padStart(2, '0');
248 |
249 | // Class (IN = 0x0001)
250 | responseBuffer += '0001';
251 |
252 | // TTL - Time to Live (4 Bytes)
253 | responseBuffer += '00000015';
254 |
255 | // Data Length
256 | responseBuffer += '0010';
257 |
258 | expandIpv6Address(responseIp).split(':').join('').match(/.{1,2}/g).forEach(x => {
259 | responseBuffer += x;
260 | });
261 | })
262 |
263 | return Buffer.from(responseBuffer, 'hex');
264 | }
265 |
266 | const construct_TXT_Record_Response = (requestData, responseTextList) => {
267 | let responseBuffer = genericHeader(requestData.id, responseTextList.length, requestData.domain, 'TXT');
268 |
269 | // Answers SECTION
270 | responseTextList.forEach(responseText => {
271 | // Name
272 | responseBuffer += 'c00c';
273 |
274 | // Record Type (TXT)
275 | responseBuffer += '00' + RECORD_TYPE_TO_ID_LOOKUP['TXT'].toString(16).padStart(2, '0');
276 |
277 | // Class (IN = 0x0001)
278 | responseBuffer += '0001';
279 |
280 | // TTL - Time to Live (4 Bytes)
281 | responseBuffer += '00000015';
282 |
283 | // Data Length (2 Bytes)
284 | responseBuffer += (responseText.length + 1).toString(16).padStart(4, '0');
285 |
286 | // TXT Length (1 Byte)
287 | responseBuffer += responseText.length.toString(16).padStart(2, '0');
288 |
289 | responseText.split('').forEach(char => {
290 | responseBuffer += char.charCodeAt().toString(16).padStart(2, '0');
291 | });
292 | })
293 |
294 | return Buffer.from(responseBuffer, 'hex');
295 | }
296 |
297 | const construct_NS_Record_Response = (requestData, responseServerList) => {
298 | let responseBuffer = genericHeader(requestData.id, responseServerList.length, requestData.domain, 'NS');
299 |
300 | // Answers SECTION
301 | responseServerList.forEach(responseServer => {
302 | // Name
303 | responseBuffer += 'c00c';
304 |
305 | // Record Type (NS)
306 | responseBuffer += '00' + RECORD_TYPE_TO_ID_LOOKUP['NS'].toString(16).padStart(2, '0');
307 |
308 | // Class (IN = 0x0001)
309 | responseBuffer += '0001';
310 |
311 | // TTL - Time to Live (4 Bytes)
312 | responseBuffer += '00000015';
313 |
314 | // Data Length (2 Bytes)
315 | responseBuffer += (responseServer.length + 3).toString(16).padStart(4, '0');
316 |
317 | // Append the length of sub-domain
318 | responseBuffer += responseServer.length.toString(16).padStart(2, '0');
319 |
320 | responseServer.split('').forEach(char => {
321 | responseBuffer += char.charCodeAt().toString(16).padStart(2, '0');
322 | });
323 |
324 | // End of Answer
325 | responseBuffer += 'c00c';
326 | })
327 |
328 | return Buffer.from(responseBuffer, 'hex');
329 | }
330 |
331 | let SERVER_SOCKET = null;
332 | let IMPORTANT_RECORD_TYPES = null;
333 | let EXTENDED_MODE = null;
334 | let SERVER_PORT = null;
335 | let LOCAL_HOST_ONLY = null;
336 | let CALLBACK_ON_ERROR = null;
337 | let CALLBACK_ON_REQUEST = null;
338 | let CALLBACK_ON_START = null;
339 | let CALLBACK_ON_STOP = null;
340 |
341 | class DnsServer {
342 | constructor({ customPort, extendedMode, importantRecordTypes, localhostOnly } = { customPort: null, extendedMode: false, importantRecordTypes: false, localhostOnly: false }) {
343 | EXTENDED_MODE = !!extendedMode;
344 |
345 | LOCAL_HOST_ONLY = !!localhostOnly;
346 |
347 | if (importantRecordTypes && Array.isArray(importantRecordTypes)) {
348 | IMPORTANT_RECORD_TYPES = new Map();
349 |
350 | importantRecordTypes.forEach(x => {
351 | if (typeof (x) === 'string') IMPORTANT_RECORD_TYPES.set(x, true);
352 | });
353 | }
354 |
355 | if (!customPort) {
356 | SERVER_PORT = 53;
357 | } else if (!Number.isInteger(customPort)) {
358 | throw new Error('Custom Port is not a valid Integer');
359 | } else if (Number.isInteger(customPort) && (customPort < 1 || customPort > 65535)) {
360 | throw new Error('Custom Port must be > 0 and < 65535');
361 | } else {
362 | SERVER_PORT = customPort;
363 | }
364 | }
365 |
366 | on(onType, callback) {
367 | if (!callback || typeof (callback) !== 'function') throw new Error('Callback Must be a function');
368 |
369 | if (onType === 'error') {
370 | CALLBACK_ON_ERROR = callback;
371 | } else if (onType === 'request') {
372 | CALLBACK_ON_REQUEST = callback;
373 | } else if (onType === 'start') {
374 | CALLBACK_ON_START = callback;
375 | } else if (onType === 'stop') {
376 | CALLBACK_ON_STOP = callback;
377 | } else return;
378 | }
379 |
380 | start() {
381 | try {
382 | SERVER_SOCKET = dgram.createSocket('udp4');
383 |
384 | SERVER_SOCKET.on('message', async (message, messageInfo) => {
385 | try {
386 | let dnsRequestData = decodeRequest(Uint8Array.from(Buffer.from(message, 'utf8')), EXTENDED_MODE);
387 |
388 | dnsRequestData = dnsRequestData.filter(request => {
389 | request.fromIp = messageInfo.address.toString();
390 |
391 | if (!IMPORTANT_RECORD_TYPES) return true;
392 |
393 | if (IMPORTANT_RECORD_TYPES && IMPORTANT_RECORD_TYPES.get(request.recordType)) {
394 | return true;
395 | }
396 | return false;
397 | });
398 |
399 | if (dnsRequestData.length < 1) return;
400 |
401 | if (CALLBACK_ON_REQUEST && typeof (CALLBACK_ON_REQUEST) === 'function') {
402 | let domain = dnsRequestData[0].domain;
403 | let id = dnsRequestData[0].id;
404 | let recordType = dnsRequestData[0].recordType;
405 | let responseData;
406 |
407 | if (CALLBACK_ON_REQUEST.constructor.name === 'AsyncFunction') {
408 | responseData = await CALLBACK_ON_REQUEST(dnsRequestData[0]);
409 | } else {
410 | responseData = CALLBACK_ON_REQUEST(dnsRequestData[0]);
411 | }
412 |
413 | if (!responseData) return;
414 |
415 | let dnsResponseBuffer;
416 |
417 | if (dnsRequestData[0].recordType === 'A') {
418 | let singleIp = isIPv4(responseData);
419 | let listOfIps = Array.isArray(responseData) ? responseData.map(x => isIPv4(x)).reduce((acum, cur) => acum && cur, true) : false;
420 |
421 | if (singleIp) dnsResponseBuffer = construct_A_Record_Response(dnsRequestData[0], [responseData]);
422 | else if (listOfIps) dnsResponseBuffer = construct_A_Record_Response(dnsRequestData[0], responseData);
423 | else throw new Error(`DNS Response Error: Response Data is not a valid IPv4 address or list of addresses for (Domain: ${domain}, RecordType: ${recordType}, ID: ${id}).\nResponse provided: ${Array.isArray(responseData) ? JSON.stringify(responseData) : responseData}`,);
424 | } else if (dnsRequestData[0].recordType === 'AAAA') {
425 | let singleIp = isIPv6(responseData);
426 | let listOfIps = Array.isArray(responseData) ? responseData.map(x => isIPv6(x)).reduce((acum, cur) => acum && cur, true) : false;
427 |
428 | if (singleIp) dnsResponseBuffer = construct_AAAA_Record_Response(dnsRequestData[0], [responseData]);
429 | else if (listOfIps) dnsResponseBuffer = construct_AAAA_Record_Response(dnsRequestData[0], responseData);
430 | else throw new Error(`DNS Response Error: Response Data is not a valid IPv6 address or list of addresses for (Domain: ${domain}, RecordType: ${recordType}, ID: ${id}).\nResponse provided: ${Array.isArray(responseData) ? JSON.stringify(responseData) : responseData}`,);
431 | } else if (dnsRequestData[0].recordType === 'TXT') {
432 | let singleText = typeof (responseData) === 'string';
433 | let listOfTexts = Array.isArray(responseData) ? responseData.map(x => typeof (x) === 'string').reduce((acum, cur) => acum && cur, true) : false;
434 |
435 | if (singleText) dnsResponseBuffer = construct_TXT_Record_Response(dnsRequestData[0], [responseData]);
436 | else if (listOfTexts) dnsResponseBuffer = construct_TXT_Record_Response(dnsRequestData[0], responseData);
437 | else throw new Error(`DNS Response Error: Response Data is not a valid string or list of strings for (Domain: ${domain}, RecordType: ${recordType}, ID: ${id})`,);
438 | } else if (dnsRequestData[0].recordType === 'NS') {
439 | let singleSubdomain = typeof (responseData) === 'string';
440 | let listOfSubdomains = Array.isArray(responseData) ? responseData.map(x => typeof (x) === 'string').reduce((acum, cur) => acum && cur, true) : false;
441 |
442 | if (singleSubdomain) dnsResponseBuffer = construct_NS_Record_Response(dnsRequestData[0], [responseData]);
443 | else if (listOfSubdomains) dnsResponseBuffer = construct_NS_Record_Response(dnsRequestData[0], responseData);
444 | else throw new Error(`DNS Response Error: Response Data is not a valid sub-domain or list of sub-domains for (Domain: ${domain}, RecordType: ${recordType}, ID: ${id})`,);
445 | }
446 |
447 | if (dnsResponseBuffer) {
448 | SERVER_SOCKET.send(dnsResponseBuffer, messageInfo.port, messageInfo.address, (err) => {
449 | if (err) throw new Error('Error while sending DNS Response');
450 | });
451 | }
452 | }
453 | } catch (err) {
454 | if (CALLBACK_ON_ERROR && typeof (CALLBACK_ON_ERROR) === 'function') {
455 | CALLBACK_ON_ERROR(err);
456 | }
457 | }
458 | });
459 |
460 | SERVER_SOCKET.on('listening', () => {
461 | if (CALLBACK_ON_START && typeof (CALLBACK_ON_START) === 'function') {
462 | CALLBACK_ON_START();
463 | }
464 | });
465 |
466 | SERVER_SOCKET.on('error', (err) => {
467 | if (CALLBACK_ON_ERROR && typeof (CALLBACK_ON_ERROR) === 'function') {
468 | CALLBACK_ON_ERROR(err);
469 | }
470 | });
471 |
472 | SERVER_SOCKET.bind({
473 | port: SERVER_PORT,
474 | address: LOCAL_HOST_ONLY ? '127.0.0.1' : '0.0.0.0'
475 | });
476 | } catch (err) {
477 | if (CALLBACK_ON_ERROR && typeof (CALLBACK_ON_ERROR) === 'function') {
478 | CALLBACK_ON_ERROR(err);
479 | }
480 | }
481 | }
482 |
483 | stop() {
484 | try {
485 | SERVER_SOCKET.close(() => {
486 | if (CALLBACK_ON_STOP && typeof (CALLBACK_ON_STOP) === 'function') {
487 | CALLBACK_ON_STOP();
488 | }
489 | });
490 | SERVER_SOCKET = null;
491 | } catch (err) {
492 | if (CALLBACK_ON_ERROR && typeof (CALLBACK_ON_ERROR) === 'function') {
493 | CALLBACK_ON_ERROR(err);
494 | }
495 | }
496 | }
497 | }
498 |
499 | Object.entries(RECORD_ID_TO_TYPE_LOOKUP).forEach(pair => {
500 | RECORD_TYPE_TO_ID_LOOKUP[pair[1]] = parseInt(pair[0]);
501 | })
502 |
503 | const checks1 = [
504 | [isIPv4('0.0.0.0'), true],
505 | [isIPv4('127.0.0.1'), true],
506 | [isIPv4('256.256.256.256'), false],
507 | [isIPv4('1.1.1.1'), true],
508 | [isIPv4('1.1.1.1.'), false],
509 | [isIPv4('1.1.1.1.1'), false],
510 | [isIPv4('.1.1.1.1'), false],
511 | [isIPv4('a.a.a.a'), false],
512 | [isIPv4(''), false],
513 | [isIPv4({}), false],
514 | [isIPv4([]), false],
515 | [isIPv4(0), false],
516 | [isIPv4(1), false],
517 | [isIPv4(null), false],
518 | [isIPv4(undefined), false],
519 | ];
520 |
521 | checks1.map(x => x[0] === x[1]).forEach((x, index) => {
522 | if (!x) {
523 | console.log('**************************************************');
524 | console.log(`IPv4 Test case Failed at index: ${index}`);
525 | console.log('**************************************************');
526 | process.exit(1);
527 | }
528 | });
529 |
530 | const checks2 = [
531 | [isIPv6('2001:db8:1111:2222:3333::51'), true],
532 | [isIPv6('2001:db8:1111:2g22:3333::51'), false],
533 | [isIPv6('2001:db8:1111:2-22:3333::51'), false],
534 | [isIPv6('2001:db8:1111:2%22:3333::51'), false],
535 | [isIPv6('2001:0db8:0000:0000:0000:ff00:0042:8329'), true],
536 | [isIPv6('2001:db8:0:0:0:ff00:42:8329'), true],
537 | [isIPv6('2001:db8::ff00:42:8329'), true],
538 | [isIPv6('2001:0db8:0000:0000:0000:ff00:0042:83291'), false],
539 | [isIPv6('1.1.1.1'), false],
540 | [isIPv6('1.1.1.1.1'), false],
541 | [isIPv6(''), false],
542 | [isIPv6({}), false],
543 | [isIPv6([]), false],
544 | [isIPv6(0), false],
545 | [isIPv6(1), false],
546 | [isIPv6(null), false],
547 | [isIPv6(undefined), false],
548 | ];
549 |
550 | checks2.map(x => x[0] === x[1]).forEach((x, index) => {
551 | if (!x) {
552 | console.log('**************************************************');
553 | console.log(`IPv6 Test case Failed at index: ${index}`);
554 | console.log('**************************************************');
555 | process.exit(1);
556 | }
557 | });
558 |
559 | module.exports = DnsServer;
--------------------------------------------------------------------------------