UNPKG

tr-ddns

Version:
162 lines (146 loc) 4.15 kB
'use strict' const EventEmitter = require('node:events'); const dns2 = require('dns2'); const ipaddr = require('ipaddr.js'); const nullish = require('./nullish'); const log = require('./log'); class NameServer extends EventEmitter { #server; #closed; #debug; #db; constructor(config) { super(); this.#debug = config?.debug ? true : false; this.#closed = false; let listen = {}; if (config?.tcp) { listen.tcp = { port: config?.tcpListenPort ?? 53, address: config?.tcpListenAddress ?? '0.0.0.0' }; if (! ipaddr.isValid(listen.tcp.address)) { throw new Error('Invalid TCP listen address'); } if (! (Number.isSafeInteger(listen.tcp.port) && (listen.tcp.port >= 0x0001) && (listen.tcp.port <= 0xffff))) { throw new Error('Invalid TCP listen port'); } if (this.#debug) { log('NameServer: TCP:', listen.tcp.address + ':' + listen.tcp.port.toString()); } } else { if (this.#debug) { log('NameServer: TCP: none'); } } if (config?.udp) { listen.udp = { port: config?.udpListenPort ?? 53, address: config?.udpListenAddress ?? '0.0.0.0' }; if (ipaddr.IPv4.isValid(listen.udp.address)) { listen.udp.type = 'udp4'; } else if (ipaddr.IPv6.isValid(listen.udp.address)) { listen.udp.type = 'udp6'; } else { throw new Error('Invalid UDP listen address'); } if (! (Number.isSafeInteger(listen.udp.port) && (listen.udp.port >= 0x0001) && (listen.udp.port <= 0xffff))) { throw new Error('Invalid UDP listen port'); } if (this.#debug) { log('NameServer: UDP:', listen.udp.address + ':' + listen.udp.port.toString()); } } else { if (this.#debug) { log('NameServer: UDP: none'); } } if (! (typeof(config?.nameDB?.get) === 'function')) { throw new Error('Invalid NameDB'); } if (! (listen.tcp || listen.udp)) { throw new Error('No listeners'); } this.#db = config.nameDB; let server = dns2.createServer({ udp: listen.udp ? true : false, tcp: listen.tcp ? true : false, handle: function(request, send, rinfo) { return this.#query(request, send, rinfo); }.bind(this) }); server.on('request', function (request, response, rinfo) { /*NOTHING*/; }.bind(this)); server.on('requestError', function (e) { log('Client sent an invalid request', e); }.bind(this)); server.on('error', function (e) { this.emit(e); this.#server = undefined; server.close(); }.bind(this)); server.on('listening', function () { if (this.#closed) { server.close(); return; } this.#server = server; this.emit('ready'); }.bind(this)); server.on('close', function () { this.#closed = true; if (! this.#server) { return; } if (this.#debug) { log('server closed'); } this.#server = undefined; this.emit('close'); }.bind(this)); server.listen(listen); } #query(request, send, rinfo) { const response = dns2.Packet.createResponseFromRequest(request); for (let q of request.questions) { if (this.#debug) { log('query:', JSON.stringify(q, null, 2)); } if (! ((q?.class === dns2.Packet.CLASS.IN) && Number.isSafeInteger(q?.type) && (typeof(q?.name) === 'string'))) { if (this.#debug) { log('response: NONE <invalid-query>'); } // If any of the questions is invalid, we don't answer at all. return; } let r = this.#db.get(q.name, q.type); if (r === false) { // Query didn't match to any domain we are serving, so // we don't bother answering. It's probably a // configuration error, hostile probing, or DNS // applification attack anyways. log('response: NONE <not-allowed> (' + q.name + ')'); return; } if (nullish(r)) { // We don't have data that was asked for, but the // query was at least inside our domains. log('response: NONE <not-found> (' + q.name + ')'); continue; } if (this.#debug) { log('response:', JSON.stringify(r, null, 2)); } response.answers.push(r); } send(response); } close() { this.#closed = true; if (! this.#server) { return; } this.#server.close(); } }; module.exports = NameServer;