commit a868b40e6a70e5e5232aecaa3cebd49c6cd48824
parent 28f62ab3bb114cf1efb1ab41a2f48c6321e2e8a4
Author: kpatenio <kpatenio@mozilla.com>
Date: Wed, 15 Oct 2025 02:24:03 +0000
Bug 1992299 — add support for site inclusions in IPPExceptionsManager. r=ip-protection-reviewers,fchasen
Differential Revision: https://phabricator.services.mozilla.com/D267504
Diffstat:
5 files changed, 286 insertions(+), 22 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -3477,6 +3477,7 @@ pref("browser.ipProtection.userEnabled", false);
pref("browser.ipProtection.variant", "");
pref("browser.ipProtection.exceptionsMode", "all");
pref("browser.ipProtection.domainExclusions", "");
+pref("browser.ipProtection.domainInclusions", "");
pref("browser.ipProtection.log", false);
pref("browser.ipProtection.guardian.endpoint", "https://vpn.mozilla.org/");
diff --git a/browser/components/ipprotection/IPPExceptionsManager.sys.mjs b/browser/components/ipprotection/IPPExceptionsManager.sys.mjs
@@ -8,6 +8,7 @@ const lazy = {};
const MODE_PREF = "browser.ipProtection.exceptionsMode";
const EXCLUSIONS_PREF = "browser.ipProtection.domainExclusions";
+const INCLUSIONS_PREF = "browser.ipProtection.domainInclusions";
const LOG_PREF = "browser.ipProtection.log";
const MODE = {
@@ -28,6 +29,7 @@ ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
class ExceptionsManager {
#inited = false;
#exclusions = null;
+ #inclusions = null;
#mode = MODE.ALL;
/**
@@ -47,6 +49,22 @@ class ExceptionsManager {
}
/**
+ * The set of domains to include for VPN protection.
+ *
+ * @returns {Set<string>}
+ * A set of domain names as strings.
+ *
+ * @example
+ * Set { "https://www.example.com", "https://www.bbc.co.uk" }
+ */
+ get inclusions() {
+ if (!this.#inclusions || !(this.#inclusions instanceof Set)) {
+ this.#inclusions = new Set();
+ }
+ return this.#inclusions;
+ }
+
+ /**
* The type of site exceptions for VPN.
*
* @see MODE
@@ -61,7 +79,12 @@ class ExceptionsManager {
}
this.#mode = this.#getModePref();
- this.#loadExceptions();
+
+ this.#exclusions = new Set();
+ this.#inclusions = new Set();
+ this.#loadExceptionsForPref(EXCLUSIONS_PREF);
+ this.#loadExceptionsForPref(INCLUSIONS_PREF);
+
this.#inited = true;
}
@@ -129,6 +152,11 @@ class ExceptionsManager {
if (pref === EXCLUSIONS_PREF) {
prefString = this.domainExclusions;
+ } else if (pref === INCLUSIONS_PREF) {
+ prefString = this.domainInclusions;
+ } else {
+ lazy.logConsole.error(`Invalid pref ${pref} found.`);
+ return "";
}
if (typeof prefString !== "string") {
@@ -140,21 +168,19 @@ class ExceptionsManager {
}
/**
- * If mode is MODE.ALL, initializes the exclusions set with domains from
- * browser.ipProtection.domainExclusions.
+ * Initializes the exclusions set with domains from
+ * browser.ipProtection.domainExclusions, or
+ * initializes the inclusions set with domains from
+ * browser.ipProtection.domainInclusions.
*
- * @see MODE
+ * @param {string} pref
+ * The exclusions pref (browser.ipProtection.domainExclusions)
+ * or inclusions pref (browser.ipProtection.domainInclusions).
* @see exclusions
+ * @see inclusions
*/
- #loadExceptions() {
- if (this.#mode == MODE.ALL) {
- this.#loadExclusions();
- }
- }
-
- #loadExclusions() {
- this.#exclusions = new Set();
- let prefString = this.#getExceptionPref(EXCLUSIONS_PREF);
+ #loadExceptionsForPref(pref) {
+ let prefString = this.#getExceptionPref(pref);
if (!prefString) {
return;
@@ -163,25 +189,30 @@ class ExceptionsManager {
let domains = prefString.trim().split(",");
for (let domain of domains) {
- if (!this.#canExcludeDomain(domain)) {
+ if (!this.#canCreateURI(domain)) {
continue;
}
let uri = Services.io.newURI(domain);
- this.#exclusions.add(uri.prePath);
+
+ if (pref === EXCLUSIONS_PREF) {
+ this.#exclusions.add(uri.prePath);
+ } else if (pref === INCLUSIONS_PREF) {
+ this.#inclusions.add(uri.prePath);
+ }
}
}
/**
- * Checks if we can exclude a domain from VPN usage.
+ * Checks if we can add a domain as an exclusion or inclusion for VPN usage.
*
* @param {string} domain
* The domain name.
* @returns {boolean}
- * True if we can exclude the domain because it meets our exclusion rules.
+ * True if a URI can be created from the domain string.
* Else false.
*/
- #canExcludeDomain(domain) {
+ #canCreateURI(domain) {
try {
return !!Services.io.newURI(domain);
} catch (e) {
@@ -201,13 +232,15 @@ class ExceptionsManager {
*/
addException(domain) {
// TODO: to be called by IPProtectionPanel or other classes (Bug 1990975, Bug 1990972)
- if (this.#mode == MODE.ALL) {
+ if (this.#mode === MODE.ALL) {
this.#addExclusion(domain);
+ } else if (this.#mode === MODE.SELECT) {
+ this.#addInclusion(domain);
}
}
#addExclusion(domain) {
- if (!this.#canExcludeDomain(domain)) {
+ if (!this.#canCreateURI(domain)) {
return;
}
@@ -215,6 +248,15 @@ class ExceptionsManager {
this.#updateExclusionPref();
}
+ #addInclusion(domain) {
+ if (!this.#canCreateURI(domain)) {
+ return;
+ }
+
+ this.#inclusions.add(domain);
+ this.#updateInclusionPref();
+ }
+
/**
* If mode is MODE.ALL, removes a domain from the exclusions set.
*
@@ -226,8 +268,10 @@ class ExceptionsManager {
*/
removeException(domain) {
// TODO: to be called by IPProtectionPanel or other classes (Bug 1990975, Bug 1990972)
- if (this.#mode == MODE.ALL) {
+ if (this.#mode === MODE.ALL) {
this.#removeExclusion(domain);
+ } else if (this.#mode === MODE.SELECT) {
+ this.#removeInclusion(domain);
}
}
@@ -237,6 +281,12 @@ class ExceptionsManager {
}
}
+ #removeInclusion(domain) {
+ if (this.#inclusions.delete(domain)) {
+ this.#updateInclusionPref();
+ }
+ }
+
/**
* Updates the value of browser.ipProtection.domainExclusions
* according to the latest version of the exclusions set.
@@ -247,11 +297,20 @@ class ExceptionsManager {
}
/**
+ * Updates the value of browser.ipProtection.domainInclusions
+ * according to the latest version of the inclusions set.
+ */
+ #updateInclusionPref() {
+ let newPrefString = [...this.#inclusions].join(",");
+ Services.prefs.setStringPref(INCLUSIONS_PREF, newPrefString);
+ }
+
+ /**
* Clear the exclusions set.
*/
#unloadExceptions() {
- // TODO: clear inclusions set here too
this.#exclusions = null;
+ this.#inclusions = null;
}
}
@@ -266,6 +325,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
XPCOMUtils.defineLazyPreferenceGetter(
IPPExceptionsManager,
+ "domainInclusions",
+ INCLUSIONS_PREF,
+ ""
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ IPPExceptionsManager,
"exceptionsMode",
MODE_PREF,
MODE.ALL
diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_exclusions.js b/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_exclusions.js
@@ -7,14 +7,18 @@ const { IPPExceptionsManager } = ChromeUtils.importESModule(
"resource:///modules/ipprotection/IPPExceptionsManager.sys.mjs"
);
+const MODE_PREF = "browser.ipProtection.exceptionsMode";
const EXCLUSIONS_PREF = "browser.ipProtection.domainExclusions";
+const ALL_MODE = "all";
+
/**
* Tests manager initialization when there are no exclusions.
*/
add_task(async function test_IPPExceptionsManager_init_with_no_exclusions() {
const stringPref = "";
+ Services.prefs.setStringPref(MODE_PREF, ALL_MODE);
Services.prefs.setStringPref(EXCLUSIONS_PREF, stringPref);
IPPExceptionsManager.init();
@@ -29,6 +33,7 @@ add_task(async function test_IPPExceptionsManager_init_with_no_exclusions() {
Assert.ok(!newStringPref, "String pref should be empty");
+ Services.prefs.clearUserPref(MODE_PREF);
Services.prefs.clearUserPref(EXCLUSIONS_PREF);
IPPExceptionsManager.uninit();
@@ -43,6 +48,7 @@ add_task(async function test_IPPExceptionsManager_init_with_exclusions() {
const site3 = "https://www.another.example.ca";
const stringPref = `${site1},${site2},${site3}`;
+ Services.prefs.setStringPref(MODE_PREF, ALL_MODE);
Services.prefs.setStringPref(EXCLUSIONS_PREF, stringPref);
IPPExceptionsManager.init();
@@ -73,6 +79,7 @@ add_task(async function test_IPPExceptionsManager_init_with_exclusions() {
Assert.ok(newStringPref.includes(site2), `String pref includes ${site2}`);
Assert.ok(newStringPref.includes(site3), `String pref includes ${site3}`);
+ Services.prefs.clearUserPref(MODE_PREF);
Services.prefs.clearUserPref(EXCLUSIONS_PREF);
IPPExceptionsManager.uninit();
});
@@ -84,6 +91,7 @@ add_task(
async function test_IPPExceptionsManager_init_with_invalid_exclusions() {
const invalidStringPref = "noturl";
+ Services.prefs.setStringPref(MODE_PREF, ALL_MODE);
Services.prefs.setStringPref(EXCLUSIONS_PREF, invalidStringPref);
IPPExceptionsManager.init();
@@ -94,6 +102,7 @@ add_task(
"exclusions set should have 0 valid domains"
);
+ Services.prefs.clearUserPref(MODE_PREF);
Services.prefs.clearUserPref(EXCLUSIONS_PREF);
IPPExceptionsManager.uninit();
}
@@ -105,6 +114,7 @@ add_task(
add_task(async function test_IPPExceptionsManager_add_exclusions() {
const site1 = "https://www.example.com";
+ Services.prefs.setStringPref(MODE_PREF, ALL_MODE);
Services.prefs.setStringPref(EXCLUSIONS_PREF, site1);
IPPExceptionsManager.init();
@@ -144,6 +154,7 @@ add_task(async function test_IPPExceptionsManager_add_exclusions() {
`String pref includes ${validSite}`
);
+ Services.prefs.clearUserPref(MODE_PREF);
Services.prefs.clearUserPref(EXCLUSIONS_PREF);
IPPExceptionsManager.uninit();
});
@@ -154,6 +165,7 @@ add_task(async function test_IPPExceptionsManager_add_exclusions() {
add_task(async function test_IPPExceptionsManager_remove_exclusions() {
const site1 = "https://www.example.com";
+ Services.prefs.setStringPref(MODE_PREF, ALL_MODE);
Services.prefs.setStringPref(EXCLUSIONS_PREF, site1);
IPPExceptionsManager.init();
@@ -174,6 +186,7 @@ add_task(async function test_IPPExceptionsManager_remove_exclusions() {
Assert.ok(!newStringPref, "String pref should be empty");
+ Services.prefs.clearUserPref(MODE_PREF);
Services.prefs.clearUserPref(EXCLUSIONS_PREF);
IPPExceptionsManager.uninit();
});
diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_inclusions.js b/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_inclusions.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { IPPExceptionsManager } = ChromeUtils.importESModule(
+ "resource:///modules/ipprotection/IPPExceptionsManager.sys.mjs"
+);
+
+const MODE_PREF = "browser.ipProtection.exceptionsMode";
+const INCLUSIONS_PREF = "browser.ipProtection.domainInclusions";
+
+const SELECT_MODE = "select";
+
+/**
+ * Tests manager initialization when there are no inclusions.
+ */
+add_task(async function test_IPPExceptionsManager_init_with_no_inclusions() {
+ const stringPref = "";
+
+ Services.prefs.setStringPref(MODE_PREF, SELECT_MODE);
+ Services.prefs.setStringPref(INCLUSIONS_PREF, stringPref);
+
+ IPPExceptionsManager.init();
+
+ Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found");
+ Assert.ok(
+ !IPPExceptionsManager.inclusions.size,
+ "inclusions set should be empty"
+ );
+
+ Services.prefs.clearUserPref(MODE_PREF);
+ Services.prefs.clearUserPref(INCLUSIONS_PREF);
+
+ IPPExceptionsManager.uninit();
+});
+
+/**
+ * Tests the manager initialization with registered inclusions.
+ */
+add_task(async function test_IPPExceptionsManager_init_with_inclusions() {
+ const site1 = "https://www.example.com";
+ const site2 = "https://www.example.org";
+ const site3 = "https://www.another.example.ca";
+ const stringPref = `${site1},${site2},${site3}`;
+
+ Services.prefs.setStringPref(MODE_PREF, SELECT_MODE);
+ Services.prefs.setStringPref(INCLUSIONS_PREF, stringPref);
+
+ IPPExceptionsManager.init();
+
+ Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found");
+ Assert.equal(
+ IPPExceptionsManager.inclusions.size,
+ 3,
+ "inclusions set should have 3 domains"
+ );
+
+ Assert.ok(
+ IPPExceptionsManager.inclusions.has(site1),
+ `inclusions set should include ${site1}`
+ );
+ Assert.ok(
+ IPPExceptionsManager.inclusions.has(site2),
+ `inclusions set should include ${site2}`
+ );
+ Assert.ok(
+ IPPExceptionsManager.inclusions.has(site3),
+ `inclusions set should include ${site3}`
+ );
+
+ Services.prefs.clearUserPref(MODE_PREF);
+ Services.prefs.clearUserPref(INCLUSIONS_PREF);
+ IPPExceptionsManager.uninit();
+});
+
+/**
+ * Tests the manager initialization with an invalid pref string for inclusions.
+ */
+add_task(
+ async function test_IPPExceptionsManager_init_with_invalid_inclusions() {
+ const invalidStringPref = "noturl";
+
+ Services.prefs.setStringPref(MODE_PREF, SELECT_MODE);
+ Services.prefs.setStringPref(INCLUSIONS_PREF, invalidStringPref);
+
+ IPPExceptionsManager.init();
+
+ Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found");
+ Assert.ok(
+ !IPPExceptionsManager.inclusions.size,
+ "inclusions set should have 0 valid domains"
+ );
+
+ Services.prefs.clearUserPref(MODE_PREF);
+ Services.prefs.clearUserPref(INCLUSIONS_PREF);
+ IPPExceptionsManager.uninit();
+ }
+);
+
+/**
+ * Tests that we can add valid domains to the inclusions set.
+ */
+add_task(async function test_IPPExceptionsManager_add_inclusions() {
+ const site1 = "https://www.example.com";
+
+ Services.prefs.setStringPref(MODE_PREF, SELECT_MODE);
+ Services.prefs.setStringPref(INCLUSIONS_PREF, site1);
+
+ IPPExceptionsManager.init();
+
+ Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found");
+
+ const validSite = "https://www.another.example.com";
+ const dupeSite = site1;
+ const invalidSite = "noturl";
+
+ IPPExceptionsManager.addException(validSite);
+ IPPExceptionsManager.addException(dupeSite);
+ IPPExceptionsManager.addException(invalidSite);
+ IPPExceptionsManager.addException(null);
+ IPPExceptionsManager.addException(undefined);
+
+ Assert.equal(
+ IPPExceptionsManager.inclusions.size,
+ 2,
+ "inclusions set should only have 2 domains"
+ );
+
+ Assert.ok(
+ IPPExceptionsManager.inclusions.has(site1),
+ `inclusions set should include ${site1}`
+ );
+ Assert.ok(
+ IPPExceptionsManager.inclusions.has(validSite),
+ `inclusions set should include ${validSite}`
+ );
+
+ let newStringPref = Services.prefs.getStringPref(INCLUSIONS_PREF);
+
+ Assert.ok(newStringPref.includes(site1), `String pref includes ${site1}`);
+ Assert.ok(
+ newStringPref.includes(validSite),
+ `String pref includes ${validSite}`
+ );
+
+ Services.prefs.clearUserPref(MODE_PREF);
+ Services.prefs.clearUserPref(INCLUSIONS_PREF);
+ IPPExceptionsManager.uninit();
+});
+
+/**
+ * Tests that we can remove domains from the inclusions set.
+ */
+add_task(async function test_IPPExceptionsManager_remove_inclusions() {
+ const site1 = "https://www.example.com";
+
+ Services.prefs.setStringPref(MODE_PREF, SELECT_MODE);
+ Services.prefs.setStringPref(INCLUSIONS_PREF, site1);
+
+ IPPExceptionsManager.init();
+
+ Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found");
+
+ const invalidSite = "urlDoesntExist";
+
+ IPPExceptionsManager.removeException(site1);
+ IPPExceptionsManager.removeException(invalidSite);
+
+ Assert.ok(
+ !IPPExceptionsManager.inclusions.size,
+ "inclusions set should be empty"
+ );
+
+ let newStringPref = Services.prefs.getStringPref(INCLUSIONS_PREF);
+
+ Assert.ok(!newStringPref, "String pref should be empty");
+
+ Services.prefs.clearUserPref(MODE_PREF);
+ Services.prefs.clearUserPref(INCLUSIONS_PREF);
+ IPPExceptionsManager.uninit();
+});
diff --git a/browser/components/ipprotection/tests/xpcshell/xpcshell.toml b/browser/components/ipprotection/tests/xpcshell/xpcshell.toml
@@ -11,6 +11,8 @@ prefs = [
["test_IPPExceptionsManager_exclusions.js"]
+["test_IPPExceptionsManager_inclusions.js"]
+
["test_IPPStartupCache.js"]
["test_IPProtection.js"]