From 8d92683a0cbec80c528ba51df8061b27464dfc68 Mon Sep 17 00:00:00 2001 From: Kershaw Chang Date: Wed, 20 Dec 2023 16:50:05 +0000 Subject: [PATCH] Bug 1852902 - Implement addHTTPSRecordOverride to set HTTPS record overrides, r=necko-reviewers,valentin Differential Revision: https://phabricator.services.mozilla.com/D196818 --- netwerk/dns/ChildDNSService.cpp | 3 +- netwerk/dns/GetAddrInfo.cpp | 86 +++++++- netwerk/dns/GetAddrInfo.h | 7 + .../dns/NativeDNSResolverOverrideChild.cpp | 8 + netwerk/dns/NativeDNSResolverOverrideChild.h | 2 + .../dns/NativeDNSResolverOverrideParent.cpp | 11 + netwerk/dns/PNativeDNSResolverOverride.ipdl | 1 + netwerk/dns/PlatformDNSUnix.cpp | 27 +-- netwerk/dns/nsINativeDNSResolverOverride.idl | 8 + netwerk/test/unit/head_trr.js | 39 ++++ netwerk/test/unit/test_dns_override.js | 193 ++++++++++++++++++ 11 files changed, 357 insertions(+), 28 deletions(-) diff --git a/netwerk/dns/ChildDNSService.cpp b/netwerk/dns/ChildDNSService.cpp index 8ef9f3332934..653608ab0176 100644 --- a/netwerk/dns/ChildDNSService.cpp +++ b/netwerk/dns/ChildDNSService.cpp @@ -105,7 +105,8 @@ nsresult ChildDNSService::AsyncResolveInternal( resolveDNSInSocketProcess = true; if (type != nsIDNSService::RESOLVE_TYPE_DEFAULT && (mTRRServiceParent->Mode() != nsIDNSService::MODE_TRRFIRST && - mTRRServiceParent->Mode() != nsIDNSService::MODE_TRRONLY)) { + mTRRServiceParent->Mode() != nsIDNSService::MODE_TRRONLY) && + !StaticPrefs::network_dns_native_https_query()) { return NS_ERROR_UNKNOWN_HOST; } } diff --git a/netwerk/dns/GetAddrInfo.cpp b/netwerk/dns/GetAddrInfo.cpp index 830254f08626..0dd1f0099a04 100644 --- a/netwerk/dns/GetAddrInfo.cpp +++ b/netwerk/dns/GetAddrInfo.cpp @@ -406,9 +406,84 @@ nsresult GetAddrInfo(const nsACString& aHost, uint16_t aAddressFamily, return rv; } +bool FindHTTPSRecordOverride(const nsACString& aHost, + TypeRecordResultType& aResult) { + LOG("FindHTTPSRecordOverride aHost=%s", nsCString(aHost).get()); + RefPtr overrideService = gOverrideService; + if (!overrideService) { + return false; + } + + AutoReadLock lock(overrideService->mLock); + auto overrides = overrideService->mHTTPSRecordOverrides.Lookup(aHost); + if (!overrides) { + return false; + } + + DNSPacket packet; + nsAutoCString host(aHost); + nsAutoCString cname; + + LOG("resolving %s\n", host.get()); + // Perform the query + nsresult rv = packet.FillBuffer( + [&](unsigned char response[DNSPacket::MAX_SIZE]) -> int { + if (overrides->Length() > DNSPacket::MAX_SIZE) { + return -1; + } + memcpy(response, overrides->Elements(), overrides->Length()); + return overrides->Length(); + }); + if (NS_FAILED(rv)) { + return false; + } + + uint32_t ttl = 0; + rv = ParseHTTPSRecord(host, packet, aResult, ttl); + + return NS_SUCCEEDED(rv); +} + +nsresult ParseHTTPSRecord(nsCString& aHost, DNSPacket& aDNSPacket, + TypeRecordResultType& aResult, uint32_t& aTTL) { + nsAutoCString cname; + nsresult rv; + + aDNSPacket.SetNativePacket(true); + + int32_t loopCount = 64; + while (loopCount > 0 && aResult.is()) { + loopCount--; + DOHresp resp; + nsClassHashtable additionalRecords; + rv = aDNSPacket.Decode(aHost, TRRTYPE_HTTPSSVC, cname, true, resp, aResult, + additionalRecords, aTTL); + if (NS_FAILED(rv)) { + LOG("Decode failed %x", static_cast(rv)); + return rv; + } + if (!cname.IsEmpty() && aResult.is()) { + aHost = cname; + cname.Truncate(); + continue; + } + } + + if (aResult.is()) { + LOG("Result is nothing"); + // The call succeeded, but no HTTPS records were found. + return NS_ERROR_UNKNOWN_HOST; + } + + return NS_OK; +} + nsresult ResolveHTTPSRecord(const nsACString& aHost, uint16_t aFlags, TypeRecordResultType& aResult, uint32_t& aTTL) { - // TODO: handle overrides here then proceed to call platform specific impl. + if (gOverrideService) { + return FindHTTPSRecordOverride(aHost, aResult) ? NS_OK + : NS_ERROR_UNKNOWN_HOST; + } return ResolveHTTPSRecordImpl(aHost, aFlags, aResult, aTTL); } @@ -453,6 +528,15 @@ NS_IMETHODIMP NativeDNSResolverOverride::AddIPOverride( return NS_OK; } +NS_IMETHODIMP NativeDNSResolverOverride::AddHTTPSRecordOverride( + const nsACString& aHost, const uint8_t* aData, uint32_t aLength) { + AutoWriteLock lock(mLock); + nsTArray data(aData, aLength); + mHTTPSRecordOverrides.InsertOrUpdate(aHost, std::move(data)); + + return NS_OK; +} + NS_IMETHODIMP NativeDNSResolverOverride::SetCnameOverride( const nsACString& aHost, const nsACString& aCNAME) { if (aCNAME.IsEmpty()) { diff --git a/netwerk/dns/GetAddrInfo.h b/netwerk/dns/GetAddrInfo.h index 3b001789413d..e8dc09541574 100644 --- a/netwerk/dns/GetAddrInfo.h +++ b/netwerk/dns/GetAddrInfo.h @@ -30,6 +30,7 @@ namespace net { extern LazyLogModule gGetAddrInfoLog; class AddrInfo; +class DNSPacket; /** * Look up a host by name. Mostly equivalent to getaddrinfo(host, NULL, ...) of @@ -79,6 +80,9 @@ nsresult ResolveHTTPSRecord(const nsACString& aHost, uint16_t aFlags, nsresult ResolveHTTPSRecordImpl(const nsACString& aHost, uint16_t aFlags, TypeRecordResultType& aResult, uint32_t& aTTL); +nsresult ParseHTTPSRecord(nsCString& aHost, DNSPacket& aDNSPacket, + TypeRecordResultType& aResult, uint32_t& aTTL); + class NativeDNSResolverOverride : public nsINativeDNSResolverOverride { NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSINATIVEDNSRESOLVEROVERRIDE @@ -93,9 +97,12 @@ class NativeDNSResolverOverride : public nsINativeDNSResolverOverride { nsTHashMap> mOverrides; nsTHashMap mCnames; + nsTHashMap> mHTTPSRecordOverrides; friend bool FindAddrOverride(const nsACString& aHost, uint16_t aAddressFamily, uint16_t aFlags, AddrInfo** aAddrInfo); + friend bool FindHTTPSRecordOverride(const nsACString& aHost, + TypeRecordResultType& aResult); }; } // namespace net diff --git a/netwerk/dns/NativeDNSResolverOverrideChild.cpp b/netwerk/dns/NativeDNSResolverOverrideChild.cpp index 0e6c76dc5cca..b52e929ab332 100644 --- a/netwerk/dns/NativeDNSResolverOverrideChild.cpp +++ b/netwerk/dns/NativeDNSResolverOverrideChild.cpp @@ -20,6 +20,14 @@ mozilla::ipc::IPCResult NativeDNSResolverOverrideChild::RecvAddIPOverride( return IPC_OK(); } +mozilla::ipc::IPCResult +NativeDNSResolverOverrideChild::RecvAddHTTPSRecordOverride( + const nsCString& aHost, nsTArray&& aData) { + Unused << mOverrideService->AddHTTPSRecordOverride(aHost, aData.Elements(), + aData.Length()); + return IPC_OK(); +} + mozilla::ipc::IPCResult NativeDNSResolverOverrideChild::RecvSetCnameOverride( const nsCString& aHost, const nsCString& aCNAME) { Unused << mOverrideService->SetCnameOverride(aHost, aCNAME); diff --git a/netwerk/dns/NativeDNSResolverOverrideChild.h b/netwerk/dns/NativeDNSResolverOverrideChild.h index 5b24cb3dcdbe..42fcc0b7c8d3 100644 --- a/netwerk/dns/NativeDNSResolverOverrideChild.h +++ b/netwerk/dns/NativeDNSResolverOverrideChild.h @@ -21,6 +21,8 @@ class NativeDNSResolverOverrideChild : public PNativeDNSResolverOverrideChild { mozilla::ipc::IPCResult RecvAddIPOverride(const nsCString& aHost, const nsCString& aIPLiteral); + mozilla::ipc::IPCResult RecvAddHTTPSRecordOverride(const nsCString& aHost, + nsTArray&& aData); mozilla::ipc::IPCResult RecvSetCnameOverride(const nsCString& aHost, const nsCString& aCNAME); mozilla::ipc::IPCResult RecvClearHostOverride(const nsCString& aHost); diff --git a/netwerk/dns/NativeDNSResolverOverrideParent.cpp b/netwerk/dns/NativeDNSResolverOverrideParent.cpp index 7be0c210a57e..47ba16918f10 100644 --- a/netwerk/dns/NativeDNSResolverOverrideParent.cpp +++ b/netwerk/dns/NativeDNSResolverOverrideParent.cpp @@ -60,6 +60,17 @@ NS_IMETHODIMP NativeDNSResolverOverrideParent::AddIPOverride( return NS_OK; } +NS_IMETHODIMP NativeDNSResolverOverrideParent::AddHTTPSRecordOverride( + const nsACString& aHost, const uint8_t* aData, uint32_t aLength) { + nsCString host(aHost); + CopyableTArray data(aData, aLength); + auto task = [self = RefPtr{this}, host, data = std::move(data)]() { + Unused << self->SendAddHTTPSRecordOverride(host, data); + }; + gIOService->CallOrWaitForSocketProcess(std::move(task)); + return NS_OK; +} + NS_IMETHODIMP NativeDNSResolverOverrideParent::SetCnameOverride( const nsACString& aHost, const nsACString& aCNAME) { if (aCNAME.IsEmpty()) { diff --git a/netwerk/dns/PNativeDNSResolverOverride.ipdl b/netwerk/dns/PNativeDNSResolverOverride.ipdl index 22cb274e8d9e..a1cd4c9ff552 100644 --- a/netwerk/dns/PNativeDNSResolverOverride.ipdl +++ b/netwerk/dns/PNativeDNSResolverOverride.ipdl @@ -16,6 +16,7 @@ async protocol PNativeDNSResolverOverride child: async __delete__(); async AddIPOverride(nsCString aHost, nsCString aIPLiteral); + async AddHTTPSRecordOverride(nsCString aHost, uint8_t[] aData); async SetCnameOverride(nsCString aHost, nsCString aCNAME); async ClearHostOverride(nsCString aHost); async ClearOverrides(); diff --git a/netwerk/dns/PlatformDNSUnix.cpp b/netwerk/dns/PlatformDNSUnix.cpp index 7c82a8d65979..753d78697793 100644 --- a/netwerk/dns/PlatformDNSUnix.cpp +++ b/netwerk/dns/PlatformDNSUnix.cpp @@ -50,33 +50,8 @@ nsresult ResolveHTTPSRecordImpl(const nsACString& aHost, uint16_t aFlags, if (NS_FAILED(rv)) { return rv; } - packet.SetNativePacket(true); - int32_t loopCount = 64; - while (loopCount > 0 && aResult.is()) { - loopCount--; - DOHresp resp; - nsClassHashtable additionalRecords; - rv = packet.Decode(host, TRRTYPE_HTTPSSVC, cname, true, resp, aResult, - additionalRecords, aTTL); - if (NS_FAILED(rv)) { - LOG("Decode failed %x", static_cast(rv)); - return rv; - } - if (!cname.IsEmpty() && aResult.is()) { - host = cname; - cname.Truncate(); - continue; - } - } - - if (aResult.is()) { - LOG("Result is nothing"); - // The call succeeded, but no HTTPS records were found. - return NS_ERROR_UNKNOWN_HOST; - } - - return NS_OK; + return ParseHTTPSRecord(host, packet, aResult, aTTL); } } // namespace mozilla::net diff --git a/netwerk/dns/nsINativeDNSResolverOverride.idl b/netwerk/dns/nsINativeDNSResolverOverride.idl index 874328fabc24..0a016af4373a 100644 --- a/netwerk/dns/nsINativeDNSResolverOverride.idl +++ b/netwerk/dns/nsINativeDNSResolverOverride.idl @@ -12,6 +12,14 @@ interface nsINativeDNSResolverOverride : nsISupports */ void addIPOverride(in AUTF8String aHost, in ACString aIPLiteral); + /** + * Adds an HTTPS record override for this specific host. + * The input needs to be the raw bytes of a DNS answer. + */ + void addHTTPSRecordOverride(in AUTF8String aHost, + [array, size_is(aLength), const] in uint8_t aData, + in unsigned long aLength); + /** * Sets a CNAME override for this specific host. */ diff --git a/netwerk/test/unit/head_trr.js b/netwerk/test/unit/head_trr.js index 06efd4bd517c..8262c735de8f 100644 --- a/netwerk/test/unit/head_trr.js +++ b/netwerk/test/unit/head_trr.js @@ -241,6 +241,44 @@ class TRRDNSListener { } } +// This is for reteriiving the raw bytes from a DNS answer. +function answerHandler(req, resp) { + let searchParams = new URL(req.url, "http://example.com").searchParams; + console.log("req.searchParams:" + searchParams); + if (!searchParams.get("host")) { + resp.writeHead(400); + resp.end("Missing search parameter"); + return; + } + + function processRequest(req1, resp1) { + let domain = searchParams.get("host"); + let type = searchParams.get("type"); + let response = global.dns_query_answers[`${domain}/${type}`] || {}; + let buf = global.dnsPacket.encode({ + type: "response", + id: 0, + flags: 0, + questions: [], + answers: response.answers || [], + additionals: response.additionals || [], + }); + let writeResponse = (resp2, buf2, context) => { + try { + let data = buf2.toString("hex"); + resp2.setHeader("Content-Length", data.length); + resp2.writeHead(200, { "Content-Type": "plain/text" }); + resp2.write(data); + resp2.end(""); + } catch (e) {} + }; + + writeResponse(resp1, buf, response); + } + + processRequest(req, resp); +} + /// This is the default handler for /dns-query /// It implements basic functionality for parsing the DoH packet, then /// queries global.dns_query_answers for available answers for the DNS query. @@ -364,6 +402,7 @@ class TRRServer extends NodeHTTP2Server { global.http2 = require("http2"); })()`); await this.registerPathHandler("/dns-query", trrQueryHandler); + await this.registerPathHandler("/dnsAnswer", answerHandler); await this.execute(getRequestCount); } diff --git a/netwerk/test/unit/test_dns_override.js b/netwerk/test/unit/test_dns_override.js index 3dad511b4ea0..b9461b7a3b71 100644 --- a/netwerk/test/unit/test_dns_override.js +++ b/netwerk/test/unit/test_dns_override.js @@ -48,6 +48,14 @@ Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]); const DOMAIN = "example.org"; const OTHER = "example.com"; +add_setup(async function setup() { + trr_test_setup(); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + }); +}); + add_task(async function test_bad_IPs() { Assert.throws( () => override.addIPOverride(DOMAIN, DOMAIN), @@ -320,3 +328,188 @@ add_task(async function test_nxdomain() { let [, , inStatus] = await listener; equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); }); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +function hexToUint8Array(hex) { + return new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); +} + +add_task(async function test_https_record_override() { + if (mozinfo.os == "android") { + Assert.ok(true, "skip this test on Android"); + return; + } + + let trrServer = new TRRServer(); + await trrServer.start(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + + await trrServer.registerDoHAnswers("service.com", "HTTPS", { + answers: [ + { + name: "service.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: ".", + values: [ + { key: "alpn", value: ["h2", "h3"] }, + { key: "no-default-alpn" }, + { key: "port", value: 8888 }, + { key: "ipv4hint", value: "1.2.3.4" }, + { key: "echconfig", value: "123..." }, + { key: "ipv6hint", value: "::1" }, + { key: "odoh", value: "456..." }, + ], + }, + }, + { + name: "service.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.com", + values: [ + { key: "alpn", value: "h2" }, + { key: "ipv4hint", value: ["1.2.3.4", "5.6.7.8"] }, + { key: "echconfig", value: "abc..." }, + { key: "ipv6hint", value: ["::1", "fe80::794f:6d2c:3d5e:7836"] }, + { key: "odoh", value: "def..." }, + ], + }, + }, + ], + }); + + let chan = makeChan( + `https://foo.example.com:${trrServer.port()}/dnsAnswer?host=service.com&type=HTTPS` + ); + let [, buf] = await channelOpenPromise(chan); + let rawBuffer = hexToUint8Array(buf); + + override.addHTTPSRecordOverride("service.com", rawBuffer, rawBuffer.length); + + Services.prefs.setBoolPref("network.dns.native_https_query", true); + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.dns.native_https_query"); + }); + + let listener = new Listener(); + Services.dns.asyncResolve( + "service.com", + Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + + let [, inRecord, inStatus] = await listener; + equal(inStatus, Cr.NS_OK); + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + equal(answer[0].priority, 1); + equal(answer[0].name, "service.com"); + Assert.deepEqual( + answer[0].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn, + ["h2", "h3"], + "got correct answer" + ); + Assert.ok( + answer[0].values[1].QueryInterface(Ci.nsISVCParamNoDefaultAlpn), + "got correct answer" + ); + Assert.equal( + answer[0].values[2].QueryInterface(Ci.nsISVCParamPort).port, + 8888, + "got correct answer" + ); + Assert.equal( + answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0] + .address, + "1.2.3.4", + "got correct answer" + ); + Assert.equal( + answer[0].values[4].QueryInterface(Ci.nsISVCParamEchConfig).echconfig, + "123...", + "got correct answer" + ); + Assert.equal( + answer[0].values[5].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0] + .address, + "::1", + "got correct answer" + ); + Assert.equal( + answer[0].values[6].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig, + "456...", + "got correct answer" + ); + + Assert.equal(answer[1].priority, 2); + Assert.equal(answer[1].name, "test.com"); + Assert.equal(answer[1].values.length, 5); + Assert.deepEqual( + answer[1].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn, + ["h2"], + "got correct answer" + ); + Assert.equal( + answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0] + .address, + "1.2.3.4", + "got correct answer" + ); + Assert.equal( + answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[1] + .address, + "5.6.7.8", + "got correct answer" + ); + Assert.equal( + answer[1].values[2].QueryInterface(Ci.nsISVCParamEchConfig).echconfig, + "abc...", + "got correct answer" + ); + Assert.equal( + answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0] + .address, + "::1", + "got correct answer" + ); + Assert.equal( + answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[1] + .address, + "fe80::794f:6d2c:3d5e:7836", + "got correct answer" + ); + Assert.equal( + answer[1].values[4].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig, + "def...", + "got correct answer" + ); +});