Bug 1852902 - Implement addHTTPSRecordOverride to set HTTPS record overrides, r=necko-reviewers,valentin
Differential Revision: https://phabricator.services.mozilla.com/D196818
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<NativeDNSResolverOverride> 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<Nothing>()) {
|
||||
loopCount--;
|
||||
DOHresp resp;
|
||||
nsClassHashtable<nsCStringHashKey, DOHresp> additionalRecords;
|
||||
rv = aDNSPacket.Decode(aHost, TRRTYPE_HTTPSSVC, cname, true, resp, aResult,
|
||||
additionalRecords, aTTL);
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG("Decode failed %x", static_cast<uint32_t>(rv));
|
||||
return rv;
|
||||
}
|
||||
if (!cname.IsEmpty() && aResult.is<Nothing>()) {
|
||||
aHost = cname;
|
||||
cname.Truncate();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (aResult.is<Nothing>()) {
|
||||
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<uint8_t> 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()) {
|
||||
|
||||
@@ -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<nsCStringHashKey, nsTArray<NetAddr>> mOverrides;
|
||||
nsTHashMap<nsCStringHashKey, nsCString> mCnames;
|
||||
nsTHashMap<nsCStringHashKey, nsTArray<uint8_t>> 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
|
||||
|
||||
@@ -20,6 +20,14 @@ mozilla::ipc::IPCResult NativeDNSResolverOverrideChild::RecvAddIPOverride(
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
mozilla::ipc::IPCResult
|
||||
NativeDNSResolverOverrideChild::RecvAddHTTPSRecordOverride(
|
||||
const nsCString& aHost, nsTArray<uint8_t>&& 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);
|
||||
|
||||
@@ -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<uint8_t>&& aData);
|
||||
mozilla::ipc::IPCResult RecvSetCnameOverride(const nsCString& aHost,
|
||||
const nsCString& aCNAME);
|
||||
mozilla::ipc::IPCResult RecvClearHostOverride(const nsCString& aHost);
|
||||
|
||||
@@ -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<uint8_t> 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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Nothing>()) {
|
||||
loopCount--;
|
||||
DOHresp resp;
|
||||
nsClassHashtable<nsCStringHashKey, DOHresp> additionalRecords;
|
||||
rv = packet.Decode(host, TRRTYPE_HTTPSSVC, cname, true, resp, aResult,
|
||||
additionalRecords, aTTL);
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG("Decode failed %x", static_cast<uint32_t>(rv));
|
||||
return rv;
|
||||
}
|
||||
if (!cname.IsEmpty() && aResult.is<Nothing>()) {
|
||||
host = cname;
|
||||
cname.Truncate();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (aResult.is<Nothing>()) {
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user