tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 525c427e1739162bb06852467d01731555a9ed1e
parent 6da49d548d3373b95591b968e764f405a1535e0d
Author: Sebastian Streich <sstreich@mozilla.com>
Date:   Mon, 15 Dec 2025 16:36:46 +0000

Bug 1996529 - Add Optional Config option to force a specific server r=ip-protection-reviewers,baku

Differential Revision: https://phabricator.services.mozilla.com/D273986

Diffstat:
Mbrowser/components/ipprotection/IPProtectionServerlist.sys.mjs | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mbrowser/components/ipprotection/docs/Preferences.rst | 3+++
Mbrowser/components/ipprotection/tests/xpcshell/test_IPProtectionServerlist.js | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 203 insertions(+), 59 deletions(-)

diff --git a/browser/components/ipprotection/IPProtectionServerlist.sys.mjs b/browser/components/ipprotection/IPProtectionServerlist.sys.mjs @@ -181,20 +181,95 @@ class Country { } /** - * + * Base Class for the Serverlist */ -class IPProtectionServerlistSingleton { - #list = null; - #runningPromise = null; +export class IPProtectionServerlistBase { + __list = null; + + init() {} + + async initOnStartupCompleted() {} + + uninit() {} + + /** + * Tries to refresh the list from the underlining source. + * + * @param {*} _forceUpdate - if true, forces a refresh even if the list is already populated. + */ + maybeFetchList(_forceUpdate = false) { + throw new Error("Not implemented"); + } + + /** + * Selects a default location - for alpha this is only the US. + * + * @returns {{Country, City}} - The best country/city to use. + */ + getDefaultLocation() { + /** @type {Country} */ + const usa = this.__list.find(country => country.code === "US"); + if (!usa) { + return null; + } + + const city = usa.cities.find(c => c.servers.length); + return { + city, + country: usa, + }; + } + + /** + * Given a city, it selects an available server. + * + * @param {City?} city + * @returns {Server|null} + */ + selectServer(city) { + if (!city) { + return null; + } + + const servers = city.servers.filter(server => !server.quarantined); + if (servers.length === 1) { + return servers[0]; + } + + if (servers.length > 1) { + return servers[Math.floor(Math.random() * servers.length)]; + } + + return null; + } + + get hasList() { + return this.__list.length !== 0; + } + + static dataToList(list) { + if (!Array.isArray(list)) { + return []; + } + return list.map(c => new Country(c)); + } +} + +/** + * Class representing the IP Protection Serverlist + * fetched from Remote Settings. + */ +export class RemoteSettingsServerlist extends IPProtectionServerlistBase { #bucket = null; + #runningPromise = null; constructor() { + super(); this.handleEvent = this.#handleEvent.bind(this); - this.#list = IPProtectionServerlistSingleton.#dataToList( + this.__list = IPProtectionServerlistBase.dataToList( lazy.IPPStartupCache.locationList ); } - init() { lazy.IPProtectionService.addEventListener( "IPProtectionService:StateChanged", @@ -222,7 +297,7 @@ class IPProtectionServerlistSingleton { } maybeFetchList(forceUpdate = false) { - if (this.#list.length !== 0 && !forceUpdate) { + if (this.__list.length !== 0 && !forceUpdate) { return Promise.resolve(); } @@ -231,11 +306,11 @@ class IPProtectionServerlistSingleton { } const fetchList = async () => { - this.#list = IPProtectionServerlistSingleton.#dataToList( + this.__list = IPProtectionServerlistBase.dataToList( await this.bucket.get() ); - lazy.IPPStartupCache.storeLocationList(this.#list); + lazy.IPPStartupCache.storeLocationList(this.__list); }; this.#runningPromise = fetchList().finally( @@ -245,68 +320,84 @@ class IPProtectionServerlistSingleton { return this.#runningPromise; } - /** - * Selects a default location - for alpha this is only the US. - * - * @returns {{Country, City}} - The best country/city to use. - */ - getDefaultLocation() { - /** @type {Country} */ - const usa = this.#list.find(country => country.code === "US"); - if (!usa) { - return null; + get bucket() { + if (!this.#bucket) { + this.#bucket = lazy.RemoteSettings("vpn-serverlist"); } - - const city = usa.cities.find(c => c.servers.length); - return { - city, - country: usa, - }; + return this.#bucket; } +} +/** + * Class representing the IP Protection Serverlist + * from about:config preferences. + */ +export class PrefServerList extends IPProtectionServerlistBase { + #observer = null; - /** - * Given a city, it selects an available server. - * - * @param {City?} city - * @returns {Server|null} - */ - selectServer(city) { - if (!city) { - return null; - } - - const servers = city.servers.filter(server => !server.quarantined); - if (servers.length === 1) { - return servers[0]; - } - - if (servers.length > 1) { - return servers[Math.floor(Math.random() * servers.length)]; - } + constructor() { + super(); + this.#observer = this.onPrefChange.bind(this); + this.maybeFetchList(); + } - return null; + onPrefChange() { + this.maybeFetchList(); } - get hasList() { - return this.#list.length !== 0; + async initOnStartupCompleted() { + Services.prefs.addObserver( + IPProtectionServerlist.PREF_NAME, + this.#observer + ); } - get bucket() { - if (!this.#bucket) { - this.#bucket = lazy.RemoteSettings("vpn-serverlist"); - } - return this.#bucket; + uninit() { + Services.prefs.removeObserver( + IPProtectionServerlist.PREF_NAME, + this.#observer + ); + } + maybeFetchList(_forceUpdate = false) { + this.__list = IPProtectionServerlistBase.dataToList( + PrefServerList.prefValue + ); + return Promise.resolve(); } - static #dataToList(list) { - if (!Array.isArray(list)) { - return []; + static get PREF_NAME() { + return "browser.ipProtection.override.serverlist"; + } + /** + * Returns true if the preference has a valid value. + */ + static get hasPrefValue() { + return ( + Services.prefs.getPrefType(this.PREF_NAME) === + Services.prefs.PREF_STRING && + !!Services.prefs.getStringPref(this.PREF_NAME).length + ); + } + static get prefValue() { + try { + const value = Services.prefs.getStringPref(this.PREF_NAME); + return JSON.parse(value); + } catch (e) { + console.error(`IPProtection: Error parsing serverlist pref value: ${e}`); + return null; } - - return list.map(c => new Country(c)); } } +/** + * + * @returns {IPProtectionServerlistBase} - The appropriate serverlist implementation. + */ +export function IPProtectionServerlistFactory() { + return PrefServerList.hasPrefValue + ? new PrefServerList() + : new RemoteSettingsServerlist(); +} -const IPProtectionServerlist = new IPProtectionServerlistSingleton(); +// Only check once which implementation to use. +const IPProtectionServerlist = IPProtectionServerlistFactory(); export { IPProtectionServerlist }; diff --git a/browser/components/ipprotection/docs/Preferences.rst b/browser/components/ipprotection/docs/Preferences.rst @@ -61,6 +61,9 @@ Networking and routing ``browser.ipProtection.domainExclusions`` (string) Comma‑separated list of domains to exclude from the proxy. +``browser.ipProtection.override.serverlist`` (string) + A JSON Payload that overrides the server list. Follows the Remote-Settings Schema. + Diagnostics ~~~~~~~~~~~ diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProtectionServerlist.js b/browser/components/ipprotection/tests/xpcshell/test_IPProtectionServerlist.js @@ -3,7 +3,12 @@ https://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -const { IPProtectionServerlist } = ChromeUtils.importESModule( +const { + IPProtectionServerlist, + PrefServerList, + RemoteSettingsServerlist, + IPProtectionServerlistFactory, +} = ChromeUtils.importESModule( "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs" ); @@ -86,6 +91,7 @@ add_setup(async function () { await IPProtectionServerlist.maybeFetchList(); await IPProtectionServerlist.initOnStartupCompleted(); + Assert.ok(IPProtectionServerlist instanceof RemoteSettingsServerlist); }); add_task(async function test_getDefaultLocation() { @@ -177,3 +183,47 @@ add_task(async function test_syncRespected() { Assert.equal(country.code, "US", "The default country should be US"); Assert.deepEqual(city, updated_city, "The updated city should be returned"); }); + +add_task(async function test_PrefServerList() { + registerCleanupFunction(() => { + Services.prefs.clearUserPref(PrefServerList.PREF_NAME); + }); + Services.prefs.setCharPref( + PrefServerList.PREF_NAME, + JSON.stringify(TEST_COUNTRIES) + ); + + Assert.equal( + PrefServerList.hasPrefValue, + true, + "PrefServerList should have a pref value set." + ); + Assert.deepEqual( + PrefServerList.prefValue, + TEST_COUNTRIES, + "PrefServerList's pref value should match the set value." + ); + + const serverList = new PrefServerList(); + await serverList.maybeFetchList(); + + const { country, city } = serverList.getDefaultLocation(); + Assert.equal(country.code, "US", "The default country should be US"); + Assert.deepEqual(city, TEST_US_CITY, "The default city should be returned."); +}); + +add_task(async function test_IPProtectionServerlistFactory() { + registerCleanupFunction(() => { + Services.prefs.clearUserPref(PrefServerList.PREF_NAME); + }); + // Without the pref set, it should return RemoteSettingsServerlist + Services.prefs.clearUserPref(PrefServerList.PREF_NAME); + let instance = IPProtectionServerlistFactory(); + Assert.ok(instance instanceof RemoteSettingsServerlist); + Services.prefs.setCharPref( + PrefServerList.PREF_NAME, + JSON.stringify(TEST_COUNTRIES) + ); + // With the pref set, it should return PrefServerList + Assert.ok(IPProtectionServerlistFactory() instanceof PrefServerList); +});