mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-07 18:04:46 +00:00
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:
parent
4d4b40d24a
commit
18e89fb613
@ -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();
|
||||
},
|
||||
|
||||
|
481
browser/components/doh/DoHController.jsm
Normal file
481
browser/components/doh/DoHController.jsm
Normal 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");
|
||||
},
|
||||
};
|
337
browser/components/doh/DoHHeuristics.jsm
Normal file
337
browser/components/doh/DoHHeuristics.jsm
Normal 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;
|
||||
}
|
@ -8,6 +8,8 @@ with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Security')
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'DoHController.jsm',
|
||||
'DoHHeuristics.jsm',
|
||||
'TRRPerformance.jsm',
|
||||
]
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
})();
|
@ -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";
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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)
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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;
|
||||
}
|
@ -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"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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"]
|
||||
|
Loading…
Reference in New Issue
Block a user