"use strict"; ChromeUtils.defineESModuleGetters(this, { DoHConfigController: "resource:///modules/DoHConfig.sys.mjs", DoHController: "resource:///modules/DoHController.sys.mjs", DoHTestUtils: "resource://testing-common/DoHTestUtils.sys.mjs", Preferences: "resource://gre/modules/Preferences.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", RegionTestUtils: "resource://testing-common/RegionTestUtils.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", }); XPCOMUtils.defineLazyModuleGetters(this, { ASRouter: "resource://activity-stream/lib/ASRouter.jsm", }); XPCOMUtils.defineLazyServiceGetter( this, "gDNSOverride", "@mozilla.org/network/native-dns-override;1", "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() { await DoHController._uninit(); await DoHConfigController._uninit(); SpecialPowers.pushPrefEnv({ set: [["security.notification_enable_delay", 0]], }); let oldCanRecord = Services.telemetry.canRecordExtended; Services.telemetry.canRecordExtended = true; Services.telemetry.clearEvents(); // Enable the CFR. Preferences.set(CFR_PREF, JSON.stringify(CFR_JSON)); // Tell DoHController that this isn't real life. Preferences.set(prefs.TESTING_PREF, true); // Avoid non-local connections to the TRR endpoint. Preferences.set(prefs.CONFIRMATION_NS_PREF, "skip"); // Enable trr selection and provider steeringfor tests. This is off // by default so it can be controlled via Normandy. Preferences.set(prefs.TRR_SELECT_ENABLED_PREF, true); Preferences.set(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. Preferences.set(prefs.TRR_SELECT_COMMIT_PREF, true); // Clear mode on shutdown by default. Preferences.set(prefs.CLEAR_ON_SHUTDOWN_PREF, true); // Generally don't bother with debouncing or throttling. // The throttling test will set this explicitly. Preferences.set(prefs.NETWORK_DEBOUNCE_TIMEOUT_PREF, -1); Preferences.set(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"); // 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(); 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. Preferences.set(CFR_PREF, "[]"); 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(() => { events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS ).parent; return events && events.length; }); events = events.filter( e => e[1] == "security.doh.trrPerformance" && e[2] == "trrselect" && e[3] == "dryrunresult" ); is(events.length, 1, "Found the expected trrselect event."); is( events[0][4], "https://example.com/dns-query", "The event records the expected decision" ); } function ensureNoTRRSelectionTelemetry() { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS ).parent; if (!events) { ok(true, "Found no trrselect events."); return; } events = events.filter( e => e[1] == "security.doh.trrPerformance" && e[2] == "trrselect" && e[3] == "dryrunresult" ); is(events.length, 0, "Found no trrselect events."); } async function checkHeuristicsTelemetry( decision, evaluateReason, steeredProvider = "" ) { let events; await TestUtils.waitForCondition(() => { events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS ).parent; events = events?.filter( e => e[1] == "doh" && e[2] == "evaluate_v2" && e[3] == "heuristics" ); return events?.length; }); is(events.length, 1, "Found the expected heuristics event."); is(events[0][4], decision, "The event records the expected decision"); if (evaluateReason) { is(events[0][5].evaluateReason, evaluateReason, "Got the expected reason."); } is(events[0][5].steeredProvider, steeredProvider, "Got expected provider."); // After checking the event, clear all telemetry. Since we check for a single // event above, this ensures all heuristics events are intentional and tested. // TODO: Test events other than heuristics. Those tests would also work the // same way, so as to test one event at a time, and this clearEvents() call // will continue to exist as-is. Services.telemetry.clearEvents(); } async function checkHeuristicsTelemetryMultiple(expectedEvaluateReasons) { let events; await TestUtils.waitForCondition(() => { events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS ).parent; if (events && events.length) { events = events.filter( e => e[1] == "doh" && e[2] == "evaluate_v2" && e[3] == "heuristics" ); if (events.length == expectedEvaluateReasons.length) { return true; } } return false; }); is( events.length, expectedEvaluateReasons.length, "Found the expected heuristics events." ); for (let reason of expectedEvaluateReasons) { let event = events.find(e => e[5].evaluateReason == reason); is(event[5].evaluateReason, reason, `${reason} event found`); } Services.telemetry.clearEvents(); } function ensureNoHeuristicsTelemetry() { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS ).parent; if (!events) { ok(true, "Found no heuristics events."); return; } events = events.filter( e => e[1] == "doh" && e[2] == "evaluate_v2" && e[3] == "heuristics" ); is(events.length, 0, "Found no heuristics events."); } async function waitForStateTelemetry(expectedStates) { let events; await TestUtils.waitForCondition(() => { events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS ).parent; return events; }); events = events.filter(e => e[1] == "doh" && e[2] == "state"); info(events); is(events.length, expectedStates.length, "Found the expected state events."); for (let state of expectedStates) { let event = events.find(e => e[3] == state); is(event[3], state, `${state} state found`); } Services.telemetry.clearEvents(); } async function restartDoHController() { let oldMode = Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF); await DoHController._uninit(); let newMode = Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF); let expectClear = Preferences.get(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(() => { return Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF) === mode; }); is(Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF), 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(() => { return Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF) !== 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. is( Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF), mode, "No change in TRR mode" ); } }