Bug 1603779 - Part 1: Implement DoHController.jsm and DoHHeuristics.jsm and minimalize doh-rollout extension. r=valentin

Differential Revision: https://phabricator.services.mozilla.com/D78598
This commit is contained in:
Nihanth Subramanya 2020-07-10 15:13:48 +00:00
parent 4d4b40d24a
commit 18e89fb613
15 changed files with 888 additions and 1464 deletions

View File

@ -694,6 +694,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
"resource://gre/modules/ContextualIdentityService.jsm",
Corroborate: "resource://gre/modules/Corroborate.jsm",
Discovery: "resource:///modules/Discovery.jsm",
DoHController: "resource:///modules/DoHController.jsm",
ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
FirefoxMonitor: "resource:///modules/FirefoxMonitor.jsm",
FxAccounts: "resource://gre/modules/FxAccounts.jsm",
@ -2263,6 +2264,7 @@ BrowserGlue.prototype = {
this._showNewInstallModal();
}
DoHController.init();
FirefoxMonitor.init();
},

View File

@ -0,0 +1,481 @@
/* 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/. */
"use strict";
/*
* This module runs the automated heuristics to enable/disable DoH on different
* networks. Heuristics are run at startup and upon network changes.
* Heuristics are disabled if the user sets their DoH provider or mode manually.
*/
var EXPORTED_SYMBOLS = ["DoHController"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ExtensionStorageIDB",
"resource://gre/modules/ExtensionStorageIDB.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Heuristics",
"resource:///modules/DoHHeuristics.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Preferences",
"resource://gre/modules/Preferences.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"gCaptivePortalService",
"@mozilla.org/network/captive-portal-service;1",
"nsICaptivePortalService"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"gDNSService",
"@mozilla.org/network/dns-service;1",
"nsIDNSService"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"gNetworkLinkService",
"@mozilla.org/network/network-link-service;1",
"nsINetworkLinkService"
);
// Enables this controller. Turned on via Normandy rollout.
const ENABLED_PREF = "doh-rollout.enabled";
// Stores whether we've done first-run.
const FIRST_RUN_PREF = "doh-rollout.doneFirstRun";
// Records if the user opted in/out of DoH study by clicking on doorhanger
const DOORHANGER_USER_DECISION_PREF = "doh-rollout.doorhanger-decision";
// Set when we detect that the user set their DoH provider or mode manually.
// If set, we don't run heuristics.
const DISABLED_PREF = "doh-rollout.disable-heuristics";
// Set when we detect either a non-DoH enterprise policy, or a DoH policy that
// tells us to disable it. This pref's effect is to suppress the opt-out CFR.
const SKIP_HEURISTICS_PREF = "doh-rollout.skipHeuristicsCheck";
const BREADCRUMB_PREF = "doh-rollout.self-enabled";
// Necko TRR prefs to watch for user-set values.
const NETWORK_TRR_MODE_PREF = "network.trr.mode";
const NETWORK_TRR_URI_PREF = "network.trr.uri";
const TRR_LIST_PREF = "network.trr.resolvers";
const ROLLOUT_MODE_PREF = "doh-rollout.mode";
const ROLLOUT_URI_PREF = "doh-rollout.uri";
const TRR_SELECT_ENABLED_PREF = "doh-rollout.trr-selection.enabled";
const TRR_SELECT_DRY_RUN_RESULT_PREF =
"doh-rollout.trr-selection.dry-run-result";
const TRR_SELECT_COMMIT_RESULT_PREF = "doh-rollout.trr-selection.commit-result";
const HEURISTICS_TELEMETRY_CATEGORY = "security.doh.heuristics";
const TRRSELECT_TELEMETRY_CATEGORY = "security.doh.trrPerformance";
const kLinkStatusChangedTopic = "network:link-status-changed";
const kConnectivityTopic = "network:captive-portal-connectivity";
const kPrefChangedTopic = "nsPref:changed";
const DoHController = {
_heuristicsAreEnabled: false,
async init() {
await this.migrateLocalStoragePrefs();
await this.migrateOldTrrMode();
await this.migrateNextDNSEndpoint();
Services.telemetry.setEventRecordingEnabled(
HEURISTICS_TELEMETRY_CATEGORY,
true
);
Services.telemetry.setEventRecordingEnabled(
TRRSELECT_TELEMETRY_CATEGORY,
true
);
Preferences.observe(ENABLED_PREF, this);
Preferences.observe(NETWORK_TRR_MODE_PREF, this);
Preferences.observe(NETWORK_TRR_URI_PREF, this);
if (Preferences.get(ENABLED_PREF, false)) {
await this.maybeEnableHeuristics();
} else if (Preferences.get(FIRST_RUN_PREF, false)) {
await this.rollback();
}
this._asyncShutdownBlocker = async () => {
await this.disableHeuristics();
};
AsyncShutdown.profileBeforeChange.addBlocker(
"DoHController: clear state and remove observers",
this._asyncShutdownBlocker
);
Preferences.set(FIRST_RUN_PREF, true);
},
// Used by tests to reset DoHController state (prefs are not cleared here -
// tests do that when needed between _uninit and init).
async _uninit() {
Preferences.ignore(ENABLED_PREF, this);
Preferences.ignore(NETWORK_TRR_MODE_PREF, this);
Preferences.ignore(NETWORK_TRR_URI_PREF, this);
AsyncShutdown.profileBeforeChange.removeBlocker(this._asyncShutdownBlocker);
await this.disableHeuristics();
},
async migrateLocalStoragePrefs() {
const BALROG_MIGRATION_COMPLETED_PREF = "doh-rollout.balrog-migration-done";
const ADDON_ID = "doh-rollout@mozilla.org";
// Migrate updated local storage item names. If this has already been done once, skip the migration
const isMigrated = Preferences.get(BALROG_MIGRATION_COMPLETED_PREF, false);
if (isMigrated) {
return;
}
let policy = WebExtensionPolicy.getByID(ADDON_ID);
if (!policy) {
return;
}
const storagePrincipal = ExtensionStorageIDB.getStoragePrincipal(
policy.extension
);
const idbConn = await ExtensionStorageIDB.open(storagePrincipal);
// Previously, the DoH heuristics were bundled as an add-on. Early versions
// of this add-on used local storage instead of prefs to persist state. This
// function migrates the values that are still relevant to their new pref
// counterparts.
const legacyLocalStorageKeys = [
"doneFirstRun",
DOORHANGER_USER_DECISION_PREF,
DISABLED_PREF,
];
for (let item of legacyLocalStorageKeys) {
let data = await idbConn.get(item);
let value = data[item];
if (data.hasOwnProperty(item)) {
let migratedName = item;
if (!item.startsWith("doh-rollout.")) {
migratedName = "doh-rollout." + item;
}
Preferences.set(migratedName, value);
}
}
await idbConn.clear();
await idbConn.close();
// Set pref to skip this function in the future.
Preferences.set(BALROG_MIGRATION_COMPLETED_PREF, true);
},
// Previous versions of the DoH frontend worked by setting network.trr.mode
// directly to turn DoH on/off. This makes sure we clear that value and also
// the pref we formerly used to track changes to it.
async migrateOldTrrMode() {
const PREVIOUS_TRR_MODE_PREF = "doh-rollout.previous.trr.mode";
if (Preferences.get(PREVIOUS_TRR_MODE_PREF) === undefined) {
return;
}
Preferences.reset(NETWORK_TRR_MODE_PREF);
Preferences.reset(PREVIOUS_TRR_MODE_PREF);
},
async migrateNextDNSEndpoint() {
// NextDNS endpoint changed from trr.dns.nextdns.io to firefox.dns.nextdns.io
// This migration updates any pref values that might be using the old value
// to the new one. We support values that match the exact URL that shipped
// in the network.trr.resolvers pref in prior versions of the browser.
// The migration is a direct static replacement of the string.
const oldURL = "https://trr.dns.nextdns.io/";
const newURL = "https://firefox.dns.nextdns.io/";
const prefsToMigrate = [
"network.trr.resolvers",
"network.trr.uri",
"network.trr.custom_uri",
"doh-rollout.trr-selection.dry-run-result",
"doh-rollout.uri",
];
for (let pref of prefsToMigrate) {
if (!Preferences.isSet(pref)) {
continue;
}
Preferences.set(pref, Preferences.get(pref).replaceAll(oldURL, newURL));
}
},
// The "maybe" is because there are two cases when we don't enable heuristics:
// 1. If we detect that TRR mode or URI have user values, or we previously
// detected this (i.e. DISABLED_PREF is true)
// 2. If there are any non-DoH enterprise policies active
async maybeEnableHeuristics() {
if (Preferences.get(DISABLED_PREF)) {
return;
}
let policyResult = await Heuristics.checkEnterprisePolicy();
if (["policy_without_doh", "disable_doh"].includes(policyResult)) {
await this.setState("policyDisabled");
Preferences.set(SKIP_HEURISTICS_PREF, true);
return;
}
Preferences.reset(SKIP_HEURISTICS_PREF);
if (
Preferences.isSet(NETWORK_TRR_MODE_PREF) ||
Preferences.isSet(NETWORK_TRR_URI_PREF)
) {
await this.setState("manuallyDisabled");
Preferences.set(DISABLED_PREF, true);
return;
}
await this.runTRRSelection();
await this.runHeuristics("startup");
Services.obs.addObserver(this, kLinkStatusChangedTopic);
Services.obs.addObserver(this, kConnectivityTopic);
this._heuristicsAreEnabled = true;
},
async runHeuristics(evaluateReason) {
let results = await Heuristics.run();
let decision = Object.values(results).includes(Heuristics.DISABLE_DOH)
? Heuristics.DISABLE_DOH
: Heuristics.ENABLE_DOH;
results.evaluateReason = evaluateReason;
if (results.steeredProvider) {
gDNSService.setDetectedTrrURI(results.steeredProvider.uri);
results.steeredProvider = results.steeredProvider.name;
}
if (decision === Heuristics.DISABLE_DOH) {
await this.setState("disabled");
} else {
await this.setState("enabled");
}
Services.telemetry.recordEvent(
HEURISTICS_TELEMETRY_CATEGORY,
"evaluate",
"heuristics",
decision,
results
);
},
async setState(state) {
switch (state) {
case "disabled":
Preferences.set(ROLLOUT_MODE_PREF, 0);
break;
case "UIOk":
Preferences.set(BREADCRUMB_PREF, true);
break;
case "enabled":
Preferences.set(ROLLOUT_MODE_PREF, 2);
Preferences.set(BREADCRUMB_PREF, true);
break;
case "policyDisabled":
case "manuallyDisabled":
case "UIDisabled":
Preferences.reset(BREADCRUMB_PREF);
// Fall through.
case "shutdown":
case "rollback":
Preferences.reset(ROLLOUT_MODE_PREF);
break;
}
Services.telemetry.recordEvent(
HEURISTICS_TELEMETRY_CATEGORY,
"state",
state,
"null"
);
},
async disableHeuristics() {
if (!this._heuristicsAreEnabled) {
return;
}
await this.setState("shutdown");
Services.obs.removeObserver(this, kLinkStatusChangedTopic);
Services.obs.removeObserver(this, kConnectivityTopic);
this._heuristicsAreEnabled = false;
},
async rollback() {
await this.setState("rollback");
await this.disableHeuristics();
},
async runTRRSelection() {
// If persisting the selection is disabled, clear the existing
// selection.
if (!Preferences.get(TRR_SELECT_COMMIT_RESULT_PREF, false)) {
Preferences.reset(ROLLOUT_URI_PREF);
}
if (!Preferences.get(TRR_SELECT_ENABLED_PREF, false)) {
return;
}
if (Preferences.isSet(ROLLOUT_URI_PREF)) {
return;
}
await this.runTRRSelectionDryRun();
// If persisting the selection is disabled, don't commit the value.
if (!Preferences.get(TRR_SELECT_COMMIT_RESULT_PREF, false)) {
return;
}
Preferences.set(
ROLLOUT_URI_PREF,
Preferences.get(TRR_SELECT_DRY_RUN_RESULT_PREF)
);
},
async runTRRSelectionDryRun() {
if (Preferences.isSet(TRR_SELECT_DRY_RUN_RESULT_PREF)) {
// Check whether the existing dry-run-result is in the default
// list of TRRs. If it is, all good. Else, run the dry run again.
let dryRunResult = Preferences.get(TRR_SELECT_DRY_RUN_RESULT_PREF);
let defaultTRRs = JSON.parse(
Services.prefs.getDefaultBranch("").getCharPref(TRR_LIST_PREF)
);
let dryRunResultIsValid = defaultTRRs.some(
trr => trr.url == dryRunResult
);
if (dryRunResultIsValid) {
return;
}
}
let setDryRunResultAndRecordTelemetry = trr => {
Preferences.set(TRR_SELECT_DRY_RUN_RESULT_PREF, trr);
Services.telemetry.recordEvent(
TRRSELECT_TELEMETRY_CATEGORY,
"trrselect",
"dryrunresult",
trr.substring(0, 40) // Telemetry payload max length
);
};
if (Cu.isInAutomation) {
// For mochitests, just record telemetry with a dummy result.
// TRRPerformance.jsm is tested in xpcshell.
setDryRunResultAndRecordTelemetry("https://dummytrr.com/query");
return;
}
// Importing the module here saves us from having to do it at startup, and
// ensures tests have time to set prefs before the module initializes.
let { TRRRacer } = ChromeUtils.import(
"resource:///modules/TRRPerformance.jsm"
);
await new Promise(resolve => {
let racer = new TRRRacer(() => {
setDryRunResultAndRecordTelemetry(racer.getFastestTRR(true));
resolve();
});
racer.run();
});
},
observe(subject, topic, data) {
switch (topic) {
case kLinkStatusChangedTopic:
this.onConnectionChanged();
break;
case kConnectivityTopic:
this.onConnectivityAvailable();
break;
case kPrefChangedTopic:
this.onPrefChanged(data);
break;
}
},
async onPrefChanged(pref) {
switch (pref) {
case ENABLED_PREF:
if (Preferences.get(ENABLED_PREF, false)) {
await this.maybeEnableHeuristics();
} else {
await this.rollback();
}
break;
case NETWORK_TRR_URI_PREF:
case NETWORK_TRR_MODE_PREF:
await this.setState("manuallyDisabled");
Preferences.set(DISABLED_PREF, true);
await this.disableHeuristics();
break;
}
},
async onConnectionChanged() {
if (!gNetworkLinkService.isLinkUp) {
return;
}
if (gCaptivePortalService.state == gCaptivePortalService.LOCKED_PORTAL) {
return;
}
// The network is up and we don't know that we're in a locked portal.
// Run heuristics. If we detect a portal later, we'll run heuristics again
// when it's unlocked. In that case, this run will likely have failed.
await this.runHeuristics("netchange");
},
async onConnectivityAvailable() {
await this.runHeuristics("connectivity");
},
};

View File

@ -0,0 +1,337 @@
/* 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/. */
"use strict";
/*
* 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.
*/
var EXPORTED_SYMBOLS = ["Heuristics"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"gDNSService",
"@mozilla.org/network/dns-service;1",
"nsIDNSService"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"gParentalControlsService",
"@mozilla.org/parental-controls-service;1",
"nsIParentalControlsService"
);
ChromeUtils.defineModuleGetter(
this,
"Preferences",
"resource://gre/modules/Preferences.jsm"
);
const GLOBAL_CANARY = "use-application-dns.net";
const NXDOMAIN_ERR = "NS_ERROR_UNKNOWN_HOST";
const kProviderSteeringEnabledPref = "doh-rollout.provider-steering.enabled";
const kProviderSteeringListPref = "doh-rollout.provider-steering.provider-list";
const Heuristics = {
// String constants used to indicate outcome of heuristics.
ENABLE_DOH: "enable_doh",
DISABLE_DOH: "disable_doh",
async run() {
let safeSearchChecks = await safeSearch();
let results = {
google: safeSearchChecks.google,
youtube: safeSearchChecks.youtube,
zscalerCanary: await zscalerCanary(),
canary: await globalCanary(),
modifiedRoots: await modifiedRoots(),
browserParent: await parentalControls(),
thirdPartyRoots: await thirdPartyRoots(),
policy: await enterprisePolicy(),
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();
},
};
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;
}
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_DISABLE_TRR |
Ci.nsIDNSService.RESOLVE_DISABLE_IPV6 |
Ci.nsIDNSService.RESOLVE_BYPASS_CACHE |
Ci.nsIDNSService.RESOLVE_CANONICAL_NAME;
try {
request = gDNSService.asyncResolve(
hostname,
dnsFlags,
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 = [];
for (let domain of domainList) {
let { addresses } = await dnsLookup(domain);
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) {
return "disable_doh";
}
return "enable_doh";
}
async function modifiedRoots() {
// Check for presence of enterprise_roots cert pref. If enabled, disable DoH
let rootsEnabled = Preferences.get(
"security.enterprise_roots.enabled",
false
);
if (rootsEnabled) {
return "disable_doh";
}
return "enable_doh";
}
async function parentalControls() {
if (Cu.isInAutomation) {
return "enable_doh";
}
if (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 allCerts = certdb.getCerts();
for (let cert of allCerts) {
if (
certdb.isCertTrusted(
cert,
Ci.nsIX509Cert.CA_CERT,
Ci.nsIX509CertDB.TRUSTED_SSL
)
) {
if (!cert.isBuiltInRoot) {
// this cert is a trust anchor that wasn't shipped with the browser
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"],
},
];
// Compare strict domain lookups to non-strict domain lookups
let safeSearchChecks = {};
for (let provider of providerList) {
let providerName = provider.name;
safeSearchChecks[providerName] = "enable_doh";
let results = {};
results.unfilteredAnswers = await dnsListLookup(provider.unfiltered);
results.safeSearchAnswers = await 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 results.safeSearchAnswers) {
if (answer && results.unfilteredAnswers.includes(answer)) {
safeSearchChecks[providerName] = "disable_doh";
}
}
}
return safeSearchChecks;
}
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";
}
// 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 (!Preferences.get(kProviderSteeringEnabledPref, false)) {
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 = Preferences.get(kProviderSteeringListPref, "[]");
try {
steeredProviders = JSON.parse(steeredProviders);
} catch (e) {
console.log("Provider list is invalid JSON, moving on.");
return null;
}
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.name) {
return null;
}
return provider;
}

View File

@ -8,6 +8,8 @@ with Files('**'):
BUG_COMPONENT = ('Firefox', 'Security')
EXTRA_JS_MODULES += [
'DoHController.jsm',
'DoHHeuristics.jsm',
'TRRPerformance.jsm',
]

View File

@ -1,515 +0,0 @@
/* 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/. */
"use strict";
/* global browser, runHeuristics */
let DEBUG;
async function log() {
// eslint-disable-next-line no-constant-condition
if (DEBUG) {
// eslint-disable-next-line no-console
console.log(...arguments);
}
}
// Gate-keeping pref to run the add-on
const DOH_ENABLED_PREF = "doh-rollout.enabled";
// Platform TRR mode pref. Can be used to turn DoH off, on with fallback, or
// on without fallback. Exposed in about:preferences. We turn off our heuristics
// if we *ever* see a user-set value for this pref.
const NETWORK_TRR_MODE_PREF = "network.trr.mode";
// Platform TRR uri pref used to set a custom DoH endpoint. Exposed in about:preferences.
// We turn off heuristics if we *ever* see a user-set value for this pref.
const NETWORK_TRR_URI_PREF = "network.trr.uri";
// Pref that signals to turn DoH to on/off. It mirrors two possible values of
// network.trr.mode:
// 0: Off (default)
// 2: Enabled, but will fall back to 0 on DNS lookup failure
const ROLLOUT_TRR_MODE_PREF = "doh-rollout.mode";
// This preference is set to TRUE when DoH has been enabled via the add-on. It will
// allow the add-on to continue to function without the aid of the Normandy-triggered pref
// of "doh-rollout.enabled". Note that instead of setting it to false, it is cleared.
const DOH_SELF_ENABLED_PREF = "doh-rollout.self-enabled";
// Records if the user opted in/out of DoH study by clicking on doorhanger
const DOH_DOORHANGER_USER_DECISION_PREF = "doh-rollout.doorhanger-decision";
// Records if user has decided to opt out of study, either by disabling via the doorhanger,
// unchecking "DNS-over-HTTPS" with about:preferences, or manually setting network.trr.mode
const DOH_DISABLED_PREF = "doh-rollout.disable-heuristics";
// Set to true when a user has ANY enterprise policy set, making sure to not run
// heuristics, overwritting the policy.
const DOH_SKIP_HEURISTICS_PREF = "doh-rollout.skipHeuristicsCheck";
// Records when the add-on has been run once. This is in place to only check
// network.trr.mode for prefHasUserValue on first run.
const DOH_DONE_FIRST_RUN_PREF = "doh-rollout.doneFirstRun";
// This pref is set once a migration function has ran, updating local storage items to the
// new doh-rollot.X namespace. This applies to both `doneFirstRun` and `skipHeuristicsCheck`.
const DOH_BALROG_MIGRATION_PREF = "doh-rollout.balrog-migration-done";
// This pref used to be part of a cache mechanism to see if the heuristics
// dictated a change in the DoH settings. We now clear it in the trr mode
// migration at startup.
const DOH_PREVIOUS_TRR_MODE_PREF = "doh-rollout.previous.trr.mode";
// If set to true, debug logging will be enabled.
const DOH_DEBUG_PREF = "doh-rollout.debug";
const stateManager = {
async setState(state) {
log("setState: ", state);
switch (state) {
case "uninstalled":
break;
case "disabled":
await rollout.setSetting(ROLLOUT_TRR_MODE_PREF, 0);
break;
case "UIOk":
await rollout.setSetting(DOH_SELF_ENABLED_PREF, true);
break;
case "enabled":
await rollout.setSetting(ROLLOUT_TRR_MODE_PREF, 2);
await rollout.setSetting(DOH_SELF_ENABLED_PREF, true);
break;
case "manuallyDisabled":
case "UIDisabled":
await browser.experiments.preferences.clearUserPref(
DOH_SELF_ENABLED_PREF
);
// Fall through.
case "rollback":
await browser.experiments.preferences.clearUserPref(
ROLLOUT_TRR_MODE_PREF
);
break;
}
await browser.experiments.heuristics.sendStatePing(state);
},
async rememberDisableHeuristics() {
log("Remembering to never run heuristics again");
await rollout.setSetting(DOH_DISABLED_PREF, true);
},
async shouldRunHeuristics() {
// Check if heuristics has been disabled from rememberDisableHeuristics()
let disableHeuristics = await rollout.getSetting(DOH_DISABLED_PREF, false);
let skipHeuristicsCheck = await rollout.getSetting(
DOH_SKIP_HEURISTICS_PREF,
false
);
if (disableHeuristics || skipHeuristicsCheck) {
// Do not modify DoH for this user.
log("shouldRunHeuristics: Will not run heuristics");
return false;
}
return true;
},
};
const rollout = {
// Pretend that there was a network change at the beginning of time.
lastNetworkChangeTime: 0,
async heuristics(evaluateReason) {
let shouldRunHeuristics = await stateManager.shouldRunHeuristics();
if (!shouldRunHeuristics) {
return;
}
// Run heuristics defined in heuristics.js and experiments/heuristics/api.js
let results = await runHeuristics();
// Check if DoH should be disabled
let decision = Object.values(results).includes("disable_doh")
? "disable_doh"
: "enable_doh";
log("Heuristics decision on " + evaluateReason + ": " + decision);
// Send Telemetry on results of heuristics
results.evaluateReason = evaluateReason;
browser.experiments.heuristics.sendHeuristicsPing(decision, results);
if (decision === "disable_doh") {
await stateManager.setState("disabled");
} else {
await stateManager.setState("enabled");
}
},
async getSetting(name, defaultValue) {
let value;
switch (typeof defaultValue) {
case "boolean":
value = await browser.experiments.preferences.getBoolPref(
name,
defaultValue
);
break;
case "number":
value = await browser.experiments.preferences.getIntPref(
name,
defaultValue
);
break;
case "string":
value = await browser.experiments.preferences.getCharPref(
name,
defaultValue
);
break;
default:
throw new Error(
`Invalid defaultValue argument when trying to fetch pref: ${JSON.stringify(
name
)}`
);
}
log({
context: "getSetting",
type: typeof defaultValue,
name,
value,
});
return value;
},
/**
* Exposed
*
* @param {type} name description
* @param {type} value description
* @return {type} description
*/
async setSetting(name, value) {
// Based on type of pref, set pref accordingly
switch (typeof value) {
case "boolean":
await browser.experiments.preferences.setBoolPref(name, value);
break;
case "number":
await browser.experiments.preferences.setIntPref(name, value);
break;
case "string":
await browser.experiments.preferences.setCharPref(name, value);
break;
default:
throw new Error("setSetting typeof value unknown!");
}
log({
context: "setSetting",
type: typeof value,
name,
value,
});
},
async trrPrefUserModifiedCheck() {
let modeHasUserValue = await browser.experiments.preferences.prefHasUserValue(
NETWORK_TRR_MODE_PREF
);
let uriHasUserValue = await browser.experiments.preferences.prefHasUserValue(
NETWORK_TRR_URI_PREF
);
if (modeHasUserValue || uriHasUserValue) {
await stateManager.setState("manuallyDisabled");
await stateManager.rememberDisableHeuristics();
}
},
async enterprisePolicyCheck() {
// Check for Policies before running the rest of the heuristics
let policyEnableDoH = await browser.experiments.heuristics.checkEnterprisePolicies();
log("Enterprise Policy Check:", policyEnableDoH);
// Determine to skip additional heuristics (by presence of an enterprise policy)
if (policyEnableDoH === "no_policy_set") {
// Resetting skipHeuristicsCheck in case a user had a policy and then removed it!
await this.setSetting(DOH_SKIP_HEURISTICS_PREF, false);
return;
}
if (policyEnableDoH === "policy_without_doh") {
await stateManager.setState("disabled");
}
// Don't check for prefHasUserValue if policy is set to disable DoH
await this.setSetting(DOH_SKIP_HEURISTICS_PREF, true);
},
async migrateLocalStoragePrefs() {
// Migrate updated local storage item names. If this has already been done once, skip the migration
const isMigrated = await browser.experiments.preferences.getBoolPref(
DOH_BALROG_MIGRATION_PREF,
false
);
if (isMigrated) {
log("User has already been migrated.");
return;
}
// Check all local storage keys from v1.0.4 users and migrate them to prefs.
// This only applies to keys that have a value.
const legacyLocalStorageKeys = [
"doneFirstRun",
"skipHeuristicsCheck",
DOH_DOORHANGER_USER_DECISION_PREF,
DOH_DISABLED_PREF,
];
for (let item of legacyLocalStorageKeys) {
let data = await browser.storage.local.get(item);
let value = data[item];
log({ context: "migration", item, value });
if (data.hasOwnProperty(item)) {
let migratedName = item;
if (!item.startsWith("doh-rollout.")) {
migratedName = "doh-rollout." + item;
}
await this.setSetting(migratedName, value);
}
}
// Set pref to skip this function in the future.
browser.experiments.preferences.setBoolPref(
DOH_BALROG_MIGRATION_PREF,
true
);
log("User successfully migrated.");
},
// Previous versions of the add-on worked by setting network.trr.mode directly
// to turn DoH on/off. This makes sure we clear that value and also the pref
// we formerly used to track changes to it.
async migrateOldTrrMode() {
const needsMigration = await browser.experiments.preferences.getIntPref(
DOH_PREVIOUS_TRR_MODE_PREF,
-1
);
if (needsMigration === -1) {
log("User's TRR mode prefs already migrated");
return;
}
await browser.experiments.preferences.clearUserPref(NETWORK_TRR_MODE_PREF);
await browser.experiments.preferences.clearUserPref(
DOH_PREVIOUS_TRR_MODE_PREF
);
log("TRR mode prefs migrated");
},
async init() {
log("calling init");
await this.setSetting(DOH_DONE_FIRST_RUN_PREF, true);
// Register the events for sending pings
browser.experiments.heuristics.setupTelemetry();
await this.enterprisePolicyCheck();
await this.trrPrefUserModifiedCheck();
if (!(await stateManager.shouldRunHeuristics())) {
return;
}
// Perform TRR selection before running heuristics.
await browser.experiments.trrselect.run();
log("TRR selection complete!");
let networkStatus = (await browser.networkStatus.getLinkInfo()).status;
let captiveState = "unknown";
try {
captiveState = await browser.captivePortal.getState();
} catch (e) {
// Captive Portal Service is disabled.
}
if (networkStatus == "up" && captiveState != "locked_portal") {
await rollout.heuristics("startup");
}
// Listen for network change events to run heuristics again
browser.networkStatus.onConnectionChanged.addListener(
rollout.onConnectionChanged
);
// Listen to the captive portal when it unlocks
try {
browser.captivePortal.onConnectivityAvailable.addListener(
rollout.onConnectivityAvailable
);
} catch (e) {
// Captive Portal Service is disabled.
}
browser.experiments.preferences.onTRRPrefChanged.addListener(
async function listener() {
await stateManager.setState("manuallyDisabled");
await stateManager.rememberDisableHeuristics();
await setup.stop();
browser.experiments.preferences.onTRRPrefChanged.removeListener(
listener
);
}
);
},
async onConnectionChanged({ status }) {
log("onConnectionChanged", status);
if (status != "up") {
return;
}
let captiveState = "unknown";
try {
captiveState = await browser.captivePortal.getState();
} catch (e) {
// Captive Portal Service is disabled. Run heuristics optimistically, but
// there's a chance the network is unavailable at this point. In that case
// we also wouldn't know when the network is back up. Worst case, we don't
// enable DoH in this case, but that's better than never enabling it.
await rollout.heuristics("netchange");
return;
}
if (captiveState == "locked_portal") {
return;
}
// The network is up and we don't know that we're in a locked portal.
// Run heuristics. When we detect a portal or lack thereof later, we'll run
// heuristics again. In that case, this run will likely have failed.
await rollout.heuristics("netchange");
},
async onConnectivityAvailable() {
log("onConnectivityAvailable");
await rollout.heuristics("connectivity");
},
};
const setup = {
async start() {
const isAddonDisabled = await rollout.getSetting(DOH_DISABLED_PREF, false);
const runAddonPref = await rollout.getSetting(DOH_ENABLED_PREF, false);
const runAddonBypassPref = await rollout.getSetting(
DOH_SELF_ENABLED_PREF,
false
);
const runAddonDoorhangerDecision = await rollout.getSetting(
DOH_DOORHANGER_USER_DECISION_PREF,
""
);
if (isAddonDisabled) {
// Regardless of pref, the user has chosen/heuristics dictated that this add-on should be disabled.
// DoH status will not be modified from whatever the current setting is at runtime
log(
"Addon has been disabled. DoH status will not be modified from current setting"
);
await stateManager.rememberDisableHeuristics();
return;
}
if (runAddonBypassPref) {
// runAddonBypassPref being set means that this is not first-run, and we
// were still running heuristics when we shutdown - so it's safe to
// do the TRR mode migration and clear network.trr.mode.
rollout.migrateOldTrrMode();
}
if (
runAddonPref ||
runAddonBypassPref ||
runAddonDoorhangerDecision === "UIOk" ||
runAddonDoorhangerDecision === "enabled"
) {
rollout.init();
} else {
log("Disabled, aborting!");
}
},
async stop() {
// Remove our listeners.
browser.networkStatus.onConnectionChanged.removeListener(
rollout.onConnectionChanged
);
try {
browser.captivePortal.onConnectivityAvailable.removeListener(
rollout.onConnectivityAvailable
);
} catch (e) {
// Captive Portal Service is disabled.
}
},
};
(async () => {
DEBUG = await browser.experiments.preferences.getBoolPref(
DOH_DEBUG_PREF,
false
);
// Run Migration First, to continue to run rest of start up logic
await rollout.migrateLocalStoragePrefs();
await browser.experiments.preferences.migrateNextDNSEndpoint();
log("Watching `doh-rollout.enabled` pref");
browser.experiments.preferences.onEnabledChanged.addListener(async () => {
let enabled = await rollout.getSetting(DOH_ENABLED_PREF, false);
if (enabled) {
setup.start();
} else {
// Reset the TRR mode if we were running normally with no user-interference.
if (await stateManager.shouldRunHeuristics()) {
await stateManager.setState("rollback");
}
setup.stop();
}
});
if (await rollout.getSetting(DOH_ENABLED_PREF, false)) {
await setup.start();
} else if (
(await rollout.getSetting(DOH_DONE_FIRST_RUN_PREF, false)) &&
(await stateManager.shouldRunHeuristics())
) {
// We previously had turned on DoH, and now after a restart we've been
// rolled back. Reset TRR mode.
await stateManager.setState("rollback");
}
})();

View File

@ -1,159 +0,0 @@
/* 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/. */
"use strict";
/* global Cc, Ci, ExtensionAPI */
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
let pcs = Cc["@mozilla.org/parental-controls-service;1"].getService(
Ci.nsIParentalControlsService
);
const gDNSService = Cc["@mozilla.org/network/dns-service;1"].getService(
Ci.nsIDNSService
);
const HEURISTICS_TELEMETRY_CATEGORY = "doh";
const HEURISTICS_TELEMETRY_EVENTS = {
evaluate: {
methods: ["evaluate"],
objects: ["heuristics"],
extra_keys: [
"google",
"youtube",
"zscalerCanary",
"canary",
"modifiedRoots",
"browserParent",
"thirdPartyRoots",
"policy",
"steeredProvider",
"evaluateReason",
],
record_on_release: true,
},
state: {
methods: ["state"],
objects: [
"enabled",
"disabled",
"manuallyDisabled",
"uninstalled",
"UIOk",
"UIDisabled",
"rollback",
],
extra_keys: [],
record_on_release: true,
},
};
this.heuristics = class heuristics extends ExtensionAPI {
getAPI() {
return {
experiments: {
heuristics: {
async isTesting() {
return Cu.isInAutomation;
},
setupTelemetry() {
// Set up the Telemetry for the heuristics and addon state
Services.telemetry.registerEvents(
HEURISTICS_TELEMETRY_CATEGORY,
HEURISTICS_TELEMETRY_EVENTS
);
},
sendHeuristicsPing(decision, results) {
Services.telemetry.recordEvent(
HEURISTICS_TELEMETRY_CATEGORY,
"evaluate",
"heuristics",
decision,
results
);
},
setDetectedTrrURI(uri) {
gDNSService.setDetectedTrrURI(uri);
},
sendStatePing(state) {
Services.telemetry.recordEvent(
HEURISTICS_TELEMETRY_CATEGORY,
"state",
state,
"null"
);
},
async checkEnterprisePolicies() {
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 checkParentalControls() {
if (Cu.isInAutomation) {
return "enable_doh";
}
if (pcs.parentalControlsEnabled) {
return "disable_doh";
}
return "enable_doh";
},
async checkThirdPartyRoots() {
if (Cu.isInAutomation) {
return "enable_doh";
}
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
let allCerts = certdb.getCerts();
for (let cert of allCerts) {
if (
certdb.isCertTrusted(
cert,
Ci.nsIX509Cert.CA_CERT,
Ci.nsIX509CertDB.TRUSTED_SSL
)
) {
if (!cert.isBuiltInRoot) {
// this cert is a trust anchor that wasn't shipped with the browser
return "disable_doh";
}
}
}
return "enable_doh";
},
},
},
};
}
};

View File

@ -1,124 +0,0 @@
[
{
"namespace": "experiments.heuristics",
"description": "Heuristics for disabling DNS-over-HTTPS (DoH)",
"functions": [
{
"name": "isTesting",
"type": "function",
"description": "Returns true if we are running in automation",
"parameters": [],
"async": true
},
{
"name": "setupTelemetry",
"type": "function",
"description": "Sets up the Telemetry for the addon",
"parameters": [],
"async": false
},
{
"name": "sendHeuristicsPing",
"type": "function",
"description": "Sends a ping for the results of the heuristics",
"parameters": [
{
"name": "decision",
"type": "string"
},
{
"name": "results",
"type": "object",
"properties": {
"google": {
"description": "Indicates whether Google safe-search is enabled",
"type": "string"
},
"youtube": {
"description": "Indicates whether YouTube safe-search is enabled",
"type": "string"
},
"zscalerCanary": {
"description": "Indicates whether Zscaler's Shift is enabled",
"type": "string"
},
"canary": {
"description": "Indicates whether global canary domain was filtered",
"type": "string"
},
"modifiedRoots": {
"description": "Indicates whether enterprise roots are enabled",
"type": "string"
},
"browserParent": {
"description": "Indicates whether browser has enabled parental controls",
"type": "string"
},
"thirdPartyRoots": {
"description": "Indicates whether third-party roots are enabled",
"type": "string"
},
"policy": {
"description": "Indicates whether browser policy blocks DoH",
"type": "string"
},
"steeredProvider": {
"description": "Indicates whether we steered to a provider-endpoint. Value is the name of the provider",
"type": "string"
},
"evaluateReason": {
"description": "Reason why we are running heuristics, e.g. startup",
"type": "string"
}
}
}
],
"async": false
},
{
"name": "setDetectedTrrURI",
"type": "function",
"description": "Sets the TRR URI signalled by the network",
"parameters": [
{
"name": "uri",
"type": "string"
}
]
},
{
"name": "sendStatePing",
"type": "function",
"description": "Sends a ping for the state of the addon",
"parameters": [
{
"name": "state",
"type": "string"
}
],
"async": false
},
{
"name": "checkEnterprisePolicies",
"type": "function",
"description": "Checks for enterprise policies",
"parameters": [],
"async": true
},
{
"name": "checkParentalControls",
"type": "function",
"description": "Checks for browser-based parental controls",
"parameters": [],
"async": true
},
{
"name": "checkThirdPartyRoots",
"type": "function",
"description": "Checks for third party roots",
"parameters": [],
"async": true
}
]
}
]

View File

@ -1,101 +0,0 @@
/* 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/. */
"use strict";
/* global ExtensionAPI, ExtensionCommon */
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
var preferences = class preferences extends ExtensionAPI {
getAPI(context) {
return {
experiments: {
preferences: {
async getIntPref(name, defaultValue) {
return Services.prefs.getIntPref(name, defaultValue);
},
async setIntPref(name, defaultValue) {
return Services.prefs.setIntPref(name, defaultValue);
},
async getBoolPref(name, defaultValue) {
return Services.prefs.getBoolPref(name, defaultValue);
},
async setBoolPref(name, defaultValue) {
return Services.prefs.setBoolPref(name, defaultValue);
},
async getCharPref(name, defaultValue) {
return Services.prefs.getCharPref(name, defaultValue);
},
async setCharPref(name, defaultValue) {
return Services.prefs.setCharPref(name, defaultValue);
},
async clearUserPref(name) {
return Services.prefs.clearUserPref(name);
},
async prefHasUserValue(name) {
return Services.prefs.prefHasUserValue(name);
},
async migrateNextDNSEndpoint() {
// NextDNS endpoint changed from trr.dns.nextdns.io to firefox.dns.nextdns.io
// This migration updates any pref values that might be using the old value
// to the new one. We support values that match the exact URL that shipped
// in the network.trr.resolvers pref in prior versions of the browser.
// The migration is a direct static replacement of the string.
let oldURL = "https://trr.dns.nextdns.io/";
let newURL = "https://firefox.dns.nextdns.io/";
let prefsToMigrate = [
"network.trr.resolvers",
"network.trr.uri",
"network.trr.custom_uri",
"doh-rollout.trr-selection.dry-run-result",
"doh-rollout.uri",
];
for (let pref of prefsToMigrate) {
if (!Services.prefs.prefHasUserValue(pref)) {
continue;
}
Services.prefs.setCharPref(
pref,
Services.prefs.getCharPref(pref).replaceAll(oldURL, newURL)
);
}
},
onEnabledChanged: new ExtensionCommon.EventManager({
context,
name: "preferences.onPrefChanged",
register: fire => {
let observer = () => {
fire.async();
};
Services.prefs.addObserver("doh-rollout.enabled", observer);
return () => {
Services.prefs.removeObserver("doh-rollout.enabled", observer);
};
},
}).api(),
onTRRPrefChanged: new ExtensionCommon.EventManager({
context,
name: "preferences.onPrefChanged",
register: fire => {
let observer = () => {
fire.async();
};
Services.prefs.addObserver("network.trr.uri", observer);
Services.prefs.addObserver("network.trr.mode", observer);
return () => {
Services.prefs.removeObserver("network.trr.uri", observer);
Services.prefs.removeObserver("network.trr.mode", observer);
};
},
}).api(),
},
},
};
}
};

View File

@ -1,179 +0,0 @@
[
{
"namespace": "experiments.preferences",
"description": "A mirror for Services.prefs.",
"events": [
{
"name": "onEnabledChanged",
"type": "function",
"description": "Fired upon changes to `doh-rollout.enabled`.",
"parameters": []
},
{
"name": "onTRRPrefChanged",
"type": "function",
"description": "Fired upon changes to `network.trr.uri` and `network.trr.mode`.",
"parameters": []
}
],
"functions": [
{
"name": "getIntPref",
"type": "function",
"description": "Get the value of a integer preference",
"parameters": [
{
"type": "string",
"name": "name",
"enum": ["doh-rollout.previous.trr.mode"]
},
{
"type": "integer",
"name": "defaultValue"
}
],
"async": true
},
{
"name": "setIntPref",
"type": "function",
"description": "Sets the value of a integer preference",
"parameters": [
{
"type": "string",
"enum": ["doh-rollout.mode"]
},
{
"type": "integer",
"name": "defaultValue"
}
],
"async": true
},
{
"name": "getBoolPref",
"type": "function",
"description": "Get the value of a boolean preference",
"parameters": [
{
"type": "string",
"name": "name",
"enum": [
"doh-rollout.enabled",
"doh-rollout.self-enabled",
"doh-rollout.doorhanger-shown",
"doh-rollout.disable-heuristics",
"doh-rollout.skipHeuristicsCheck",
"doh-rollout.doneFirstRun",
"doh-rollout.balrog-migration-done",
"doh-rollout.debug",
"doh-rollout.provider-steering.enabled",
"security.enterprise_roots.enabled"
]
},
{
"type": "boolean",
"name": "defaultValue"
}
],
"async": true
},
{
"name": "setBoolPref",
"type": "function",
"description": "Sets the value of a boolean preference",
"parameters": [
{
"type": "string",
"enum": [
"doh-rollout.doorhanger-shown",
"doh-rollout.self-enabled",
"doh-rollout.disable-heuristics",
"doh-rollout.doneFirstRun",
"doh-rollout.skipHeuristicsCheck",
"doh-rollout.balrog-migration-done"
]
},
{
"type": "boolean",
"name": "defaultValue"
}
],
"async": true
},
{
"name": "getCharPref",
"type": "function",
"description": "Gets the value of a string preference",
"parameters": [
{
"type": "string",
"enum": [
"doh-rollout.doorhanger-decision",
"doh-rollout.heuristics.mockValues",
"doh-rollout.provider-steering.provider-list"
]
},
{
"type": "string",
"name": "defaultValue"
}
],
"async": true
},
{
"name": "setCharPref",
"type": "function",
"description": "Sets the value of a string preference",
"parameters": [
{
"type": "string",
"enum": ["doh-rollout.doorhanger-decision"]
},
{
"type": "string",
"name": "defaultValue"
}
],
"async": true
},
{
"name": "clearUserPref",
"type": "function",
"description": "Resets value of prefence back to default",
"parameters": [
{
"type": "string",
"enum": [
"doh-rollout.self-enabled",
"doh-rollout.mode",
"network.trr.mode",
"doh-rollout.previous.trr.mode"
]
}
],
"async": true
},
{
"name": "prefHasUserValue",
"type": "function",
"description": "Check if the user has set a value of a preference",
"parameters": [
{
"type": "string",
"name": "name",
"enum": ["network.trr.mode", "network.trr.uri"]
}
],
"async": true
},
{
"name": "migrateNextDNSEndpoint",
"type": "function",
"description": "Migrates any occurrances of old NextDNS endpoint URL in pref values to the new endpoint.",
"parameters": [],
"async": true
}
]
}
]

View File

@ -1,108 +0,0 @@
/* 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/. */
"use strict";
/* global Cc, Ci, ExtensionAPI, TRRRacer */
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
const kEnabledPref = "doh-rollout.trr-selection.enabled";
const kCommitSelectionPref = "doh-rollout.trr-selection.commit-result";
const kDryRunResultPref = "doh-rollout.trr-selection.dry-run-result";
const kRolloutURIPref = "doh-rollout.uri";
const kTRRListPref = "network.trr.resolvers";
const TRRSELECT_TELEMETRY_CATEGORY = "security.doh.trrPerformance";
Services.telemetry.setEventRecordingEnabled(TRRSELECT_TELEMETRY_CATEGORY, true);
this.trrselect = class trrselect extends ExtensionAPI {
getAPI() {
return {
experiments: {
trrselect: {
async dryRun() {
if (Services.prefs.prefHasUserValue(kDryRunResultPref)) {
// Check whether the existing dry-run-result is in the default
// list of TRRs. If it is, all good. Else, run the dry run again.
let dryRunResult = Services.prefs.getCharPref(kDryRunResultPref);
let defaultTRRs = JSON.parse(
Services.prefs.getDefaultBranch("").getCharPref(kTRRListPref)
);
let dryRunResultIsValid = defaultTRRs.some(
trr => trr.url == dryRunResult
);
if (dryRunResultIsValid) {
return;
}
}
let setDryRunResultAndRecordTelemetry = trr => {
Services.prefs.setCharPref(kDryRunResultPref, trr);
Services.telemetry.recordEvent(
TRRSELECT_TELEMETRY_CATEGORY,
"trrselect",
"dryrunresult",
trr.substring(0, 40) // Telemetry payload max length
);
};
if (Cu.isInAutomation) {
// For mochitests, just record telemetry with a dummy result.
// TRRPerformance.jsm is tested in xpcshell.
setDryRunResultAndRecordTelemetry("https://dummytrr.com/query");
return;
}
// Importing the module here saves us from having to do it at add-on
// startup, and ensures tests have time to set prefs before the
// module initializes.
let { TRRRacer } = ChromeUtils.import(
"resource:///modules/TRRPerformance.jsm"
);
await new Promise(resolve => {
let racer = new TRRRacer(() => {
setDryRunResultAndRecordTelemetry(racer.getFastestTRR(true));
resolve();
});
racer.run();
});
},
async run() {
// If persisting the selection is disabled, clear the existing
// selection.
if (!Services.prefs.getBoolPref(kCommitSelectionPref, false)) {
Services.prefs.clearUserPref(kRolloutURIPref);
}
if (!Services.prefs.getBoolPref(kEnabledPref, false)) {
return;
}
// If we already have a selection, nothing to be done.
if (Services.prefs.prefHasUserValue(kRolloutURIPref)) {
return;
}
// Populate the dry-run-result if needed.
await this.dryRun();
// If persisting the selection is disabled, don't commit the value.
if (!Services.prefs.getBoolPref(kCommitSelectionPref, false)) {
return;
}
// All good, commit the value!
Services.prefs.setCharPref(
kRolloutURIPref,
Services.prefs.getCharPref(kDryRunResultPref)
);
},
},
},
};
}
};

View File

@ -1,15 +0,0 @@
[
{
"namespace": "experiments.trrselect",
"description": "API for running TRR performance measurement",
"functions": [
{
"name": "run",
"type": "function",
"description": "Runs TRR performance measurement if necessary and commits best TRR for the client",
"parameters": [],
"async": true
}
]
}
]

View File

@ -1,201 +0,0 @@
/* 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/. */
"use strict";
/* global browser */
/* exported runHeuristics */
const GLOBAL_CANARY = "use-application-dns.net";
const NXDOMAIN_ERR = "NS_ERROR_UNKNOWN_HOST";
async function dnsLookup(hostname, resolveCanonicalName = false) {
let flags = ["disable_trr", "disable_ipv6", "bypass_cache"];
if (resolveCanonicalName) {
flags.push("canonical_name");
}
let addresses, canonicalName, err;
try {
let response = await browser.dns.resolve(hostname, flags);
addresses = response.addresses;
canonicalName = response.canonicalName;
} catch (e) {
addresses = [null];
err = e.message;
}
return { addresses, canonicalName, err };
}
async function dnsListLookup(domainList) {
let results = [];
for (let domain of domainList) {
let { addresses } = await dnsLookup(domain);
results = results.concat(addresses);
}
return results;
}
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"],
},
];
// Compare strict domain lookups to non-strict domain lookups
let safeSearchChecks = {};
for (let provider of providerList) {
let providerName = provider.name;
safeSearchChecks[providerName] = "enable_doh";
let results = {};
results.unfilteredAnswers = await dnsListLookup(provider.unfiltered);
results.safeSearchAnswers = await 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 results.safeSearchAnswers) {
if (answer && results.unfilteredAnswers.includes(answer)) {
safeSearchChecks[providerName] = "disable_doh";
}
}
}
return safeSearchChecks;
}
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";
}
// 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) {
return "disable_doh";
}
return "enable_doh";
}
async function modifiedRoots() {
// Check for presence of enterprise_roots cert pref. If enabled, disable DoH
let rootsEnabled = await browser.experiments.preferences.getBoolPref(
"security.enterprise_roots.enabled",
false
);
if (rootsEnabled) {
return "disable_doh";
}
return "enable_doh";
}
// Check if the network provides a DoH endpoint to use. Returns the name of the
// provider if the check is successful, else null.
async function providerSteering() {
if (
!(await browser.experiments.preferences.getBoolPref(
"doh-rollout.provider-steering.enabled",
false
))
) {
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 = await browser.experiments.preferences.getCharPref(
"doh-rollout.provider-steering.provider-list",
"[]"
);
try {
steeredProviders = JSON.parse(steeredProviders);
} catch (e) {
console.log("Provider list is invalid JSON, moving on.");
return null;
}
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 && p.canonicalName == canonicalName;
});
if (!provider || !provider.uri || !provider.name) {
return null;
}
// We handle this here instead of background.js since we need to set this
// override every time we run heuristics.
browser.experiments.heuristics.setDetectedTrrURI(provider.uri);
return provider.name;
}
async function runHeuristics() {
let safeSearchChecks = await safeSearch();
let results = {
google: safeSearchChecks.google,
youtube: safeSearchChecks.youtube,
zscalerCanary: await zscalerCanary(),
canary: await globalCanary(),
modifiedRoots: await modifiedRoots(),
browserParent: await browser.experiments.heuristics.checkParentalControls(),
thirdPartyRoots: await browser.experiments.heuristics.checkThirdPartyRoots(),
policy: await browser.experiments.heuristics.checkEnterprisePolicies(),
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;
}

View File

@ -1,8 +1,8 @@
{
"manifest_version": 2,
"name": "DoH Roll-Out",
"description": "Mozilla add-on that supports the roll-out of DoH",
"version": "1.3.0",
"description": "This used to be a Mozilla add-on that supported the roll-out of DoH, but now only exists as a stub to enable migrations.",
"version": "2.0.0",
"hidden": true,
@ -11,43 +11,5 @@
"id": "doh-rollout@mozilla.org",
"strict_min_version": "72.0a1"
}
},
"permissions": [
"captivePortal",
"dns",
"networkStatus",
"storage"
],
"background": {
"scripts": ["heuristics.js", "background.js"]
},
"experiment_apis": {
"preferences": {
"schema": "experiments/preferences/schema.json",
"parent": {
"scopes": ["addon_parent"],
"script": "experiments/preferences/api.js",
"paths": [["experiments", "preferences"]]
}
},
"heuristics": {
"schema": "experiments/heuristics/schema.json",
"parent": {
"scopes": ["addon_parent"],
"script": "experiments/heuristics/api.js",
"paths": [["experiments", "heuristics"]]
}
},
"trrselect": {
"schema": "experiments/trrselect/schema.json",
"parent": {
"scopes": ["addon_parent"],
"script": "experiments/trrselect/api.js",
"paths": [["experiments", "trrselect"]]
}
}
}
}

View File

@ -9,29 +9,8 @@ DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
FINAL_TARGET_FILES.features['doh-rollout@mozilla.org'] += [
'background.js',
'heuristics.js',
'manifest.json'
]
FINAL_TARGET_FILES.features['doh-rollout@mozilla.org']["experiments"]["heuristics"] += [
'experiments/heuristics/api.js',
'experiments/heuristics/schema.json'
]
FINAL_TARGET_FILES.features['doh-rollout@mozilla.org']["experiments"]["preferences"] += [
'experiments/preferences/api.js',
'experiments/preferences/schema.json'
]
FINAL_TARGET_FILES.features['doh-rollout@mozilla.org']["experiments"]["trrselect"] += [
'experiments/trrselect/api.js',
'experiments/trrselect/schema.json'
]
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Security')

View File

@ -313,7 +313,7 @@ exp_import:
downloads:
added:
description: >
Sent when downloading a new file. Possible values are in contained in DownloadsCommon::kFileExtensions.
Sent when downloading a new file. Possible values are in contained in DownloadsCommon::kFileExtensions.
All other downloads not in the listare marked as other.
objects: ["fileExtension"]
bug_numbers: [1627676]
@ -2048,6 +2048,69 @@ pictureinpicture:
expiry_version: "86"
release_channel_collection: opt-out
security.doh.heuristics:
evaluate:
methods: ["evaluate"]
objects: ["heuristics"]
bug_numbers:
- 1573840
- 1631609
- 1603779
description: >
Results of DoH heuristics at startup and after network changes.
expiry_version: never
record_in_processes:
- main
release_channel_collection: opt-out
notification_emails:
- nhnt11@mozilla.com
- ddamjanovic@mozilla.com
- seceng-telemetry@mozilla.com
- necko@mozilla.com
products:
- firefox
extra_keys:
google: Google safe search result
youtube: YouTube safe search result
zscalerCanary: ZScaler canary result
canary: Global canary result
modifiedRoots: Whether enterprise roots were enabled
browserParent: Whether OS parental controls were detected
thirdPartyRoots: Whether third party roots were installed
policy: Enterprise policy presence - no policy/with DoH/without DoH.
steeredProvider: Whether we detected a steering provider
evaluateReason: The reason for running heuristics - startup or netchange
state:
methods: ["state"]
objects: [
"enabled",
"disabled",
"manuallyDisabled",
"policyDisabled",
"uninstalled",
"UIOk",
"UIDisabled",
"rollback",
"shutdown",
]
bug_numbers:
- 1573840
- 1631609
- 1603779
description: >
Results of DoH heuristics at startup and after network changes.
expiry_version: never
record_in_processes:
- main
release_channel_collection: opt-out
notification_emails:
- nhnt11@mozilla.com
- ddamjanovic@mozilla.com
- seceng-telemetry@mozilla.com
- necko@mozilla.com
products:
- firefox
security.doh.trrPerformance:
resolved:
objects: ["record"]