Fix DoH URL path validation, more error handling for connections

This commit is contained in:
ekrekeler 2025-06-16 00:54:31 -05:00
parent 47cf7f9d13
commit a670386281
No known key found for this signature in database
GPG key ID: 4C66C864B6B00854
3 changed files with 34 additions and 22 deletions

View file

@ -480,6 +480,7 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
let client; let client;
let resolver; let resolver;
const socketName = `${resolverServer}:${resolverPort}`;
// Transport method determines which client type to use // Transport method determines which client type to use
switch (method) { switch (method) {
@ -500,13 +501,13 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
options.servername = resolverServer; options.servername = resolverServer;
} }
client = tls.connect(options, () => { client = tls.connect(options, () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`); log.debug("dns", `Connected to ${socketName}`);
client.write(buf); client.write(buf);
}); });
} else { } else {
client = new Socket(); client = new Socket();
client.connect(resolverPort, resolverServer, () => { client.connect(resolverPort, resolverServer, () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`); log.debug("dns", `Connected to ${socketName}`);
client.write(buf); client.write(buf);
}); });
} }
@ -518,11 +519,14 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
let expectedLength = 0; let expectedLength = 0;
let isValidLength = false; let isValidLength = false;
client.on("error", (err) => { client.on("error", (err) => {
if (err.code === "ETIMEDOUT") {
err.message = `Connection to ${socketName} timed out`;
}
reject(err); reject(err);
}); });
client.setTimeout(timeout, () => { client.setTimeout(timeout, () => {
client.destroy(); client.destroy();
reject({ message: "Connection timed out" }); reject({ message: `Request to ${socketName} timed out` });
}); });
client.on("data", (chunk) => { client.on("data", (chunk) => {
if (data.length === 0) { if (data.length === 0) {
@ -542,7 +546,7 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
} }
}); });
client.on("close", () => { client.on("close", () => {
log.debug("dns", "Connection closed"); log.debug("dns", `Connection to ${socketName} closed`);
if (!isValidLength) { if (!isValidLength) {
reject({ message: lenErrMsg }); reject({ message: lenErrMsg });
} }
@ -553,7 +557,7 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
case "DOH": { case "DOH": {
const queryPath = dohUsePost ? dohQuery : `${dohQuery}?dns=${buf.toString("base64url")}`; const queryPath = dohUsePost ? dohQuery : `${dohQuery}?dns=${buf.toString("base64url")}`;
const requestURL = url.parse(`https://${resolverServer}:${resolverPort}/${queryPath}`, true); const requestURL = url.parse(`https://${socketName}/${queryPath}`, true);
const mimeType = "application/dns-message"; const mimeType = "application/dns-message";
const options = { const options = {
hostname: requestURL.hostname, hostname: requestURL.hostname,
@ -596,10 +600,10 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
} }
// Validate response from resolver // Validate response from resolver
if (statusCode !== 200) { if (statusCode !== 200) {
reject({ message: `Request failed with status code ${statusCode}` }); reject({ message: `Request to ${socketName} failed with status code ${statusCode}` });
return; return;
} else if (contentType !== mimeType) { } else if (contentType !== mimeType) {
reject({ message: `Content-Type was "${contentType}", expected ${mimeType}` }); reject({ message: `Response from ${socketName} Content-Type was "${contentType}", expected ${mimeType}` });
return; return;
} }
// Read the response body into a buffer // Read the response body into a buffer
@ -627,10 +631,10 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
}); });
client.setTimeout(timeout, () => { client.setTimeout(timeout, () => {
client.destroy(); client.destroy();
reject({ message: "Request timed out" }); reject({ message: `Request to ${socketName} timed out` });
}); });
client.on("connect", () => { client.on("connect", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`); log.debug("dns", `Connected to ${socketName}`);
}); });
const req = client.request(headers); const req = client.request(headers);
req.on("error", (err) => { req.on("error", (err) => {
@ -653,11 +657,11 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
}); });
client.setTimeout(timeout, () => { client.setTimeout(timeout, () => {
client.destroy(); client.destroy();
reject({ message: "Request timed out" }); reject({ message: `Request to ${socketName} timed out` });
}); });
client.on("socket", (socket) => { client.on("socket", (socket) => {
socket.on("secureConnect", () => { socket.on("secureConnect", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`); log.debug("dns", `Connected to ${socketName}`);
}); });
}); });
if (dohUsePost) { if (dohUsePost) {
@ -669,7 +673,7 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
reject(err); reject(err);
}); });
client.on("close", () => { client.on("close", () => {
log.debug("dns", "Connection closed"); log.debug("dns", `Connection to ${socketName} closed`);
}); });
}); });
break; break;
@ -695,15 +699,15 @@ exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
reject(err); reject(err);
}); });
client.on("listening", () => { client.on("listening", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`); log.debug("dns", `Connected to ${socketName}`);
timer = setTimeout(() => { timer = setTimeout(() => {
reject({ message: "Query timed out" }); reject({ message: `Query to ${socketName} timed out` });
client.close(); client.close();
}, timeout); }, timeout);
}); });
client.on("close", () => { client.on("close", () => {
clearTimeout(timer); clearTimeout(timer);
log.debug("dns", "Connection closed"); log.debug("dns", `Connection to ${socketName} closed`);
}); });
}); });
client.send(buf, 0, buf.length, resolverPort, resolverServer); client.send(buf, 0, buf.length, resolverPort, resolverServer);

View file

@ -1279,6 +1279,7 @@ export default {
case "DoH": case "DoH":
return this.hostnameRegexPattern.source; return this.hostnameRegexPattern.source;
case "DoT": case "DoT":
case "DoQ":
return this.ipOrHostnameRegexPattern.source; return this.ipOrHostnameRegexPattern.source;
} }
} }

View file

@ -162,20 +162,27 @@ export function dnsNameRegexPattern() {
/** /**
* Regex patterns for validating URL paths * Regex patterns for validating URL paths
* @param {boolean} qstr whether or not the url follows query string format * @param {boolean} qstr True if the url should contain a query string
* @param {boolean} tmpl True if the url should contain templating, `{query}`
* Takes precedence over qstr if both are true.
* @returns {RegExp} The requested regex * @returns {RegExp} The requested regex
*/ */
export function urlPathRegexPattern(qstr = true) { export function urlPathRegexPattern(qstr = false, tmpl = false) {
// Matches any URL path, including empty string
const pathRegexPattern = /^\/?(([a-zA-Z0-9\-_%])+\/)*[a-zA-Z0-9\-_%]*(\?([a-zA-Z0-9\-_%]+=[a-zA-Z0-9\-_%]*&?)+)?$/;
// Ensures a URL path follows query string format // Ensures a URL path follows query string format
const queryStringRegexPattern = /^\/?(([a-zA-Z0-9\-_%])+\/)*[a-zA-Z0-9\-_%]*\?([a-zA-Z0-9\-_%]+=[a-zA-Z0-9\-_%]*&?)+$/; const queryRegexPattern = /^\/?(([a-zA-Z0-9\-_%])+\/)*[a-zA-Z0-9\-_%]*\?([a-zA-Z0-9\-_%]+=[a-zA-Z0-9\-_%]*&?)+$/;
// Only checks for valid URL path containing "{query}" // Only checks for valid URL path containing "{query}"
/* eslint-disable-next-line no-useless-escape */ /* eslint-disable-next-line no-useless-escape */
const queryRegexPattern = /^[a-zA-Z0-9\-._~:\/?#\[\]@!$&'\(\)*+,;=]*\{query\}[a-zA-Z0-9\-._~:\/?#\[\]@!$&'\(\)*+,;=]*$/; const queryTemplateRegexPattern = /^[a-zA-Z0-9\-._~:\/?#\[\]@!$&'\(\)*+,;=]*\{query\}[a-zA-Z0-9\-._~:\/?#\[\]@!$&'\(\)*+,;=]*$/;
if (qstr) { if (tmpl) {
return queryStringRegexPattern; return queryTemplateRegexPattern;
} }
return queryRegexPattern; if (qstr) {
return queryRegexPattern;
}
return pathRegexPattern;
} }
/** /**