Backed out changeset 820d70c9c0a3 (bug 1955579) Backed out changeset b3c4ae7bc073 (bug 1955579) Backed out changeset 7a78799b24a0 (bug 1955579) Backed out changeset b3ec267e42e0 (bug 1955579)
438 lines
12 KiB
JavaScript
438 lines
12 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/*
|
|
* This module implements the heuristics used to determine whether to enable
|
|
* or disable DoH on different networks. DoHController is responsible for running
|
|
* these at startup and upon network changes.
|
|
*/
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"gNetworkLinkService",
|
|
"@mozilla.org/network/network-link-service;1",
|
|
"nsINetworkLinkService"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"gParentalControlsService",
|
|
"@mozilla.org/parental-controls-service;1",
|
|
"nsIParentalControlsService"
|
|
);
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
DoHConfigController: "resource:///modules/DoHConfig.sys.mjs",
|
|
});
|
|
|
|
const GLOBAL_CANARY = "use-application-dns.net.";
|
|
|
|
const NXDOMAIN_ERR = "NS_ERROR_UNKNOWN_HOST";
|
|
|
|
export const Heuristics = {
|
|
// String constants used to indicate outcome of heuristics.
|
|
ENABLE_DOH: "enable_doh",
|
|
DISABLE_DOH: "disable_doh",
|
|
|
|
async run() {
|
|
// Run all the heuristics at the same time.
|
|
let [safeSearchChecks, zscaler, canary] = await Promise.all([
|
|
safeSearch(),
|
|
zscalerCanary(),
|
|
globalCanary(),
|
|
]);
|
|
|
|
let platformChecks = await platform();
|
|
let results = {
|
|
google: safeSearchChecks.google,
|
|
youtube: safeSearchChecks.youtube,
|
|
zscalerCanary: zscaler,
|
|
canary,
|
|
browserParent: await parentalControls(),
|
|
thirdPartyRoots: await thirdPartyRoots(),
|
|
policy: await enterprisePolicy(),
|
|
vpn: platformChecks.vpn,
|
|
proxy: platformChecks.proxy,
|
|
nrpt: platformChecks.nrpt,
|
|
steeredProvider: "",
|
|
};
|
|
|
|
// If any of those were triggered, return the results immediately.
|
|
if (Object.values(results).includes("disable_doh")) {
|
|
return results;
|
|
}
|
|
|
|
// Check for provider steering only after the other heuristics have passed.
|
|
results.steeredProvider = (await providerSteering()) || "";
|
|
return results;
|
|
},
|
|
|
|
async checkEnterprisePolicy() {
|
|
return enterprisePolicy();
|
|
},
|
|
|
|
// Test only
|
|
async _setMockLinkService(mockLinkService) {
|
|
this.mockLinkService = mockLinkService;
|
|
},
|
|
|
|
heuristicNameToSkipReason(heuristicName) {
|
|
const namesToSkipReason = {
|
|
google: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_GOOGLE_SAFESEARCH,
|
|
youtube: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_YOUTUBE_SAFESEARCH,
|
|
zscalerCanary: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_ZSCALER_CANARY,
|
|
canary: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_CANARY,
|
|
modifiedRoots: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_MODIFIED_ROOTS,
|
|
browserParent:
|
|
Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_PARENTAL_CONTROLS,
|
|
thirdPartyRoots:
|
|
Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_THIRD_PARTY_ROOTS,
|
|
policy: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_ENTERPRISE_POLICY,
|
|
vpn: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_VPN,
|
|
proxy: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_PROXY,
|
|
nrpt: Ci.nsITRRSkipReason.TRR_HEURISTIC_TRIPPED_NRPT,
|
|
};
|
|
|
|
let value = namesToSkipReason[heuristicName];
|
|
if (value != undefined) {
|
|
return value;
|
|
}
|
|
return Ci.nsITRRSkipReason.TRR_FAILED;
|
|
},
|
|
|
|
// Keep this in sync with the description of networking.doh_heuristics_result
|
|
// defined in Scalars.yaml and the equivalent Glean metric in metrics.yaml.
|
|
Telemetry: {
|
|
incomplete: 0,
|
|
pass: 1,
|
|
optOut: 2,
|
|
manuallyDisabled: 3,
|
|
manuallyEnabled: 4,
|
|
enterpriseDisabled: 5,
|
|
enterprisePresent: 6,
|
|
enterpriseEnabled: 7,
|
|
vpn: 8,
|
|
proxy: 9,
|
|
nrpt: 10,
|
|
browserParent: 11,
|
|
modifiedRoots: 12,
|
|
thirdPartyRoots: 13,
|
|
google: 14,
|
|
youtube: 15,
|
|
zscalerCanary: 16,
|
|
canary: 17,
|
|
ignored: 18,
|
|
|
|
heuristicNames() {
|
|
return [
|
|
"google",
|
|
"youtube",
|
|
"zscalerCanary",
|
|
"canary",
|
|
"browserParent",
|
|
"thirdPartyRoots",
|
|
"policy",
|
|
"vpn",
|
|
"proxy",
|
|
"nrpt",
|
|
];
|
|
},
|
|
|
|
fromResults(results) {
|
|
for (let label of Heuristics.Telemetry.heuristicNames()) {
|
|
if (results[label] == Heuristics.DISABLE_DOH) {
|
|
return Heuristics.Telemetry[label];
|
|
}
|
|
}
|
|
return Heuristics.Telemetry.pass;
|
|
},
|
|
},
|
|
};
|
|
|
|
async function dnsLookup(hostname, resolveCanonicalName = false) {
|
|
let lookupPromise = new Promise((resolve, reject) => {
|
|
let request;
|
|
let response = {
|
|
addresses: [],
|
|
};
|
|
let listener = {
|
|
onLookupComplete(inRequest, inRecord, inStatus) {
|
|
if (inRequest === request) {
|
|
if (!Components.isSuccessCode(inStatus)) {
|
|
reject({ message: new Components.Exception("", inStatus).name });
|
|
return;
|
|
}
|
|
inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
|
|
if (resolveCanonicalName) {
|
|
try {
|
|
response.canonicalName = inRecord.canonicalName;
|
|
} catch (e) {
|
|
// no canonicalName
|
|
}
|
|
}
|
|
while (inRecord.hasMore()) {
|
|
let addr = inRecord.getNextAddrAsString();
|
|
// Sometimes there are duplicate records with the same ip.
|
|
if (!response.addresses.includes(addr)) {
|
|
response.addresses.push(addr);
|
|
}
|
|
}
|
|
resolve(response);
|
|
}
|
|
},
|
|
};
|
|
let dnsFlags =
|
|
Ci.nsIDNSService.RESOLVE_TRR_DISABLED_MODE |
|
|
Ci.nsIDNSService.RESOLVE_DISABLE_IPV6 |
|
|
Ci.nsIDNSService.RESOLVE_BYPASS_CACHE |
|
|
Ci.nsIDNSService.RESOLVE_CANONICAL_NAME;
|
|
try {
|
|
request = Services.dns.asyncResolve(
|
|
hostname,
|
|
Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
|
|
dnsFlags,
|
|
null,
|
|
listener,
|
|
null,
|
|
{} /* defaultOriginAttributes */
|
|
);
|
|
} catch (e) {
|
|
// handle exceptions such as offline mode.
|
|
reject({ message: e.name });
|
|
}
|
|
});
|
|
|
|
let addresses, canonicalName, err;
|
|
|
|
try {
|
|
let response = await lookupPromise;
|
|
addresses = response.addresses;
|
|
canonicalName = response.canonicalName;
|
|
} catch (e) {
|
|
addresses = [null];
|
|
err = e.message;
|
|
}
|
|
|
|
return { addresses, canonicalName, err };
|
|
}
|
|
|
|
async function dnsListLookup(domainList) {
|
|
let results = [];
|
|
|
|
let resolutions = await Promise.all(
|
|
domainList.map(domain => dnsLookup(domain))
|
|
);
|
|
for (let { addresses } of resolutions) {
|
|
results = results.concat(addresses);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// TODO: Confirm the expected behavior when filtering is on
|
|
async function globalCanary() {
|
|
let { addresses, err } = await dnsLookup(GLOBAL_CANARY);
|
|
|
|
if (
|
|
err === NXDOMAIN_ERR ||
|
|
!addresses.length ||
|
|
addresses.every(addr =>
|
|
Services.io.hostnameIsLocalIPAddress(Services.io.newURI(`http://${addr}`))
|
|
)
|
|
) {
|
|
return "disable_doh";
|
|
}
|
|
|
|
return "enable_doh";
|
|
}
|
|
|
|
export async function parentalControls() {
|
|
if (lazy.gParentalControlsService.parentalControlsEnabled) {
|
|
return "disable_doh";
|
|
}
|
|
|
|
return "enable_doh";
|
|
}
|
|
|
|
async function thirdPartyRoots() {
|
|
if (Cu.isInAutomation) {
|
|
return "enable_doh";
|
|
}
|
|
|
|
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
|
|
Ci.nsIX509CertDB
|
|
);
|
|
|
|
let hasThirdPartyRoots = await new Promise(resolve => {
|
|
certdb.asyncHasThirdPartyRoots(resolve);
|
|
});
|
|
|
|
if (hasThirdPartyRoots) {
|
|
return "disable_doh";
|
|
}
|
|
|
|
return "enable_doh";
|
|
}
|
|
|
|
async function enterprisePolicy() {
|
|
if (Services.policies.status === Services.policies.ACTIVE) {
|
|
let policies = Services.policies.getActivePolicies();
|
|
|
|
if (!policies.hasOwnProperty("DNSOverHTTPS")) {
|
|
// If DoH isn't in the policy, return that there is a policy (but no DoH specifics)
|
|
return "policy_without_doh";
|
|
}
|
|
|
|
if (policies.DNSOverHTTPS.Enabled === true) {
|
|
// If DoH is enabled in the policy, enable it
|
|
return "enable_doh";
|
|
}
|
|
|
|
// If DoH is disabled in the policy, disable it
|
|
return "disable_doh";
|
|
}
|
|
|
|
// Default return, meaning no policy related to DNSOverHTTPS
|
|
return "no_policy_set";
|
|
}
|
|
|
|
async function safeSearch() {
|
|
const providerList = [
|
|
{
|
|
name: "google",
|
|
unfiltered: ["www.google.com.", "google.com."],
|
|
safeSearch: ["forcesafesearch.google.com."],
|
|
},
|
|
{
|
|
name: "youtube",
|
|
unfiltered: [
|
|
"www.youtube.com.",
|
|
"m.youtube.com.",
|
|
"youtubei.googleapis.com.",
|
|
"youtube.googleapis.com.",
|
|
"www.youtube-nocookie.com.",
|
|
],
|
|
safeSearch: ["restrict.youtube.com.", "restrictmoderate.youtube.com."],
|
|
},
|
|
];
|
|
|
|
async function checkProvider(provider) {
|
|
let [unfilteredAnswers, safeSearchAnswers] = await Promise.all([
|
|
dnsListLookup(provider.unfiltered),
|
|
dnsListLookup(provider.safeSearch),
|
|
]);
|
|
|
|
// Given a provider, check if the answer for any safe search domain
|
|
// matches the answer for any default domain
|
|
for (let answer of safeSearchAnswers) {
|
|
if (answer && unfilteredAnswers.includes(answer)) {
|
|
return { name: provider.name, result: "disable_doh" };
|
|
}
|
|
}
|
|
|
|
return { name: provider.name, result: "enable_doh" };
|
|
}
|
|
|
|
// Compare strict domain lookups to non-strict domain lookups.
|
|
// Resolutions has a type of [{ name, result }]
|
|
let resolutions = await Promise.all(
|
|
providerList.map(provider => checkProvider(provider))
|
|
);
|
|
|
|
// Reduce that array entries into a single map
|
|
return resolutions.reduce(
|
|
(accumulator, check) => {
|
|
accumulator[check.name] = check.result;
|
|
return accumulator;
|
|
},
|
|
{} // accumulator
|
|
);
|
|
}
|
|
|
|
async function zscalerCanary() {
|
|
const ZSCALER_CANARY = "sitereview.zscaler.com.";
|
|
|
|
let { addresses } = await dnsLookup(ZSCALER_CANARY);
|
|
for (let address of addresses) {
|
|
if (
|
|
["213.152.228.242", "199.168.151.251", "8.25.203.30"].includes(address)
|
|
) {
|
|
// if sitereview.zscaler.com resolves to either one of the 3 IPs above,
|
|
// Zscaler Shift service is in use, don't enable DoH
|
|
return "disable_doh";
|
|
}
|
|
}
|
|
|
|
return "enable_doh";
|
|
}
|
|
|
|
async function platform() {
|
|
let platformChecks = {};
|
|
|
|
let indications = Ci.nsINetworkLinkService.NONE_DETECTED;
|
|
try {
|
|
let linkService = lazy.gNetworkLinkService;
|
|
if (Heuristics.mockLinkService) {
|
|
linkService = Heuristics.mockLinkService;
|
|
}
|
|
indications = linkService.platformDNSIndications;
|
|
} catch (e) {
|
|
if (e.result != Cr.NS_ERROR_NOT_IMPLEMENTED) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
platformChecks.vpn =
|
|
indications & Ci.nsINetworkLinkService.VPN_DETECTED
|
|
? "disable_doh"
|
|
: "enable_doh";
|
|
platformChecks.proxy =
|
|
indications & Ci.nsINetworkLinkService.PROXY_DETECTED
|
|
? "disable_doh"
|
|
: "enable_doh";
|
|
platformChecks.nrpt =
|
|
indications & Ci.nsINetworkLinkService.NRPT_DETECTED
|
|
? "disable_doh"
|
|
: "enable_doh";
|
|
|
|
return platformChecks;
|
|
}
|
|
|
|
// Check if the network provides a DoH endpoint to use. Returns the name of the
|
|
// provider if the check is successful, else null. Currently we only support
|
|
// this for Comcast networks.
|
|
async function providerSteering() {
|
|
if (!lazy.DoHConfigController.currentConfig.providerSteering.enabled) {
|
|
return null;
|
|
}
|
|
const TEST_DOMAIN = "doh.test.";
|
|
|
|
// Array of { name, canonicalName, uri } where name is an identifier for
|
|
// telemetry, canonicalName is the expected CNAME when looking up doh.test,
|
|
// and uri is the provider's DoH endpoint.
|
|
let steeredProviders =
|
|
lazy.DoHConfigController.currentConfig.providerSteering.providerList;
|
|
|
|
if (!steeredProviders || !steeredProviders.length) {
|
|
return null;
|
|
}
|
|
|
|
let { canonicalName, err } = await dnsLookup(TEST_DOMAIN, true);
|
|
if (err || !canonicalName) {
|
|
return null;
|
|
}
|
|
|
|
let provider = steeredProviders.find(p => {
|
|
return p.canonicalName == canonicalName;
|
|
});
|
|
if (!provider || !provider.uri || !provider.id) {
|
|
return null;
|
|
}
|
|
|
|
return provider;
|
|
}
|