forked from mirrors/gecko-dev
		
	 f7f289f0dd
			
		
	
	
		f7f289f0dd
		
	
	
	
	
		
			
			Backed out changeset c2121c4069d9 (bug 1822495) Backed out changeset 356e59590921 (bug 1822495)
		
			
				
	
	
		
			405 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			405 lines
		
	
	
	
		
			11 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",
 | |
|   Preferences: "resource://gre/modules/Preferences.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,
 | |
|       modifiedRoots: await modifiedRoots(),
 | |
|       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;
 | |
|   },
 | |
| };
 | |
| 
 | |
| 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";
 | |
| }
 | |
| 
 | |
| async function modifiedRoots() {
 | |
|   // Check for presence of enterprise_roots cert pref. If enabled, disable DoH
 | |
|   let rootsEnabled = lazy.Preferences.get(
 | |
|     "security.enterprise_roots.enabled",
 | |
|     false
 | |
|   );
 | |
| 
 | |
|   if (rootsEnabled) {
 | |
|     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;
 | |
| }
 |