Last active
December 24, 2023 03:18
-
-
Save ncruces/f024ca13da77a19a7a62776f4bfac3b0 to your computer and use it in GitHub Desktop.
Cloudflare DNS64 over HTTPS with any NAT64 prefix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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