├── 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; --------------------------------------------------------------------------------