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.
// 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}`);