DNS-over-TLS Proxy Code

This page contains the JavaScript code for a DNS-over-TLS proxy that uses WebSockets. Click the button below the code to copy the code to your clipboard.

Checksums: MD5: 74760e79c70289a3a32a1aa91c556138
SHA-256: 673903626605bb16272bcc0686c1478fa2a373d042cf932c7e1cf85b8629fe2f

dns-o-tls-proxy.js

// dns-o-tls-proxy.js

const WebSocket = require('ws');
const tls = require('tls');
const dnsPacket = require('dns-packet');

const PORT = process.env.PORT || 8853;

const wss = new WebSocket.Server({ port: PORT });

const DEFAULT_DOT_SERVERS = [
    { hostname: 'dns.quad9.net', port: 853 },
    { hostname: '1dot1dot1dot1.cloudflare-dns.com', port: 853 }
];

wss.on('connection', (ws) => {
    console.log('Client connected.');
    ws.on('message', async (message) => {
        try {
            const { domain, type, custom_dot_server } = JSON.parse(message);

            if (!domain || !type) {
                throw new Error("Invalid request: 'domain' and 'type' are required.");
            }

            let serversToQuery;
            if (custom_dot_server) {
                console.log(`Received request for custom DoT server: ${custom_dot_server}`);
                serversToQuery = [{ hostname: custom_dot_server, port: 853 }];
            } else {
                serversToQuery = DEFAULT_DOT_SERVERS;
            }

            const { answers, resolverHostname, isDnssecValidated } = await queryServersInSequence(domain, type, serversToQuery);

            const answersWithAD = answers.map(answer => {
                const answerWithAD = {...answer};
                if (isDnssecValidated) {
                    answerWithAD.flags = (answerWithAD.flags || 0) | dnsPacket.AUTHENTIC_DATA;
                    answerWithAD.AD = true;
                } else {
                    answerWithAD.AD = false;
                }
                return answerWithAD;
            });

            ws.send(JSON.stringify({
                dnsResponse: answersWithAD,
                resolverHostname: resolverHostname,
                DnssecValidated: isDnssecValidated
            }));

        } catch (err) {
            console.error(`Error processing request: ${err.message}`);
            ws.send(JSON.stringify({ error: err.message }));
        }
    });

    ws.on('close', () => {
        console.log('Client disconnected.');
    });
});

async function queryServersInSequence(domain, type, servers) {
    for (const server of servers) {
        try {
            const { answers, adBitSet, hasRRSIG } = await querySingleServer(domain, type, server);

            const isDnssecValidated = adBitSet || hasRRSIG;

            return {
                answers: answers,
                resolverHostname: server.hostname,
                isDnssecValidated: isDnssecValidated
            };
        } catch (err) {
            console.warn(`DoT query failed for ${server.hostname}: ${err.message}. Trying next server...`);
        }
    }
    throw new Error('All DoT query attempts failed.');
}

function querySingleServer(domain, type, server) {
    return new Promise((resolve, reject) => {
        const query = dnsPacket.encode({
            type: 'query',
            id: Math.floor(Math.random() * 65535),
            flags: dnsPacket.RECURSION_DESIRED | dnsPacket.DNSSEC_OK,
            questions: [{
                type: type,
                name: domain,
                class: 'IN'
            }]
        });

        const buffer = Buffer.alloc(2 + query.length);
        buffer.writeUInt16BE(query.length, 0);
        query.copy(buffer, 2);

        const socket = tls.connect({
            port: server.port,
            host: server.hostname,
            rejectUnauthorized: true,
            minVersion: 'TLSv1.2'
        }, () => {
            socket.write(buffer);
        });

        let responseData = Buffer.alloc(0);

        socket.on('data', (data) => {
            responseData = Buffer.concat([responseData, data]);

            if (responseData.length < 2) return;
            const responseLength = responseData.readUInt16BE(0);
            if (responseData.length < responseLength + 2) return;

            socket.end();

            const packet = responseData.slice(2, responseLength + 2);
            const decoded = dnsPacket.decode(packet);

            const adBitSet = (decoded.flags & dnsPacket.AUTHENTIC_DATA) !== 0;
            const hasRRSIG = (decoded.answers || []).some(r => r.type === 'RRSIG') ||
                             (decoded.additionals || []).some(r => r.type === 'RRSIG');

            resolve({
                answers: decoded.answers || [],
                adBitSet: adBitSet,
                hasRRSIG: hasRRSIG
            });
        });

        socket.on('error', (err) => reject(new Error(`Socket error with ${server.hostname}: ${err.message}`)));
        socket.setTimeout(5000, () => {
            socket.destroy();
            reject(new Error(`Query to ${server.hostname} timed out`));
        });
    });
}

console.log(`DoT Proxy running on ws://localhost:${PORT}`);