Adds DoH capabilities
This commit is contained in:
parent
88e3470f1f
commit
2a41c40dfd
3 changed files with 202 additions and 4 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "domain-forrward",
|
"name": "domain-forward",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Public service to forward domain for free",
|
"description": "Service to forward domain via DoH",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -21,7 +21,9 @@
|
||||||
"await-lock": "^2.2.2",
|
"await-lock": "^2.2.2",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"jose": "^3.20.4",
|
"jose": "^3.20.4",
|
||||||
"pem": "^1.14.7"
|
"pem": "^1.14.7",
|
||||||
|
"base32-encode": "^1.1.1",
|
||||||
|
"dns-packet": "^5.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pm2": "^5.3.0"
|
"pm2": "^5.3.0"
|
||||||
|
|
|
||||||
173
src/doh.js
Normal file
173
src/doh.js
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
import dnsPacket from 'dns-packet';
|
||||||
|
import https from 'https';
|
||||||
|
import http from 'http';
|
||||||
|
import base32Encode from 'base32-encode';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A super lame DNS over HTTPS stub resolver
|
||||||
|
*/
|
||||||
|
class DohResolver {
|
||||||
|
/**
|
||||||
|
* Creates a new DoH resolver
|
||||||
|
* @param nameserver_url {string} The URL we're going to be sending DNS requests to
|
||||||
|
*/
|
||||||
|
constructor(nameserver_url) {
|
||||||
|
this.nameserver_url = nameserver_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a DNS lookup for the given query name and type.
|
||||||
|
*
|
||||||
|
* @param qname {string} the domain name to query for (e.g. example.com)
|
||||||
|
* @param qtype {string} the type of record we're looking for (e.g. A, AAAA, TXT, MX)
|
||||||
|
* @param timeout {number} the number of milliseconds to wait for a response before aborting the request
|
||||||
|
* @returns {Promise<object>} The DNS response received
|
||||||
|
*/
|
||||||
|
query(qname, qtype='TXT', timeout=null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let dnsMessage = makeQuery(qname, qtype);
|
||||||
|
sendDohMsg(dnsMessage, this.nameserver_url, timeout)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a DNS query message of type {@link object} (see [dns-packet]{@link https://github.com/mafintosh/dns-packet}). Use this before calling {@link sendDohMsg}
|
||||||
|
*
|
||||||
|
* The recursion desired flag will be set, and the ID in the header will be set to zero, per the RFC ([section 4.1](https://tools.ietf.org/html/rfc8484#section-4.1)).
|
||||||
|
* @param qname {string} the domain name to put in the query message (e.g. example.com)
|
||||||
|
* @param qtype {string} the query type to put in the query message (e.g. A, AAAA, DS, DNSKEY)
|
||||||
|
* @returns {object} The DNS query message
|
||||||
|
*/
|
||||||
|
function makeQuery(qname, qtype='TXT') {
|
||||||
|
return {
|
||||||
|
type: 'query',
|
||||||
|
id: 0,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [{
|
||||||
|
type: qtype,
|
||||||
|
name: qname,
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a DNS message over HTTPS to `url` using the given request method
|
||||||
|
*
|
||||||
|
* @param packet {object} the DNS message to send
|
||||||
|
* @param url {string} the url to send the DNS message to
|
||||||
|
* @param timeout {number} the number of milliseconds to wait for a response before aborting the request
|
||||||
|
* @returns {Promise<object>} the response (if we got any)
|
||||||
|
*/
|
||||||
|
function sendDohMsg(packet, url, timeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transport = url.startsWith('https://') ? https : http;
|
||||||
|
const buf = dnsPacket.encode(packet);
|
||||||
|
let requestOptions;
|
||||||
|
const headers = {
|
||||||
|
'Accept': 'application/dns-message',
|
||||||
|
'User-Agent': 'domain-forward/1.0.0'
|
||||||
|
};
|
||||||
|
const dnsQueryParam = buf.toString('base64').toString('utf-8').replace(/=/g, '');
|
||||||
|
url = `${url}?dns=${dnsQueryParam}`;
|
||||||
|
url = new URL(url);
|
||||||
|
requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || 443,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
let data;
|
||||||
|
let timer;
|
||||||
|
const request = transport.request(requestOptions, (response) => {
|
||||||
|
response.on('data', (d) => {
|
||||||
|
if (!data) {
|
||||||
|
data = d;
|
||||||
|
} else {
|
||||||
|
data = Buffer.concat([data, d]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
response.on('end', () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const decoded = dnsPacket.decode(data);
|
||||||
|
resolve(decoded);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
request.on('error', (err) => {
|
||||||
|
request.destroy();
|
||||||
|
return reject(err);
|
||||||
|
});
|
||||||
|
if (timeout) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
request.destroy();
|
||||||
|
return reject(new Error(`Query timed out after ${timeout} milliseconds of inactivity`));
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
request.end()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 'Prettifies' a dnsPacket message.
|
||||||
|
*
|
||||||
|
* Namely, this convert Buffers the the appropriate presentation format.
|
||||||
|
* This is useful to make json human readable.
|
||||||
|
*
|
||||||
|
* *NOTE* This function may modify the message such that it no longer works with dnsPacket.
|
||||||
|
* Caution should be used when calling this object on query packets before sending them
|
||||||
|
*
|
||||||
|
* @param msg {object} a dnsPacket
|
||||||
|
* @returns {object} the msg which has been modified in-place. *May not be a valid <dnsPacket> afterwards*
|
||||||
|
*/
|
||||||
|
function prettify(msg) {
|
||||||
|
for (const rr of (msg['answers'] || []).concat((msg['authorities'] || []))) {
|
||||||
|
if (rr.hasOwnProperty('data')) {
|
||||||
|
switch (rr.type) {
|
||||||
|
case 'TXT':
|
||||||
|
rr.data = rr.data.toString('utf8');
|
||||||
|
break;
|
||||||
|
case 'DNSKEY':
|
||||||
|
rr.data.key = rr.data.key.toString('base64').replace('=', '');
|
||||||
|
break;
|
||||||
|
case 'DS':
|
||||||
|
rr.data.digest = rr.data.digest.toString('hex');
|
||||||
|
break;
|
||||||
|
case 'NSEC3':
|
||||||
|
rr.data.salt = rr.data.salt.toString('hex');
|
||||||
|
rr.data.nextDomain = base32Encode(rr.data.nextDomain, 'RFC4648-HEX').replace('=', '')
|
||||||
|
break;
|
||||||
|
case 'RRSIG':
|
||||||
|
rr.data.signature = rr.data.signature.toString('base64').replace('=', '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const rr of (msg['additionals'] || [])) {
|
||||||
|
if (rr.type === 'OPT') {
|
||||||
|
for (const opt of rr['options']) {
|
||||||
|
switch(opt.code) {
|
||||||
|
case 12:
|
||||||
|
opt.length = opt.data.length;
|
||||||
|
opt.data = opt.data.toString('hex').substring(0, 80);
|
||||||
|
if (opt.data.length === 80 ) {
|
||||||
|
opt.data += '...'
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DohResolver;
|
||||||
25
src/util.js
25
src/util.js
|
|
@ -2,6 +2,7 @@ import request from "./certnode/lib/request.js";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import {fileURLToPath} from "url";
|
import {fileURLToPath} from "url";
|
||||||
|
import DohResolver from "./doh.js";
|
||||||
|
|
||||||
const recordParamDestUrl = 'forward-to';
|
const recordParamDestUrl = 'forward-to';
|
||||||
const recordParamHttpStatus = 'http-status';
|
const recordParamHttpStatus = 'http-status';
|
||||||
|
|
@ -62,6 +63,28 @@ const parseTxtRecordData = (value) => {
|
||||||
* @return {Promise<{url: string, httpStatus?: string} | null>}
|
* @return {Promise<{url: string, httpStatus?: string} | null>}
|
||||||
*/
|
*/
|
||||||
export async function findTxtRecord(host) {
|
export async function findTxtRecord(host) {
|
||||||
|
const resolver = new DohResolver('https://doh.hosterra.tech/dns-query');
|
||||||
|
const resolved = await resolver.query( host);
|
||||||
|
|
||||||
|
if (resolved.response.answers) {
|
||||||
|
for (const head of resolve.data.Answer) {
|
||||||
|
const txtData = parseTxtRecordData(head.data);
|
||||||
|
if (!txtData[recordParamDestUrl]) continue;
|
||||||
|
return {
|
||||||
|
url: txtData[recordParamDestUrl],
|
||||||
|
httpStatus: txtData[recordParamHttpStatus],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*resolver.query( host)
|
||||||
|
.then(response => {
|
||||||
|
response.answers.forEach(ans => console.log(ans.data));
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const resolve = await request(`https://dns.google/resolve?name=_.${encodeURIComponent(host)}&type=TXT`);
|
const resolve = await request(`https://dns.google/resolve?name=_.${encodeURIComponent(host)}&type=TXT`);
|
||||||
if (resolve.data.Answer) {
|
if (resolve.data.Answer) {
|
||||||
for (const head of resolve.data.Answer) {
|
for (const head of resolve.data.Answer) {
|
||||||
|
|
@ -72,7 +95,7 @@ export async function findTxtRecord(host) {
|
||||||
httpStatus: txtData[recordParamHttpStatus],
|
httpStatus: txtData[recordParamHttpStatus],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
editor.link_modal.header
Reference in a new issue