Skip to content

Instantly share code, notes, and snippets.

@ncruces
Last active December 24, 2023 03:18
Show Gist options
  • Save ncruces/f024ca13da77a19a7a62776f4bfac3b0 to your computer and use it in GitHub Desktop.
Save ncruces/f024ca13da77a19a7a62776f4bfac3b0 to your computer and use it in GitHub Desktop.
Cloudflare DNS64 over HTTPS with any NAT64 prefix
// A Cloudflare worker that uses https://dns.google to offer DNS64 over HTTP
// for any NAT64 prefix.
//
// Google's DNS64 service only supports the 64:ff9b::/96 'well know prefix',
// which is not publicly routable over the internet.
// See https://nat64.xyz/ for a list of publicly available NAT64 services and
// their prefixes.
//
// To use this, deploy it to a worker, and then configure your DNS over HTTPS
// resolver with this URL (specify more than one prefix for redundancy):
// https://[DNS64_WORKER_NAME].workers.dev/resolve?prefix=[PREFIX]&name=ipv4only.arpa&type=AAAA
addEventListener(
'fetch', event => event.respondWith(handleRequest(event.request)));
const WELL_KNOWN_PREFIX_JSON = /"(64:ff9b::[0-9a-f:.]+)"/g;
const WELL_KNOWN_PREFIX_AAAA = regexp('g')`
\0\x1c // AAAA record type 28
\0\x01 // IN class type
.... // TTL
\0\x10 // AAAA record length 16
\0\x64\xff\x9b\0\0\0\0\0\0\0\0 // 12 byte well known prefix
`;
async function handleRequest(req) {
if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'POST') {
return new Response(
null, {status: 405, headers: {allow: 'GET, HEAD, POST'}});
}
let url = new URL(req.url);
if (url.pathname !== '/dns-query' && url.pathname !== '/resolve') {
return new Response(null, {status: 404});
}
let prefixes = url.searchParams.getAll('prefix');
if (prefixes.length < 1) {
return new Response(null, {status: 400});
}
let ips = parsePrefixIPs(prefixes);
if (ips == null) {
return new Response(null, {status: 400});
}
url.hostname = 'dns64.dns.google';
url.searchParams.delete('prefix');
let res = await fetch(url.href, req);
if (res.status === 200) {
let ct = res.headers.get('content-type');
if (ct && ct.includes('json')) {
let body = await res.text();
body = replacePrefixJSON(body, ips);
return new Response(body, res);
}
if (ct === 'application/dns-message' ||
ct === 'application/dns-udpwireformat') {
let body = await res.arrayBuffer();
body = replacePrefixAAAA(body, ips);
return new Response(body, res);
}
}
return res;
}
function replacePrefixAAAA(body, prefixes) {
let data = new Uint8Array(body);
let text = String.fromCharCode.apply(null, data);
let n = Math.random() * prefixes.length | 0;
text.replace(WELL_KNOWN_PREFIX_AAAA, (match, index) => {
let prefix = prefixes[n];
n = (n + 1) % prefixes.length;
for (let i = 0; i < 12; ++i) {
data[index+10+i] = prefix.getByte(i);
}
return match;
});
return body;
}
function replacePrefixJSON(text, prefixes) {
let n = Math.random() * prefixes.length | 0;
return text.replace(WELL_KNOWN_PREFIX_JSON, (match, group) => {
let ip = parseIPv6(group);
if (ip == null) return match;
if ((ip[2] | ip[3] | ip[4] | ip[5]) !== 0) return match;
let prefix = prefixes[n];
n = (n + 1) % prefixes.length;
for (let i = 0; i < 6; ++i) {
ip[i] = prefix[i];
}
return '"' + ip.toString() + '"';
});
}
function parsePrefixIPs(prefixes) {
let ips = Array(prefixes.length);
for (let i = 0; i < prefixes.length; ++i) {
let prefix = prefixes[i];
let ip = parseIPv6(prefix);
if (ip == null) return null;
if ((ip[6] | ip[7]) !== 0) return null;
ips[i] = ip;
}
return ips;
}
// https://gist.github.com/ncruces/ad0a7f303f8714b152d7f3d184f0956a
// https://tools.ietf.org/html/rfc5952
function parseIPv6(addr) {
// does it have an IPv4 suffix
let ipv4 = /^([\s\S]*):(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(addr);
if (ipv4) {
// dot-decimal to ints
let dec = ipv4.slice(2).map(s => parseInt(s, 10));
if (dec.some(i => i > 255)) return null;
// ints to hexs
let hex = dec.map(i => ('0' + i.toString(16)).substr(-2));
// rebuild
addr = ipv4[1] + ':' + hex[0] + hex[1] + ':' + hex[2] + hex[3];
}
// where's the ::, is there a single one?
let ellipsis = addr.indexOf('::');
if (ellipsis !== addr.lastIndexOf('::')) return null;
let groups;
if (ellipsis < 0) {
// must have exactly 8 groups
groups = addr.split(':', 9);
if (groups.length != 8) return null;
} else {
// must have less than 8 groups
let head = [];
let tail = [];
if (ellipsis > 0)
head = addr.slice(0, ellipsis).split(':', 8);
if (ellipsis < addr.length - 2)
tail = addr.slice(ellipsis + 2).split(':', 8);
if (head.length + tail.length > 7) return null;
// fill in ellipsis, concat tail
head.length = 8 - tail.length;
groups = head.concat(tail);
}
// convert to an array of 8 16-bit ints
let ipv6 = [];
for (let i = 0; i < 8; ++i) {
let g = groups[i] || '0';
if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;
ipv6.push(parseInt(g, 16));
}
Object.defineProperty(ipv6, 'getByte', {
value: function(i) {
let b = this[i / 2 >>> 0];
return (i & 1) === 0 ? b >>> 8 : b & 0xff;
}
});
Object.defineProperty(ipv6, 'toString', {
value: function() {
// find longest run of zeros
let e0 = -1;
let e1 = -1;
for (let i = 0; i < 8; ++i) {
let j = i;
while (this[j] === 0) ++j;
if (j - i > e1 - e0) {
e0 = i;
e1 = j;
}
if (j > i) i = j;
}
// don't replace single group
if (e1 - e0 <= 1) {
e0 = -1;
e1 = -1;
}
let str = '';
for (let i = 0; i < 8; ++i) {
if (i === e0) {
str += '::';
if (e1 >= 8) break;
i = e1;
} else if (i > 0) {
str += ':';
}
str += this[i].toString(16);
}
return str;
}
});
return ipv6;
}
// https://gist.github.com/ncruces/04ef282b63ecfb28e1bc1e93a03d1a7a
function regexp(...args) {
function cleanup(string) {
// remove whitespace, single and multi-line comments
return string.replace(/\s+|\/\/.*|\/\*[\s\S]*?\*\//g, '');
}
function escape(string) {
// escape regular expression
return string.replace(/[-.*+?^${}()|[\]\\]/g, '\\$&');
}
function create(flags, strings, ...values) {
let pattern = '';
for (let i = 0; i < values.length; ++i) {
pattern += cleanup(strings.raw[i]); // strings are cleaned up
pattern += escape(values[i]); // values are escaped
}
pattern += cleanup(strings.raw[values.length]);
return RegExp(pattern, flags);
}
if (Array.isArray(args[0])) {
// used as a template tag (no flags)
return create('', ...args);
}
// used as a function (with flags)
return create.bind(void 0, args[0]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment