From b12213d9ebd24d6cd69c3eb38c3525868bef8ae7 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:12:42 +0530 Subject: [PATCH 01/10] env: allow fake snis --- src/commons/envutil.js | 7 ++++++- src/core/env.js | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/commons/envutil.js b/src/commons/envutil.js index 8fcbd2f666..bc135f4cb1 100644 --- a/src/commons/envutil.js +++ b/src/commons/envutil.js @@ -184,6 +184,11 @@ export function tlsKey() { return envManager.get("TLS_KEY") || null; } +export function allowDomainFronting() { + if (!envManager) return false; + return envManager.get("TLS_ALLOW_ANY_SNI") || false; +} + export function kdfSvcSecretHex() { if (!envManager) return null; return envManager.get("KDF_SVC") || null; @@ -407,7 +412,7 @@ export function logpushSecretKey() { if (!envManager) return ""; const secretkey = envManager.get("CF_LOGPUSH_R2_SECRET_KEY") || ""; - if (onCloudflare() || onLocal()) return secretkey; + if (onCloudflare() || onLocal()) return secretkey || ""; return ""; } diff --git a/src/core/env.js b/src/core/env.js index 514ff4e833..018e6fd062 100644 --- a/src/core/env.js +++ b/src/core/env.js @@ -79,6 +79,12 @@ const defaults = new Map( type: "boolean", default: false, }, + // if true, do not validate the SNI field in TLS handshake + // effectively allowing clients to "fake" SNI + TLS_ALLOW_ANY_SNI: { + type: "boolean", + default: false, + }, // global log level (debug, info, warn, error) LOG_LEVEL: { type: "string", From 9220042eb1c4478f1eca419ee756251d3d5856c2 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:13:32 +0530 Subject: [PATCH 02/10] doh: handle HEAD with 204 --- src/core/doh.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/doh.js b/src/core/doh.js index 77293270c2..ebc5b0340a 100644 --- a/src/core/doh.js +++ b/src/core/doh.js @@ -31,6 +31,7 @@ export function handleRequest(event) { */ async function proxyRequest(event) { if (optionsRequest(event.request)) return util.respond204(); + if (headRequest(event.request)) return util.respond204(); const io = new IOState(); const ua = event.request.headers.get("User-Agent"); @@ -61,6 +62,10 @@ function optionsRequest(request) { return request.method === "OPTIONS"; } +function headRequest(request) { + return request.method === "HEAD"; +} + /** * Must not throw! * @param {IOState} io From ae6913bc9302955d8c37a27f13c93553f34d8536 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:13:49 +0530 Subject: [PATCH 03/10] node/blocklists: m mmap log --- src/core/node/blocklists.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/node/blocklists.js b/src/core/node/blocklists.js index 1bd3cbe2c8..d42052f71b 100644 --- a/src/core/node/blocklists.js +++ b/src/core/node/blocklists.js @@ -80,6 +80,7 @@ async function fmmap(fp) { const isDeno = envutil.isDeno(); if (dynimports && isNode) { + log.i("mmap f:", fp, "on node"); try { const mmap = (await import("@riaskov/mmap-io")).default; const fd = fs.openSync(fp, "r+"); From 74d740fd476ad61c7a2dcfcf7ea072bce2f983c0 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:14:27 +0530 Subject: [PATCH 04/10] dns/cache: avoid trimming cnames --- src/plugins/cache-util.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/plugins/cache-util.js b/src/plugins/cache-util.js index 74e2b8d573..f2d35c7aed 100644 --- a/src/plugins/cache-util.js +++ b/src/plugins/cache-util.js @@ -256,11 +256,11 @@ export function isAnswerFresh(m, n = 0) { export function updatedAnswer(dnsPacket, qid, expiry) { updateQueryId(dnsPacket, qid); updateTtl(dnsPacket, expiry); - retainOneAnswer(dnsPacket); + trimAQuadAAnswer(dnsPacket); return dnsPacket; } -function retainOneAnswer(decodedDnsPacket) { +function trimAQuadAAnswer(decodedDnsPacket) { // retain only the first answer, drop the rest if ( !dnsutil.hasSingleQuestion(decodedDnsPacket) || @@ -270,11 +270,20 @@ function retainOneAnswer(decodedDnsPacket) { return; } + let dotrim = false; + const trimmed = new Array(0); for (const a of decodedDnsPacket.answers) { + if (dnsutil.isAnswerCname(a)) { + trimmed.push(a); + } if (dnsutil.isAnswerA(a) || dnsutil.isAnswerAAAA(a)) { - decodedDnsPacket.answers = [a]; + trimmed.push(a); + dotrim = true; break; } } + if (dotrim) { + decodedDnsPacket.answers = trimmed; + } // else: nothing to trim, return as-is return; } From 7ed13397a70fbd082243320cd0daa63411de668e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:14:48 +0530 Subject: [PATCH 05/10] dns/cache: m jsdoc --- src/plugins/dns-op/cache-api.js | 2 +- src/plugins/dns-op/cache.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/dns-op/cache-api.js b/src/plugins/dns-op/cache-api.js index e59ec7a9f7..83f8e7cc1a 100644 --- a/src/plugins/dns-op/cache-api.js +++ b/src/plugins/dns-op/cache-api.js @@ -30,7 +30,7 @@ export class CacheApi { /** * @param {string} href * @param {Response} response - * @returns + * @returns {Promise} */ put(href, response) { if (this.noop) return false; diff --git a/src/plugins/dns-op/cache.js b/src/plugins/dns-op/cache.js index d19c85eaf1..14b0e01111 100644 --- a/src/plugins/dns-op/cache.js +++ b/src/plugins/dns-op/cache.js @@ -153,7 +153,7 @@ export class DnsCache { /** * @param {URL} url * @param {cacheutil.DnsCacheData} data - * @returns + * @returns {Promise} */ async putHttpCache(url, data) { const k = url.href; From 5adb77ce76384fd039180ebe46bebe5f3dd8adb6 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:15:22 +0530 Subject: [PATCH 06/10] dns/resolver: timeout doh with abort signal --- src/plugins/dns-op/resolver.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js index f90b606094..3e7b0a158f 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -449,6 +449,7 @@ DNSResolver.prototype.resolveDnsUpstream = async function ( dnsreq = new Request(u.href, { method: "GET", headers: util.dnsHeaders(), + signal: AbortSignal.timeout(this.timeout), }); } else if (util.isPostRequest(request)) { dnsreq = new Request(u.href, { @@ -458,10 +459,12 @@ DNSResolver.prototype.resolveDnsUpstream = async function ( util.dnsHeaders() ), body: query, + signal: AbortSignal.timeout(this.timeout), }); } else { throw new Error("get/post only"); } + this.log.d(rxid, "upstream doh2/fetch", u.href); promisedPromises.push(fetch(dnsreq)); } From c79b52de526df5e08c848048f124df48af5bd54f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:16:06 +0530 Subject: [PATCH 07/10] node/server: lint --- src/server-node.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 77c227007b..76452c1cf7 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -189,7 +189,7 @@ class Tracker { } cmap.set(connid, new ConnW(sock)); - sock.on("close", (haderr) => cmap.delete(connid)); + sock.on("close", (_haderr) => cmap.delete(connid)); return connid; } @@ -238,7 +238,7 @@ const maxHeapSnaps = 20; const maxCertUpdateAttempts = 20; let adjTimer = null; -((main) => { +((_main) => { // listen for "go" and start the server system.sub("go", systemUp); // listen for "end" and stop the server @@ -452,7 +452,10 @@ function systemUp() { * @param {int} n */ async function certUpdateForever(secopts, s, n = 0) { - if (n > maxCertUpdateAttempts) return false; + if (n > maxCertUpdateAttempts) { + console.error("crt: max update attempts reached", n); + return false; + } const crtpem = secopts.cert; if (bufutil.emptyBuf(crtpem)) { @@ -567,7 +570,7 @@ function trapServerEvents(id, s) { }); // emitted when the req is discarded due to maxConnections - s.on("drop", (data) => { + s.on("drop", (_data) => { stats.nofdrops += 1; stats.nofconns += 1; }); @@ -665,7 +668,7 @@ function trapSecureServerEvents(id, s) { }); // emitted when the req is discarded due to maxConnections - s.on("drop", (data) => { + s.on("drop", (_data) => { stats.nofdrops += 1; stats.nofconns += 1; }); @@ -725,7 +728,7 @@ function up(server, addr) { /** * RST and/or closes tcp socket. - * @param {Socket | TLSSocket} sock + * @param {Socket | TLSSocket | null} sock */ function close(sock) { if (!sock || sock.destroyed) return; @@ -825,7 +828,7 @@ function serveDoTProxyProto(clientSocket) { } } - clientSocket.on("error", (e) => { + clientSocket.on("error", (_e) => { log.w("pp: client err, closing"); close(clientSocket); close(dotSock); @@ -972,7 +975,7 @@ function serveTLS(socket) { const [flag, host] = isOurWcDn ? getMetadata(sni) : ["", sni]; const sb = new ScratchBuffer(); - log.d("----> dot request", host, flag); + log.d("----> dot request", flag, host); socket.on("data", async (data) => { const len = await handleTCPData(socket, data, sb, host, flag); adjustTLSFragAfterWrites(socket, len); @@ -998,7 +1001,7 @@ function serveTCP(socket) { /** * Handle DNS over TCP/TLS data stream. * @param {Socket} socket - * @param {Buffer} chunk - A TCP data segment + * @param {ArrayBuffer} chunk - A TCP data segment * @param {ScratchBuffer} sb - Scratch buffer * @param {String} host - Hostname * @param {String} flag - Blocklist Flag @@ -1037,7 +1040,7 @@ async function handleTCPData(socket, chunk, sb, host, flag) { // chunk out dns-query starting rem-th byte const data = chunk.slice(rem, qlimit); // out of band data, if any - const oob = chunk.slice(qlimit); + const oob = qlimit < cl ? chunk.slice(qlimit) : null; sb.allocOnce(qlen); @@ -1105,7 +1108,7 @@ async function handleTCPQuery(q, socket, host, flag) { * @param {string} rxid * @param {Socket} socket * @param {Uint8Array} data - * @param {int} n - bytes written to socket + * @returns {int} n - bytes written to socket */ function measuredWrite(rxid, socket, data) { let ok = tcpOkay(socket); From 946cb75534630ad8a5ad7829f4ab7e83aa869df3 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:18:04 +0530 Subject: [PATCH 08/10] node/server: conditionally allow domain fronting --- src/server-node.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 76452c1cf7..de65f3eb89 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -915,9 +915,13 @@ function getDnRE(socket) { /** * Gets flag and hostname from the wildcard domain name. * @param {String} sni - Wildcard SNI - * @return {Array} [flag, hostname] + * @return {<[String, String]>} [flag, hostname] - may be empty strings */ function getMetadata(sni) { + if (util.emptyString(sni)) { + const fakesni = envutil.allowDomainFronting(); + return ["", fakesni ? "nosni.tld" : ""]; + } // 1-flag.max.rethinkdns.com => ["1-flag", "max", "rethinkdns", "com"] // 1-flag.somedomain.tld => ["1-flag", "somedomain", "tld"] const s = sni.split("."); @@ -934,7 +938,7 @@ function getMetadata(sni) { return [flag, host]; } else { // sni => max.rethinkdns.com - log.d(`flag: "", host: ${host}`); + log.d(`flag: "", host: ${sni}`); return ["", sni]; } } @@ -945,23 +949,25 @@ function getMetadata(sni) { */ function serveTLS(socket) { const sni = socket.servername; - if (!sni) { - log.d("no sni, close conn"); - close(socket); - return; - } + if (!envutil.allowDomainFronting()) { + if (!sni) { + log.d("no sni, close conn"); + close(socket); + return; + } - if (!OUR_RG_DN_RE || !OUR_WC_DN_RE) { - [OUR_RG_DN_RE, OUR_WC_DN_RE] = getDnRE(socket); - } + if (!OUR_RG_DN_RE || !OUR_WC_DN_RE) { + [OUR_RG_DN_RE, OUR_WC_DN_RE] = getDnRE(socket); + } - const isOurRgDn = OUR_RG_DN_RE.test(sni); - const isOurWcDn = OUR_WC_DN_RE.test(sni); + const isOurRgDn = OUR_RG_DN_RE.test(sni); + const isOurWcDn = OUR_WC_DN_RE.test(sni); - if (!isOurWcDn && !isOurRgDn) { - log.w("unexpected sni, close conn", sni); - close(socket); - return; + if (!isOurWcDn && !isOurRgDn) { + log.w("unexpected sni, close conn", sni); + close(socket); + return; + } } if (false) { From c990a7292c45babc8591f17d7e4eae07bb231f45 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:32:07 +0530 Subject: [PATCH 09/10] node/server: suppress verbose tls client errs --- src/server-node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server-node.js b/src/server-node.js index de65f3eb89..318fdbc2eb 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -676,7 +676,7 @@ function trapSecureServerEvents(id, s) { s.on("tlsClientError", (err, /** @type {TLSSocket} */ tlsSocket) => { stats.tlserr += 1; // fly tcp healthchecks also trigger tlsClientErrors - log.d("tls: client err;", err.message, addrstr(tlsSocket)); + // log.d("tls: client err;", err.message, addrstr(tlsSocket)); close(tlsSocket); }); } From c923eab4de5fcc333c013ac411d8443de7dae36f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 14 Aug 2025 02:51:15 +0530 Subject: [PATCH 10/10] node/server: m lint --- src/server-node.js | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 318fdbc2eb..3ad5d51892 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -682,20 +682,7 @@ function trapSecureServerEvents(id, s) { } /** - * @param {TLSSocket|Socket} sock - */ -function addrstr(sock) { - if (!sock) return ""; - if (sock.localAddress == null || sock.remoteAddress == null) return ""; - return ( - `[${sock.localAddress}]:${sock.localPort}` + - "->" + - `[${sock.remoteAddress}]:${sock.remotePort}` - ); -} - -/** - * @param {tls.Server} s + * @param {tls.Server?} s * @returns {void} */ function rotateTkt(s) { @@ -738,14 +725,14 @@ function close(sock) { } /** - * @param {Http2ServerResponse} res + * @param {Http2ServerResponse?} res */ function resClose(res) { if (res && !res.destroy) res.destroy(); } /** - * @param {Http2ServerResponse} res + * @param {Http2ServerResponse?} res * @returns {Boolean} */ function resOkay(res) { @@ -754,20 +741,21 @@ function resOkay(res) { } /** - * @param {Socket} sock + * @param {Socket?} sock * @returns {Boolean} */ function tcpOkay(sock) { - return sock.writable; + return sock && sock.writable; } /** * Creates a duplex pipe between `a` and `b` sockets. - * @param {Socket} a - * @param {Socket} b + * @param {Socket?} a + * @param {Socket?} b * @return {Boolean} - true if pipe created, false if error */ function proxySockets(a, b) { + if (!a || !b) return false; if (a.destroyed || b.destroyed) return false; // handle errors? stackoverflow.com/a/61091744 a.pipe(b); @@ -914,12 +902,13 @@ function getDnRE(socket) { /** * Gets flag and hostname from the wildcard domain name. - * @param {String} sni - Wildcard SNI + * @param {String?} sni - Wildcard SNI * @return {<[String, String]>} [flag, hostname] - may be empty strings */ function getMetadata(sni) { + const fakesni = envutil.allowDomainFronting(); + if (util.emptyString(sni)) { - const fakesni = envutil.allowDomainFronting(); return ["", fakesni ? "nosni.tld" : ""]; } // 1-flag.max.rethinkdns.com => ["1-flag", "max", "rethinkdns", "com"] @@ -978,7 +967,7 @@ function serveTLS(socket) { log.d(`(${proto}), reused? ${reused}; ticket: ${tkt}; sess: ${sess}`); } - const [flag, host] = isOurWcDn ? getMetadata(sni) : ["", sni]; + const [flag, host] = getMetadata(sni); const sb = new ScratchBuffer(); log.d("----> dot request", flag, host);