"use strict";
ChromeUtils.defineESModuleGetters(
this, {
ASRouter:
"resource:///modules/asrouter/ASRouter.sys.mjs",
DoHConfigController:
"moz-src:///toolkit/components/doh/DoHConfig.sys.mjs",
DoHController:
"moz-src:///toolkit/components/doh/DoHController.sys.mjs",
DoHTestUtils:
"resource://testing-common/DoHTestUtils.sys.mjs",
Heuristics:
"moz-src:///toolkit/components/doh/DoHHeuristics.sys.mjs",
Region:
"resource://gre/modules/Region.sys.mjs",
RegionTestUtils:
"resource://testing-common/RegionTestUtils.sys.mjs",
RemoteSettings:
"resource://services-settings/remote-settings.sys.mjs",
});
// Map DoH state names (as used by DoHController.setState) to their Glean
// metric properties on `Glean.doh`.
const DOH_STATE_TO_GLEAN_METRIC = {
enabled:
"stateEnabled",
disabled:
"stateDisabled",
manuallyDisabled:
"stateManuallyDisabled",
policyDisabled:
"statePolicyDisabled",
uninstalled:
"stateUninstalled",
UIOk:
"stateUiok",
UIDisabled:
"stateUidisabled",
rollback:
"stateRollback",
shutdown:
"stateShutdown",
};
const ALL_DOH_STATES = Object.keys(DOH_STATE_TO_GLEAN_METRIC);
function gleanStateMetricFor(state) {
return Glean.doh[DOH_STATE_TO_GLEAN_METRIC[state]];
}
XPCOMUtils.defineLazyServiceGetter(
this,
"gDNSOverride",
"@mozilla.org/network/native-dns-override;1",
Ci.nsINativeDNSResolverOverride
);
const { CommonUtils } = ChromeUtils.importESModule(
"resource://services-common/utils.sys.mjs"
);
const EXAMPLE_URL =
"https://example.com/";
const prefs = {
TESTING_PREF:
"doh-rollout._testing",
ENABLED_PREF:
"doh-rollout.enabled",
ROLLOUT_TRR_MODE_PREF:
"doh-rollout.mode",
NETWORK_TRR_MODE_PREF:
"network.trr.mode",
CONFIRMATION_NS_PREF:
"network.trr.confirmationNS",
BREADCRUMB_PREF:
"doh-rollout.self-enabled",
DOORHANGER_USER_DECISION_PREF:
"doh-rollout.doorhanger-decision",
DISABLED_PREF:
"doh-rollout.disable-heuristics",
SKIP_HEURISTICS_PREF:
"doh-rollout.skipHeuristicsCheck",
CLEAR_ON_SHUTDOWN_PREF:
"doh-rollout.clearModeOnShutdown",
FIRST_RUN_PREF:
"doh-rollout.doneFirstRun",
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",
TRR_SELECT_DRY_RUN_RESULT_PREF:
"doh-rollout.trr-selection.dry-run-result",
PROVIDER_STEERING_PREF:
"doh-rollout.provider-steering.enabled",
PROVIDER_STEERING_LIST_PREF:
"doh-rollout.provider-steering.provider-list",
NETWORK_DEBOUNCE_TIMEOUT_PREF:
"doh-rollout.network-debounce-timeout",
HEURISTICS_THROTTLE_TIMEOUT_PREF:
"doh-rollout.heuristics-throttle-timeout",
HEURISTICS_THROTTLE_RATE_LIMIT_PREF:
"doh-rollout.heuristics-throttle-rate-limit",
};
const CFR_PREF =
"browser.newtabpage.activity-stream.asrouter.providers.cfr";
const CFR_JSON = {
id:
"cfr",
enabled:
true,
type:
"local",
localProvider:
"CFRMessageProvider",
categories: [
"cfrAddons",
"cfrFeatures"],
};
async
function setup() {
try {
await DoHController._uninit();
await DoHConfigController._uninit();
}
catch (e) {}
SpecialPowers.pushPrefEnv({
set: [[
"security.notification_enable_delay",
0]],
});
Services.fog.testResetFOG();
_resetConsumed();
// Enable the CFR.
Services.prefs.setStringPref(CFR_PREF, JSON.stringify(CFR_JSON));
// Tell DoHController that this isn't real life.
Services.prefs.setBoolPref(prefs.TESTING_PREF,
true);
// Avoid non-local connections to the TRR endpoint.
Services.prefs.setStringPref(prefs.CONFIRMATION_NS_PREF,
"skip");
// Enable trr selection and provider steeringfor tests. This is off
// by default so it can be controlled via Normandy.
Services.prefs.setBoolPref(prefs.TRR_SELECT_ENABLED_PREF,
true);
Services.prefs.setBoolPref(prefs.PROVIDER_STEERING_PREF,
true);
// Enable committing the TRR selection. This pref ships false by default so
// it can be controlled e.g. via Normandy, but for testing let's set enable.
Services.prefs.setBoolPref(prefs.TRR_SELECT_COMMIT_PREF,
true);
// Clear mode on shutdown by default.
Services.prefs.setBoolPref(prefs.CLEAR_ON_SHUTDOWN_PREF,
true);
// Generally don't bother with debouncing or throttling.
// The throttling test will set this explicitly.
Services.prefs.setIntPref(prefs.NETWORK_DEBOUNCE_TIMEOUT_PREF, -
1);
Services.prefs.setIntPref(prefs.HEURISTICS_THROTTLE_TIMEOUT_PREF, -
1);
// Set up heuristics, all passing by default.
// Google safesearch overrides
gDNSOverride.addIPOverride(
"www.google.com.",
"1.1.1.1");
gDNSOverride.addIPOverride(
"google.com.",
"1.1.1.1");
gDNSOverride.addIPOverride(
"forcesafesearch.google.com.",
"1.1.1.2");
// YouTube safesearch overrides
gDNSOverride.addIPOverride(
"www.youtube.com.",
"2.1.1.1");
gDNSOverride.addIPOverride(
"m.youtube.com.",
"2.1.1.1");
gDNSOverride.addIPOverride(
"youtubei.googleapis.com.",
"2.1.1.1");
gDNSOverride.addIPOverride(
"youtube.googleapis.com.",
"2.1.1.1");
gDNSOverride.addIPOverride(
"www.youtube-nocookie.com.",
"2.1.1.1");
gDNSOverride.addIPOverride(
"restrict.youtube.com.",
"2.1.1.2");
gDNSOverride.addIPOverride(
"restrictmoderate.youtube.com.",
"2.1.1.2");
// Zscaler override
gDNSOverride.addIPOverride(
"sitereview.zscaler.com.",
"3.1.1.1");
// Helper domains when checking the canary
gDNSOverride.addIPOverride(
"firefox.com.",
"9.8.7.6");
gDNSOverride.addIPOverride(
"mozilla.org.",
"8.7.6.5");
// Global canary
gDNSOverride.addIPOverride(
"use-application-dns.net.",
"4.1.1.1");
await DoHTestUtils.resetRemoteSettingsConfig(
false);
Services.fog.testResetFOG();
_resetConsumed();
await DoHConfigController.init();
await DoHController.init();
await waitForStateTelemetry([
"rollback"]);
registerCleanupFunction(async () => {
Services.fog.testResetFOG();
_resetConsumed();
gDNSOverride.clearOverrides();
if (ASRouter.state.messageBlockList.includes(
"DOH_ROLLOUT_CONFIRMATION")) {
await ASRouter.unblockMessageById(
"DOH_ROLLOUT_CONFIRMATION");
}
// The CFR pref is set to an empty array in user.js for testing profiles,
// so "reset" it back to that value.
Services.prefs.setStringPref(CFR_PREF,
"[]");
await DoHController._uninit();
Services.fog.testResetFOG();
_resetConsumed();
for (let pref of Object.values(prefs)) {
Services.prefs.clearUserPref(pref);
}
await DoHTestUtils.resetRemoteSettingsConfig(
false);
await DoHController.init();
});
}
const kTestRegion =
"DE";
const kRegionalPrefNamespace = `doh-rollout.${kTestRegion.toLowerCase()}`;
async
function setupRegion() {
Region._setHomeRegion(
null,
false);
RegionTestUtils.setNetworkRegion(kTestRegion);
await Region._fetchRegion();
is(Region.home, kTestRegion,
"Should have correct region");
Services.prefs.clearUserPref(
"doh-rollout.home-region");
await DoHConfigController.loadRegion();
}
// Glean events are not cleared per-metric, and `Services.fog.testResetFOG()`
// would also wipe the scalar/counter values that other helpers verify. To
// preserve the "check one event at a time" semantics of the legacy helpers,
// track how many events have already been verified and only inspect new ones.
let _consumedHeuristics =
0;
let _consumedTrrSelect =
0;
let _consumedStates = {};
function _resetConsumed() {
_consumedHeuristics =
0;
_consumedTrrSelect =
0;
_consumedStates = {};
}
// Mark all currently-recorded state events as consumed. The original tests
// called Services.telemetry.clearEvents() in each "check..." helper, which
// dropped any incidental state events that fired between checks. We do the
// same here so that waitForStateTelemetry sees only the events that fired
// since the last "check..." helper completed.
async
function _consumeAllStateEvents() {
for (let state of ALL_DOH_STATES) {
let events = (await gleanStateMetricFor(state).testGetValue()) ?? [];
_consumedStates[state] = events.length;
}
}
async
function checkTRRSelectionTelemetry() {
let events;
await TestUtils.waitForCondition(async () => {
events =
(await Glean.securityDohTrrPerformance.trrselectDryrunresult.testGetValue()) ??
[];
return events.length > _consumedTrrSelect;
});
let newEvents = events.slice(_consumedTrrSelect);
is(newEvents.length,
1,
"Found the expected trrselect event.");
is(
newEvents[
0].extra.value,
"https://example.com/dns-query",
"The event records the expected decision"
);
_consumedTrrSelect = events.length;
}
async
function ensureNoTRRSelectionTelemetry() {
let events =
(await Glean.securityDohTrrPerformance.trrselectDryrunresult.testGetValue()) ??
[];
is(events.length - _consumedTrrSelect,
0,
"Found no trrselect events.");
}
async
function checkHeuristicsTelemetry(
decision,
evaluateReason,
steeredProvider =
""
) {
let events;
await TestUtils.waitForCondition(async () => {
events = (await Glean.doh.evaluateV2Heuristics.testGetValue()) ?? [];
return events.length > _consumedHeuristics;
});
let newEvents = events.slice(_consumedHeuristics);
is(newEvents.length,
1,
"Found the expected heuristics event.");
is(
newEvents[
0].extra.value,
decision,
"The event records the expected decision"
);
if (evaluateReason) {
is(
newEvents[
0].extra.evaluateReason,
evaluateReason,
"Got the expected reason."
);
}
is(
newEvents[
0].extra.steeredProvider ??
"",
steeredProvider,
"Got expected provider."
);
_consumedHeuristics = events.length;
await _consumeAllStateEvents();
}
// Assert a list of Glean metric values. Each entry is `[metric, expected]`
// or `[metric, expected, message]`. Boolean expectations treat a null
// testGetValue (metric never set) as false.
async
function assertGleanValues(expectations) {
for (let [metric, expected, message] of expectations) {
let actual = await metric.testGetValue();
if (
typeof expected ===
"boolean") {
actual = actual ??
false;
}
is(actual, expected, message);
}
}
// Returns Glean expectations asserting all `dohHeuristicEverTripped` keys
// are false, except those listed in `except`.
function allHeuristicsFalseExpectations(except = []) {
return Heuristics.Telemetry.heuristicNames()
.filter(name => !except.includes(name))
.map(name => [
Glean.networking.dohHeuristicEverTripped[name],
false,
`dohHeuristicEverTripped[${name}] should be
false`,
]);
}
async
function checkHeuristicsTelemetryMultiple(expectedEvaluateReasons) {
let newEvents;
await TestUtils.waitForCondition(async () => {
let events = (await Glean.doh.evaluateV2Heuristics.testGetValue()) ?? [];
newEvents = events.slice(_consumedHeuristics);
if (newEvents.length == expectedEvaluateReasons.length) {
_consumedHeuristics = events.length;
return true;
}
return false;
});
is(
newEvents.length,
expectedEvaluateReasons.length,
"Found the expected heuristics events."
);
for (let reason of expectedEvaluateReasons) {
let event = newEvents.find(e => e.extra.evaluateReason == reason);
is(event.extra.evaluateReason, reason, `${reason} event found`);
}
await _consumeAllStateEvents();
}
async
function ensureNoHeuristicsTelemetry() {
let events = (await Glean.doh.evaluateV2Heuristics.testGetValue()) ?? [];
is(events.length - _consumedHeuristics,
0,
"Found no heuristics events.");
}
async
function waitForStateTelemetry(expectedStates) {
let stateCounts = {};
// Wait for at least the expected number of new state events to land.
await TestUtils.waitForCondition(async () => {
let newCount =
0;
for (let state of ALL_DOH_STATES) {
let events = (await gleanStateMetricFor(state).testGetValue()) ?? [];
stateCounts[state] = events.length;
newCount += events.length - (_consumedStates[state] ??
0);
}
return newCount >= expectedStates.length;
});
let totalNew =
0;
for (let state of ALL_DOH_STATES) {
let consumed = _consumedStates[state] ??
0;
let newForState = stateCounts[state] - consumed;
let expected = expectedStates.filter(s => s == state).length;
is(
newForState,
expected,
`${state}: expected ${expected}
new state event(s)`
);
totalNew += newForState;
_consumedStates[state] = stateCounts[state];
}
is(totalNew, expectedStates.length,
"Found the expected state events.");
}
async
function restartDoHController() {
let oldMode = Services.prefs.prefHasUserValue(prefs.ROLLOUT_TRR_MODE_PREF)
? Services.prefs.getIntPref(prefs.ROLLOUT_TRR_MODE_PREF)
: undefined;
await DoHController._uninit();
let newMode = Services.prefs.prefHasUserValue(prefs.ROLLOUT_TRR_MODE_PREF)
? Services.prefs.getIntPref(prefs.ROLLOUT_TRR_MODE_PREF)
: undefined;
let expectClear = Services.prefs.getBoolPref(prefs.CLEAR_ON_SHUTDOWN_PREF);
is(
newMode,
expectClear ? undefined : oldMode,
`Mode was ${expectClear ?
"cleared" :
"persisted"} on shutdown.`
);
await DoHController.init();
}
// setPassing/FailingHeuristics are used generically to test that DoH is enabled
// or disabled correctly. We use the zscaler canary arbitrarily here, individual
// heuristics are tested separately.
function setPassingHeuristics() {
gDNSOverride.clearHostOverride(
"sitereview.zscaler.com.");
gDNSOverride.addIPOverride(
"sitereview.zscaler.com.",
"3.1.1.1");
}
function setFailingHeuristics() {
gDNSOverride.clearHostOverride(
"sitereview.zscaler.com.");
gDNSOverride.addIPOverride(
"sitereview.zscaler.com.",
"213.152.228.242");
}
async
function waitForDoorhanger() {
const popupID =
"contextual-feature-recommendation";
const bucketID =
"DOH_ROLLOUT_CONFIRMATION";
let panel;
await BrowserTestUtils.waitForEvent(document,
"popupshown",
true, event => {
panel = event.originalTarget;
let popupNotification = event.originalTarget.firstChild;
return (
popupNotification &&
popupNotification.notification &&
popupNotification.notification.id == popupID &&
popupNotification.getAttribute(
"data-notification-bucket") == bucketID
);
});
return panel;
}
function simulateNetworkChange() {
// The networkStatus API does not actually propagate the link status we supply
// here, but rather sends the link status from the NetworkLinkService.
// This means there's no point sending a down and then an up - the extension
// will just receive "up" twice.
// TODO: Implement a mock NetworkLinkService and use it to also simulate
// network down events.
Services.obs.notifyObservers(
null,
"network:link-status-changed",
"up");
}
async
function ensureTRRMode(mode) {
await TestUtils.waitForCondition(() => {
let val = Services.prefs.prefHasUserValue(prefs.ROLLOUT_TRR_MODE_PREF)
? Services.prefs.getIntPref(prefs.ROLLOUT_TRR_MODE_PREF)
: undefined;
return val === mode;
});
let actualMode = Services.prefs.prefHasUserValue(prefs.ROLLOUT_TRR_MODE_PREF)
? Services.prefs.getIntPref(prefs.ROLLOUT_TRR_MODE_PREF)
: undefined;
is(actualMode, mode, `TRR mode is ${mode}`);
}
async
function ensureNoTRRModeChange(mode) {
try {
// Try and wait for the TRR pref to change... waitForCondition should throw
// after trying for a while.
await TestUtils.waitForCondition(() => {
let val = Services.prefs.prefHasUserValue(prefs.ROLLOUT_TRR_MODE_PREF)
? Services.prefs.getIntPref(prefs.ROLLOUT_TRR_MODE_PREF)
: undefined;
return val !== mode;
});
// If we reach this, the waitForCondition didn't throw. Fail!
ok(
false,
"TRR mode changed when it shouldn't have!");
}
catch (e) {
// Assert for clarity.
let actualMode = Services.prefs.prefHasUserValue(
prefs.ROLLOUT_TRR_MODE_PREF
)
? Services.prefs.getIntPref(prefs.ROLLOUT_TRR_MODE_PREF)
: undefined;
is(actualMode, mode,
"No change in TRR mode");
}
}