Bug 1690105 - Implement RemoteSettings client in DoHConfig.jsm for provider and config data collections. r=dragana,preferences-reviewers,Gijs

Differential Revision: https://phabricator.services.mozilla.com/D103714
This commit is contained in:
Nihanth Subramanya 2021-05-29 23:36:45 +00:00
parent 71eae32383
commit 475c6ea29c
20 changed files with 706 additions and 163 deletions

View File

@ -5,72 +5,287 @@
"use strict";
/*
* This module provides an interface to acces DoH config settings - e.g. whether
* DoH is enabled, whether capabilities are enabled, etc. Currently this just
* provides getters for prefs, but imminently will be extended to read config
* from a Remote Settings collection and filter by client region etc.
* This module provides an interface to access DoH configuration - e.g. whether
* DoH is enabled, whether capabilities are enabled, etc. The configuration is
* sourced from either Remote Settings or pref values, with Remote Settings
* being preferred.
*/
var EXPORTED_SYMBOLS = ["Config"];
var EXPORTED_SYMBOLS = ["DoHConfigController"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(
this,
"Preferences",
"resource://gre/modules/Preferences.jsm"
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const kEnabledPref = "doh-rollout.enabled";
XPCOMUtils.defineLazyModuleGetters(this, {
Preferences: "resource://gre/modules/Preferences.jsm",
Region: "resource://gre/modules/Region.jsm",
RemoteSettings: "resource://services-settings/remote-settings.js",
Services: "resource://gre/modules/Services.jsm",
});
const kTRRSelectionEnabledPref = "doh-rollout.trr-selection.enabled";
const kTRRSelectionCommitResultPref = "doh-rollout.trr-selection.commit-result";
const kGlobalPrefBranch = "doh-rollout";
var kRegionPrefBranch;
const kProviderSteeringEnabledPref = "doh-rollout.provider-steering.enabled";
const kProviderSteeringListPref = "doh-rollout.provider-steering.provider-list";
const kEnabledPref = "enabled";
const kProvidersPref = "provider-list";
const kTRRSelectionEnabledPref = "trr-selection.enabled";
const kTRRSelectionProvidersPref = "trr-selection.provider-list";
const kTRRSelectionCommitResultPref = "trr-selection.commit-result";
const kProviderSteeringEnabledPref = "provider-steering.enabled";
const kProviderSteeringListPref = "provider-steering.provider-list";
const kPrefChangedTopic = "nsPref:changed";
const Config = {
init() {
Preferences.observe(kEnabledPref, this);
const gProvidersCollection = RemoteSettings("doh-providers");
const gConfigCollection = RemoteSettings("doh-config");
function getPrefValueRegionFirst(prefName, defaultValue) {
return (
Preferences.get(`${kRegionPrefBranch}.${prefName}`) ||
Preferences.get(`${kGlobalPrefBranch}.${prefName}`, defaultValue)
);
}
function getProviderListFromPref(prefName) {
try {
return JSON.parse(getPrefValueRegionFirst(prefName, "[]"));
} catch (e) {
Cu.reportError(`DoH provider list not a valid JSON array: ${prefName}`);
}
return [];
}
// Generate a base config object with getters that return pref values. When
// Remote Settings values become available, a new config object will be
// generated from this and specific fields will be replaced by the RS value.
// If we use a class to store base config and instantiate new config objects
// from it, we lose the ability to override getters because they are defined
// as non-configureable properties on class instances. So just use a function.
function makeBaseConfigObject() {
return {
get enabled() {
return getPrefValueRegionFirst(kEnabledPref, false);
},
get providerList() {
return getProviderListFromPref(kProvidersPref);
},
get fallbackProviderURI() {
return this.providerList[0]?.uri;
},
trrSelection: {
get enabled() {
return getPrefValueRegionFirst(kTRRSelectionEnabledPref, false);
},
get commitResult() {
return getPrefValueRegionFirst(kTRRSelectionCommitResultPref, false);
},
get providerList() {
return getProviderListFromPref(kTRRSelectionProvidersPref);
},
},
providerSteering: {
get enabled() {
return getPrefValueRegionFirst(kProviderSteeringEnabledPref, false);
},
get providerList() {
return getProviderListFromPref(kProviderSteeringListPref);
},
},
};
}
const DoHConfigController = {
initComplete: null,
_resolveInitComplete: null,
// This field always contains the current config state, for
// consumer use.
currentConfig: makeBaseConfigObject(),
// Loads the client's region via Region.jsm. This might mean waiting
// until the region is available.
async loadRegion() {
await new Promise(resolve => {
let homeRegion = Preferences.get(`${kGlobalPrefBranch}.home-region`);
if (homeRegion) {
kRegionPrefBranch = `${kGlobalPrefBranch}.${homeRegion.toLowerCase()}`;
resolve();
return;
}
let updateRegionAndResolve = () => {
kRegionPrefBranch = `${kGlobalPrefBranch}.${Region.home.toLowerCase()}`;
Preferences.set(`${kGlobalPrefBranch}.home-region`, Region.home);
resolve();
};
if (Region.home) {
updateRegionAndResolve();
return;
}
Services.obs.addObserver(function obs(sub, top, data) {
Services.obs.removeObserver(obs, Region.REGION_TOPIC);
updateRegionAndResolve();
}, Region.REGION_TOPIC);
});
// Finally, reload config.
await this.updateFromRemoteSettings();
},
async init() {
await this.loadRegion();
Services.prefs.addObserver(`${kGlobalPrefBranch}.`, this, true);
gProvidersCollection.on("sync", this.updateFromRemoteSettings);
gConfigCollection.on("sync", this.updateFromRemoteSettings);
this._resolveInitComplete();
},
// Useful for tests to set prior state before init()
async _uninit() {
await this.initComplete;
Services.prefs.removeObserver(`${kGlobalPrefBranch}`, this);
gProvidersCollection.off("sync", this.updateFromRemoteSettings);
gConfigCollection.off("sync", this.updateFromRemoteSettings);
this.initComplete = new Promise(resolve => {
this._resolveInitComplete = resolve;
});
},
observe(subject, topic, data) {
switch (topic) {
case kPrefChangedTopic:
if (
!data.startsWith(kRegionPrefBranch) &&
data != `${kGlobalPrefBranch}.${kEnabledPref}` &&
data != `${kGlobalPrefBranch}.${kProvidersPref}`
) {
break;
}
this.notifyNewConfig();
break;
}
},
QueryInterface: ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",
]),
// Creates new config object from currently available
// Remote Settings values.
async updateFromRemoteSettings() {
let providers = await gProvidersCollection.get();
let config = await gConfigCollection.get();
let providersById = new Map();
providers.forEach(p => providersById.set(p.id, p));
let configByRegion = new Map();
config.forEach(c => {
c.id = c.id.toLowerCase();
configByRegion.set(c.id, c);
});
let homeRegion = Preferences.get(`${kGlobalPrefBranch}.home-region`);
let localConfig =
configByRegion.get(homeRegion?.toLowerCase()) ||
configByRegion.get("global");
// Make a new config object first, mutate it as needed, then synchronously
// replace the currentConfig object at the end to ensure atomicity.
let newConfig = makeBaseConfigObject();
if (!localConfig) {
DoHConfigController.currentConfig = newConfig;
DoHConfigController.notifyNewConfig();
return;
}
if (localConfig.rolloutEnabled) {
delete newConfig.enabled;
newConfig.enabled = true;
}
let parseProviderList = (list, checkFn) => {
let parsedList = [];
list?.split(",")?.forEach(p => {
p = p.trim();
if (!p.length) {
return;
}
p = providersById.get(p);
if (!p || (checkFn && !checkFn(p))) {
return;
}
parsedList.push(p);
});
return parsedList;
};
let regionalProviders = parseProviderList(localConfig.providers);
if (regionalProviders?.length) {
delete newConfig.providerList;
newConfig.providerList = regionalProviders;
}
if (localConfig.steeringEnabled) {
let steeringProviders = parseProviderList(
localConfig.steeringProviders,
p => p.canonicalName?.length
);
if (steeringProviders?.length) {
delete newConfig.providerSteering.providerList;
newConfig.providerSteering.providerList = steeringProviders;
delete newConfig.providerSteering.enabled;
newConfig.providerSteering.enabled = true;
}
}
if (localConfig.autoDefaultEnabled) {
let defaultProviders = parseProviderList(
localConfig.autoDefaultProviders
);
if (defaultProviders?.length) {
delete newConfig.trrSelection.providerList;
newConfig.trrSelection.providerList = defaultProviders;
delete newConfig.trrSelection.enabled;
newConfig.trrSelection.enabled = true;
}
}
// Finally, update the currentConfig object synchronously.
DoHConfigController.currentConfig = newConfig;
DoHConfigController.notifyNewConfig();
},
kConfigUpdateTopic: "doh-config-updated",
notifyNewConfig() {
Services.obs.notifyObservers(null, this.kConfigUpdateTopic);
},
get enabled() {
return Preferences.get(kEnabledPref, false);
},
trrSelection: {
get enabled() {
return Preferences.get(kTRRSelectionEnabledPref, false);
},
get commitResult() {
return Preferences.get(kTRRSelectionCommitResultPref, false);
},
},
providerSteering: {
get enabled() {
return Preferences.get(kProviderSteeringEnabledPref, false);
},
get providerList() {
return Preferences.get(kProviderSteeringListPref, "[]");
},
},
};
Config.init();
DoHConfigController.initComplete = new Promise(resolve => {
DoHConfigController._resolveInitComplete = resolve;
});
DoHConfigController.init();

View File

@ -20,7 +20,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
ClientID: "resource://gre/modules/ClientID.jsm",
ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
Config: "resource:///modules/DoHConfig.jsm",
DoHConfigController: "resource:///modules/DoHConfig.jsm",
Heuristics: "resource:///modules/DoHHeuristics.jsm",
Preferences: "resource://gre/modules/Preferences.jsm",
setTimeout: "resource://gre/modules/Timer.jsm",
@ -114,8 +114,6 @@ const BREADCRUMB_PREF = "doh-rollout.self-enabled";
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";
@ -168,11 +166,13 @@ const DoHController = {
true
);
Services.obs.addObserver(this, Config.kConfigUpdateTopic);
await DoHConfigController.initComplete;
Services.obs.addObserver(this, DoHConfigController.kConfigUpdateTopic);
Preferences.observe(NETWORK_TRR_MODE_PREF, this);
Preferences.observe(NETWORK_TRR_URI_PREF, this);
if (Config.enabled) {
if (DoHConfigController.currentConfig.enabled) {
await this.maybeEnableHeuristics();
} else if (Preferences.get(FIRST_RUN_PREF, false)) {
await this.rollback();
@ -193,7 +193,7 @@ const DoHController = {
// Also used by tests to reset DoHController state (prefs are not cleared
// here - tests do that when needed between _uninit and init).
async _uninit() {
Services.obs.removeObserver(this, Config.kConfigUpdateTopic);
Services.obs.removeObserver(this, DoHConfigController.kConfigUpdateTopic);
Preferences.ignore(NETWORK_TRR_MODE_PREF, this);
Preferences.ignore(NETWORK_TRR_URI_PREF, this);
AsyncShutdown.profileBeforeChange.removeBlocker(this._asyncShutdownBlocker);
@ -201,9 +201,15 @@ const DoHController = {
},
// Called to reset state when a new config is available.
resetPromise: Promise.resolve(),
async reset() {
await this._uninit();
await this.init();
this.resetPromise = this.resetPromise.then(async () => {
await this._uninit();
await this.init();
Services.obs.notifyObservers(null, "doh:controller-reloaded");
});
return this.resetPromise;
},
async migrateLocalStoragePrefs() {
@ -328,6 +334,14 @@ const DoHController = {
}
await this.runTRRSelection();
// If we enter this branch it means that no automatic selection was possible.
// In this case, we try to set a fallback (as defined by DoHConfigController).
if (!Preferences.isSet(ROLLOUT_URI_PREF)) {
Preferences.set(
ROLLOUT_URI_PREF,
DoHConfigController.currentConfig.fallbackProviderURI
);
}
this.runHeuristicsThrottled("startup");
Services.obs.addObserver(this, kLinkStatusChangedTopic);
Services.obs.addObserver(this, kConnectivityTopic);
@ -560,22 +574,26 @@ const DoHController = {
async runTRRSelection() {
// If persisting the selection is disabled, clear the existing
// selection.
if (!Config.trrSelection.commitResult) {
if (!DoHConfigController.currentConfig.trrSelection.commitResult) {
Preferences.reset(ROLLOUT_URI_PREF);
}
if (!Config.trrSelection.enabled) {
if (!DoHConfigController.currentConfig.trrSelection.enabled) {
return;
}
if (Preferences.isSet(ROLLOUT_URI_PREF)) {
if (
Preferences.isSet(ROLLOUT_URI_PREF) &&
Preferences.get(ROLLOUT_URI_PREF) ==
Preferences.get(TRR_SELECT_DRY_RUN_RESULT_PREF)
) {
return;
}
await this.runTRRSelectionDryRun();
// If persisting the selection is disabled, don't commit the value.
if (!Config.trrSelection.commitResult) {
if (!DoHConfigController.currentConfig.trrSelection.commitResult) {
return;
}
@ -590,31 +608,28 @@ const DoHController = {
// 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
let dryRunResultIsValid = DoHConfigController.currentConfig.providerList.some(
trr => trr.uri == dryRunResult
);
if (dryRunResultIsValid) {
return;
}
}
let setDryRunResultAndRecordTelemetry = trr => {
Preferences.set(TRR_SELECT_DRY_RUN_RESULT_PREF, trr);
let setDryRunResultAndRecordTelemetry = trrUri => {
Preferences.set(TRR_SELECT_DRY_RUN_RESULT_PREF, trrUri);
Services.telemetry.recordEvent(
TRRSELECT_TELEMETRY_CATEGORY,
"trrselect",
"dryrunresult",
trr.substring(0, 40) // Telemetry payload max length
trrUri.substring(0, 40) // Telemetry payload max length
);
};
if (kIsInAutomation) {
// For mochitests, just record telemetry with a dummy result.
// TRRPerformance.jsm is tested in xpcshell.
setDryRunResultAndRecordTelemetry("https://dummytrr.com/query");
setDryRunResultAndRecordTelemetry("https://example.com/dns-query");
return;
}
@ -624,10 +639,13 @@ const DoHController = {
"resource:///modules/TRRPerformance.jsm"
);
await new Promise(resolve => {
let trrList = DoHConfigController.currentConfig.trrSelection.providerList.map(
trr => trr.uri
);
let racer = new TRRRacer(() => {
setDryRunResultAndRecordTelemetry(racer.getFastestTRR(true));
resolve();
});
}, trrList);
racer.run();
});
},
@ -643,7 +661,7 @@ const DoHController = {
case kPrefChangedTopic:
this.onPrefChanged(data);
break;
case Config.kConfigUpdateTopic:
case DoHConfigController.kConfigUpdateTopic:
this.reset();
break;
}

View File

@ -39,7 +39,7 @@ XPCOMUtils.defineLazyServiceGetter(
ChromeUtils.defineModuleGetter(
this,
"Config",
"DoHConfigController",
"resource:///modules/DoHConfig.jsm"
);
@ -363,7 +363,7 @@ async function platform() {
// provider if the check is successful, else null. Currently we only support
// this for Comcast networks.
async function providerSteering() {
if (!Config.providerSteering.enabled) {
if (!DoHConfigController.currentConfig.providerSteering.enabled) {
return null;
}
const TEST_DOMAIN = "doh.test.";
@ -371,13 +371,8 @@ async function providerSteering() {
// 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 = Config.providerSteering.providerList;
try {
steeredProviders = JSON.parse(steeredProviders);
} catch (e) {
console.log("Provider list is invalid JSON, moving on.");
return null;
}
let steeredProviders =
DoHConfigController.currentConfig.providerSteering.providerList;
if (!steeredProviders || !steeredProviders.length) {
return null;

View File

@ -0,0 +1,142 @@
/* 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";
var EXPORTED_SYMBOLS = ["DoHTestUtils"];
ChromeUtils.defineModuleGetter(
this,
"RemoteSettings",
"resource://services-settings/remote-settings.js"
);
ChromeUtils.defineModuleGetter(
this,
"TestUtils",
"resource://testing-common/TestUtils.jsm"
);
const kConfigCollectionKey = "doh-config";
const kProviderCollectionKey = "doh-providers";
const kConfigUpdateTopic = "doh-config-updated";
const kControllerReloadedTopic = "doh:controller-reloaded";
/*
* Some helpers for loading and modifying DoH config in
* Remote Settings. Call resetRemoteSettingsConfig to set up
* basic default config that omits external URLs. Use
* waitForConfigFlush to wait for DoH actors to pick up changes.
*
* Some tests need to load/reset config while DoH actors are
* uninitialized. Pass waitForConfigFlushes = false in these cases.
*/
const DoHTestUtils = {
providers: [
{
uri: "https://example.com/1",
UIName: "Example 1",
autoDefault: false,
canonicalName: "",
id: "example-1",
},
{
uri: "https://example.com/2",
UIName: "Example 2",
autoDefault: false,
canonicalName: "",
id: "example-2",
},
],
async loadRemoteSettingsProviders(providers, waitForConfigFlushes = true) {
let configFlushedPromise = this.waitForConfigFlush(waitForConfigFlushes);
let providerRS = RemoteSettings(kProviderCollectionKey);
let db = await providerRS.db;
await db.importChanges({}, Date.now(), providers, { clear: true });
// Trigger a sync.
await this.triggerSync(providerRS);
await configFlushedPromise;
},
async loadRemoteSettingsConfig(config, waitForConfigFlushes = true) {
let configFlushedPromise = this.waitForConfigFlush(waitForConfigFlushes);
let configRS = RemoteSettings(kConfigCollectionKey);
let db = await configRS.db;
await db.importChanges({}, Date.now(), [config]);
// Trigger a sync.
await this.triggerSync(configRS);
await configFlushedPromise;
},
// Loads default config for testing without clearing existing entries.
async loadDefaultRemoteSettingsConfig(waitForConfigFlushes = true) {
await this.loadRemoteSettingsProviders(
this.providers,
waitForConfigFlushes
);
await this.loadRemoteSettingsConfig(
{
providers: "example-1, example-2",
rolloutEnabled: false,
steeringEnabled: false,
steeringProviders: "",
autoDefaultEnabled: false,
autoDefaultProviders: "",
id: "global",
},
waitForConfigFlushes
);
},
// Clears existing config AND loads defaults.
async resetRemoteSettingsConfig(waitForConfigFlushes = true) {
let providerRS = RemoteSettings(kProviderCollectionKey);
let configRS = RemoteSettings(kConfigCollectionKey);
for (let rs of [providerRS, configRS]) {
let configFlushedPromise = this.waitForConfigFlush(waitForConfigFlushes);
await rs.db.importChanges({}, Date.now(), [], { clear: true });
// Trigger a sync to clear.
await this.triggerSync(rs);
await configFlushedPromise;
}
await this.loadDefaultRemoteSettingsConfig(waitForConfigFlushes);
},
triggerSync(rs) {
return rs.emit("sync", {
data: {
current: [],
},
});
},
waitForConfigUpdate() {
return TestUtils.topicObserved(kConfigUpdateTopic);
},
waitForControllerReload() {
return TestUtils.topicObserved(kControllerReloadedTopic);
},
waitForConfigFlush(shouldWait = true) {
if (!shouldWait) {
return Promise.resolve();
}
return Promise.all([
this.waitForConfigUpdate(),
this.waitForControllerReload(),
]);
},
};

View File

@ -58,11 +58,6 @@ XPCOMUtils.defineLazyServiceGetter(
"nsIUUIDGenerator"
);
// The list of participating TRRs.
const kTRRs = JSON.parse(
Services.prefs.getDefaultBranch("").getCharPref("network.trr.resolvers")
).map(trr => trr.url);
// The canonical domain whose subdomains we will be resolving.
XPCOMUtils.defineLazyPreferenceGetter(
this,
@ -154,8 +149,9 @@ DNSLookup.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]);
// triggered and the results aggregated before telemetry is sent. If aborted,
// any aggregated results are discarded.
class LookupAggregator {
constructor(onCompleteCallback) {
constructor(onCompleteCallback, trrList) {
this.onCompleteCallback = onCompleteCallback;
this.trrList = trrList;
this.aborted = false;
this.networkUnstable = false;
this.captivePortal = false;
@ -166,7 +162,7 @@ class LookupAggregator {
this.domains.push(null);
}
this.domains.push(...kPopularDomains);
this.totalLookups = kTRRs.length * this.domains.length;
this.totalLookups = this.trrList.length * this.domains.length;
this.completedLookups = 0;
this.results = [];
}
@ -178,7 +174,7 @@ class LookupAggregator {
}
this._ran = true;
for (let trr of kTRRs) {
for (let trr of this.trrList) {
for (let domain of this.domains) {
new DNSLookup(
domain,
@ -256,11 +252,12 @@ class LookupAggregator {
// spawned next time we get a link, up to 5 times. On the fifth time, we just
// let the aggegator complete and mark it as tainted.
class TRRRacer {
constructor(onCompleteCallback) {
constructor(onCompleteCallback, trrList) {
this._aggregator = null;
this._retryCount = 0;
this._complete = false;
this._onCompleteCallback = onCompleteCallback;
this._trrList = trrList;
}
run() {
@ -365,7 +362,10 @@ class TRRRacer {
}
_runNewAggregator() {
this._aggregator = new LookupAggregator(() => this.onComplete());
this._aggregator = new LookupAggregator(
() => this.onComplete(),
this._trrList
);
this._aggregator.run();
this._retryCount++;
}

View File

@ -14,5 +14,9 @@ EXTRA_JS_MODULES += [
"TRRPerformance.jsm",
]
TESTING_JS_MODULES += [
"DoHTestUtils.jsm",
]
XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]

View File

@ -8,6 +8,10 @@ head = head.js
[browser_NextDNSMigration.js]
[browser_policyOverride.js]
[browser_providerSteering.js]
[browser_remoteSettings_newProfile.js]
skip-if = os == 'win' && bits == 32 # Bug 1713464
[browser_remoteSettings_rollout.js]
skip-if = os == 'win' && bits == 32 # Bug 1713464
[browser_rollback.js]
[browser_trrMode_migration.js]
[browser_trrSelect.js]

View File

@ -17,7 +17,7 @@ add_task(async function testCleanFlow() {
is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"TRR selection complete."
);
await checkTRRSelectionTelemetry();

View File

@ -17,7 +17,7 @@ add_task(async function testDoorhangerUserReject() {
is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"TRR selection complete."
);
await checkTRRSelectionTelemetry();

View File

@ -5,7 +5,7 @@
"use strict";
const TEST_DOMAIN = "doh.test.";
const AUTO_TRR_URI = "https://dummytrr.com/query";
const AUTO_TRR_URI = "https://example.com/dns-query";
add_task(setup);

View File

@ -0,0 +1,94 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
add_task(setup);
add_task(setupRegion);
add_task(async function testNewProfile() {
is(
DoHConfigController.currentConfig.enabled,
false,
"Rollout should not be enabled"
);
let provider1 = {
id: "provider1",
uri: "https://example.org/1",
autoDefault: true,
};
let provider2 = {
id: "provider2",
uri: "https://example.org/2",
canonicalName: "https://example.org/cname",
};
let provider3 = {
id: "provider3",
uri: "https://example.org/3",
autoDefault: true,
};
await DoHTestUtils.loadRemoteSettingsProviders([
provider1,
provider2,
provider3,
]);
await DoHTestUtils.loadRemoteSettingsConfig({
id: kTestRegion.toLowerCase(),
rolloutEnabled: true,
providers: "provider1, provider3",
steeringEnabled: true,
steeringProviders: "provider2",
autoDefaultEnabled: true,
autoDefaultProviders: "provider1, provider3",
});
is(
DoHConfigController.currentConfig.enabled,
true,
"Rollout should be enabled"
);
await ensureTRRMode(2);
Assert.deepEqual(
DoHConfigController.currentConfig.providerList,
[provider1, provider3],
"Provider list should be loaded"
);
is(
DoHConfigController.currentConfig.providerSteering.enabled,
true,
"Steering should be enabled"
);
Assert.deepEqual(
DoHConfigController.currentConfig.providerSteering.providerList,
[provider2],
"Steering provider list should be loaded"
);
is(
DoHConfigController.currentConfig.trrSelection.enabled,
true,
"TRR Selection should be enabled"
);
Assert.deepEqual(
DoHConfigController.currentConfig.trrSelection.providerList,
[provider1, provider3],
"TRR Selection provider list should be loaded"
);
is(
DoHConfigController.currentConfig.fallbackProviderURI,
provider1.uri,
"Fallback provider URI should be that of the first one"
);
await DoHTestUtils.resetRemoteSettingsConfig();
is(
DoHConfigController.currentConfig.enabled,
false,
"Rollout should be disabled"
);
await ensureTRRMode(undefined);
});

View File

@ -0,0 +1,69 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
add_task(setup);
add_task(setupRegion);
add_task(async function testPrefFirstRollout() {
setPassingHeuristics();
is(
DoHConfigController.currentConfig.enabled,
false,
"Rollout should not be enabled"
);
let configFlushedPromise = DoHTestUtils.waitForConfigFlush();
Preferences.set(`${kRegionalPrefNamespace}.enabled`, true);
await configFlushedPromise;
is(
DoHConfigController.currentConfig.enabled,
true,
"Rollout should be enabled"
);
await ensureTRRMode(2);
await DoHTestUtils.loadRemoteSettingsProviders([
{
id: "provider1",
uri: "https://example.org/1",
autoDefault: true,
},
]);
await DoHTestUtils.loadRemoteSettingsConfig({
id: kTestRegion.toLowerCase(),
rolloutEnabled: true,
providers: "provider1",
});
is(
DoHConfigController.currentConfig.enabled,
true,
"Rollout should still be enabled"
);
let configUpdatedPromise = DoHTestUtils.waitForConfigUpdate();
Preferences.reset(`${kRegionalPrefNamespace}.enabled`);
await configUpdatedPromise;
is(
DoHConfigController.currentConfig.enabled,
true,
"Rollout should still be enabled"
);
await ensureTRRMode(2);
await DoHTestUtils.resetRemoteSettingsConfig();
is(
DoHConfigController.currentConfig.enabled,
false,
"Rollout should not be enabled"
);
await ensureTRRMode(undefined);
});

View File

@ -19,7 +19,7 @@ add_task(async function testRollback() {
is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"TRR selection complete."
);
await checkTRRSelectionTelemetry();

View File

@ -7,20 +7,6 @@
add_task(setup);
add_task(async function testTRRSelect() {
// Set up the resolver lists in the default and user pref branches.
// dummyTRR3 which only exists in the user-branch value should be ignored.
let oldResolverList = Services.prefs.getCharPref("network.trr.resolvers");
Services.prefs
.getDefaultBranch("")
.setCharPref(
"network.trr.resolvers",
`[{"url": "https://dummytrr.com/query"}, {"url": "https://dummytrr2.com/query"}]`
);
Services.prefs.setCharPref(
"network.trr.resolvers",
`[{"url": "https://dummytrr.com/query"}, {"url": "https://dummytrr2.com/query"}, {"url": "https://dummytrr3.com/query"}]`
);
// Clean start: doh-rollout.uri should be set after init.
setPassingHeuristics();
let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
@ -29,7 +15,7 @@ add_task(async function testTRRSelect() {
is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"TRR selection complete."
);
@ -46,7 +32,7 @@ add_task(async function testTRRSelect() {
await prefPromise;
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"TRR selection complete."
);
@ -54,13 +40,17 @@ add_task(async function testTRRSelect() {
await ensureTRRMode(2);
await checkHeuristicsTelemetry("enable_doh", "startup");
// Disable committing and reset. The committed URI should be cleared but the
// dry-run-result should persist.
// Disable committing and reset. The committed URI should be reset to the
// default provider and the dry-run-result should persist.
Preferences.set(prefs.TRR_SELECT_COMMIT_PREF, false);
prefPromise = TestUtils.waitForPrefChange(prefs.TRR_SELECT_URI_PREF);
await restartDoHController();
await prefPromise;
ok(!Preferences.isSet(prefs.TRR_SELECT_URI_PREF), "TRR selection cleared.");
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://example.com/1",
"Default TRR selected."
);
try {
await BrowserTestUtils.waitForCondition(() => {
return !Preferences.isSet(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF);
@ -71,7 +61,7 @@ add_task(async function testTRRSelect() {
}
is(
Preferences.get(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"dry-run result has the correct value."
);
@ -86,15 +76,23 @@ add_task(async function testTRRSelect() {
await restartDoHController();
try {
await BrowserTestUtils.waitForCondition(() => {
return Preferences.get(prefs.TRR_SELECT_URI_PREF);
return (
Preferences.get(prefs.TRR_SELECT_URI_PREF) ==
"https://example.com/dns-query"
);
});
ok(false, "Dry run result got committed, fail!");
} catch (e) {
ok(true, "Dry run result did not get committed");
}
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://example.com/1",
"Default TRR selected."
);
is(
Preferences.get(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"TRR selection complete, dry-run result recorded."
);
Preferences.set(prefs.TRR_SELECT_COMMIT_PREF, true);
@ -108,14 +106,17 @@ add_task(async function testTRRSelect() {
Preferences.reset(prefs.TRR_SELECT_URI_PREF);
Preferences.set(
prefs.TRR_SELECT_DRY_RUN_RESULT_PREF,
"https://dummytrr2.com/query"
"https://example.com/2"
);
prefPromise = TestUtils.waitForPrefChange(
prefs.TRR_SELECT_URI_PREF,
newVal => newVal == "https://example.com/2"
);
prefPromise = TestUtils.waitForPrefChange(prefs.TRR_SELECT_URI_PREF);
await restartDoHController();
await prefPromise;
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://dummytrr2.com/query",
"https://example.com/2",
"TRR selection complete, existing dry-run-result committed."
);
@ -128,23 +129,18 @@ add_task(async function testTRRSelect() {
Preferences.reset(prefs.TRR_SELECT_URI_PREF);
Preferences.set(
prefs.TRR_SELECT_DRY_RUN_RESULT_PREF,
"https://dummytrr3.com/query"
"https://example.com/4"
);
prefPromise = TestUtils.waitForPrefChange(prefs.TRR_SELECT_URI_PREF);
await restartDoHController();
await prefPromise;
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"TRR selection complete, existing dry-run-result discarded and refreshed."
);
// Wait for heuristics to complete.
await ensureTRRMode(2);
await checkHeuristicsTelemetry("enable_doh", "startup");
Services.prefs
.getDefaultBranch("")
.setCharPref("network.trr.resolvers", oldResolverList);
Services.prefs.clearUserPref("network.trr.resolvers");
});

View File

@ -23,8 +23,8 @@ add_task(async function testTrrSelectionDisable() {
);
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
undefined,
"doh-rollout.uri remained unset."
"https://example.com/1",
"doh-rollout.uri set to first provider in the list."
);
ensureNoTRRSelectionTelemetry();
@ -62,8 +62,8 @@ add_task(async function testTrrSelectionDisable() {
);
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
undefined,
"doh-rollout.uri remained unset."
"https://example.com/1",
"doh-rollout.uri set to first provider in the list."
);
await ensureTRRMode(2);
await checkHeuristicsTelemetry("enable_doh", "startup");

View File

@ -17,7 +17,7 @@ add_task(async function testUserInterference() {
is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
is(
Preferences.get(prefs.TRR_SELECT_URI_PREF),
"https://dummytrr.com/query",
"https://example.com/dns-query",
"TRR selection complete."
);
await checkTRRSelectionTelemetry();

View File

@ -1,22 +1,15 @@
"use strict";
ChromeUtils.defineModuleGetter(
this,
"ASRouter",
"resource://activity-stream/lib/ASRouter.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"DoHController",
"resource:///modules/DoHController.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Preferences",
"resource://gre/modules/Preferences.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
ASRouter: "resource://activity-stream/lib/ASRouter.jsm",
DoHController: "resource:///modules/DoHController.jsm",
DoHConfigController: "resource:///modules/DoHConfig.jsm",
DoHTestUtils: "resource://testing-common/DoHTestUtils.jsm",
Preferences: "resource://gre/modules/Preferences.jsm",
Region: "resource://gre/modules/Region.jsm",
RegionTestUtils: "resource://testing-common/RegionTestUtils.jsm",
RemoteSettings: "resource://services-settings/remote-settings.js",
});
XPCOMUtils.defineLazyServiceGetter(
this,
@ -52,6 +45,7 @@ const prefs = {
FIRST_RUN_PREF: "doh-rollout.doneFirstRun",
BALROG_MIGRATION_PREF: "doh-rollout.balrog-migration-done",
PREVIOUS_TRR_MODE_PREF: "doh-rollout.previous.trr.mode",
PROVIDER_LIST_PREF: "doh-rollout.provider-list",
TRR_SELECT_ENABLED_PREF: "doh-rollout.trr-selection.enabled",
TRR_SELECT_URI_PREF: "doh-rollout.uri",
TRR_SELECT_COMMIT_PREF: "doh-rollout.trr-selection.commit-result",
@ -74,6 +68,8 @@ const CFR_JSON = {
};
async function setup() {
await DoHController._uninit();
await DoHConfigController._uninit();
SpecialPowers.pushPrefEnv({
set: [["security.notification_enable_delay", 0]],
});
@ -128,6 +124,13 @@ async function setup() {
// Global canary
gDNSOverride.addIPOverride("use-application-dns.net.", "4.1.1.1");
await DoHTestUtils.resetRemoteSettingsConfig(false);
await DoHConfigController.init();
await DoHController.init();
await waitForStateTelemetry(["rollback"]);
registerCleanupFunction(async () => {
Services.telemetry.canRecordExtended = oldCanRecord;
Services.telemetry.clearEvents();
@ -141,10 +144,23 @@ async function setup() {
await DoHController._uninit();
Services.telemetry.clearEvents();
Preferences.reset(Object.values(prefs));
await DoHTestUtils.resetRemoteSettingsConfig(false);
await DoHController.init();
});
}
const kTestRegion = "DE";
const kRegionalPrefNamespace = `doh-rollout.${kTestRegion.toLowerCase()}`;
async function setupRegion() {
Region._home = null;
RegionTestUtils.setNetworkRegion(kTestRegion);
await Region._fetchRegion();
is(Region.home, kTestRegion, "Should have correct region");
Preferences.reset("doh-rollout.home-region");
await DoHConfigController.loadRegion();
}
async function checkTRRSelectionTelemetry() {
let events;
await TestUtils.waitForCondition(() => {
@ -162,7 +178,7 @@ async function checkTRRSelectionTelemetry() {
is(events.length, 1, "Found the expected trrselect event.");
is(
events[0][4],
"https://dummytrr.com/query",
"https://example.com/dns-query",
"The event records the expected decision"
);
}

View File

@ -14,7 +14,7 @@ const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
let h2Port, trrServer1, trrServer2;
let h2Port, trrServer1, trrServer2, trrList;
let DNSLookup, LookupAggregator, TRRRacer;
function readFile(file) {
@ -63,6 +63,7 @@ function setup() {
// use the h2 server as DOH provider
trrServer1 = `https://foo.example.com:${h2Port}/doh?responseIP=1.1.1.1`;
trrServer2 = `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`;
trrList = [trrServer1, trrServer2];
// make all native resolve calls "secretly" resolve localhost instead
Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
@ -85,16 +86,6 @@ function setup() {
"firefox-dns-perf-test.net."
);
let defaultPrefBranch = Services.prefs.getDefaultBranch("");
let origResolverList = defaultPrefBranch.getCharPref("network.trr.resolvers");
Services.prefs
.getDefaultBranch("")
.setCharPref(
"network.trr.resolvers",
`[{"url": "${trrServer1}"}, {"url": "${trrServer2}"}]`
);
let TRRPerformance = ChromeUtils.import(
"resource:///modules/TRRPerformance.jsm"
);
@ -110,7 +101,6 @@ function setup() {
Services.prefs.clearUserPref("network.http.spdy.enabled");
Services.prefs.clearUserPref("network.http.spdy.enabled.http2");
Services.prefs.clearUserPref("network.dns.native-is-localhost");
defaultPrefBranch.setCharPref("network.trr.resolvers", origResolverList);
Services.telemetry.canRecordExtended = oldCanRecord;
});

View File

@ -13,7 +13,7 @@ async function helper_SuccessfulLookupAggregator(
captivePortal = false
) {
let deferred = PromiseUtils.defer();
let aggregator = new LookupAggregator(() => deferred.resolve());
let aggregator = new LookupAggregator(() => deferred.resolve(), trrList);
// The aggregator's domain list should correctly reflect our set
// prefs for number of random subdomains (2) and the list of
// popular domains.
@ -124,7 +124,7 @@ add_task(async function test_SuccessfulLookupAggregator() {
add_task(async function test_AbortedLookupAggregator() {
let deferred = PromiseUtils.defer();
let aggregator = new LookupAggregator(() => deferred.resolve());
let aggregator = new LookupAggregator(() => deferred.resolve(), trrList);
// The aggregator's domain list should correctly reflect our set
// prefs for number of random subdomains (2) and the list of
// popular domains.

View File

@ -11,7 +11,7 @@ add_task(async function test_TRRRacer_cleanRun() {
let racer = new TRRRacer(() => {
deferred.resolve();
deferred.resolved = true;
});
}, trrList);
racer.run();
await deferred.promise;
@ -60,7 +60,7 @@ async function test_TRRRacer_networkFlux_helper(captivePortal = false) {
let racer = new TRRRacer(() => {
deferred.resolve();
deferred.resolved = true;
});
}, trrList);
racer.run();
if (captivePortal) {
@ -109,7 +109,7 @@ async function test_TRRRacer_maxRetries_helper(captivePortal = false) {
let racer = new TRRRacer(() => {
deferred.resolve();
deferred.resolved = true;
});
}, trrList);
racer.run();
info("ran new racer");
// Start at i = 1 since we're already at retry #1.
@ -179,7 +179,7 @@ add_task(async function test_TRRRacer_getFastestTRRFromResults() {
{ trr: "trr5", time: 20 },
{ trr: "trr5", time: 1000 },
];
let racer = new TRRRacer();
let racer = new TRRRacer(undefined, trrList);
let fastest = racer._getFastestTRRFromResults(results);
// trr1's geometric mean is 100
// trr2's geometric mean is 110