tor-browser

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

commit ecab32c779fc4877e1563ac06af5eb901d2b18a1
parent 32a0f944dc91a5f86e87540515f05559162100eb
Author: Andrea Marchesini <amarchesini@mozilla.com>
Date:   Fri, 24 Oct 2025 15:57:04 +0000

Bug 1993191 - IPProtection: Fetch the server list as soon as possible before creating the channel filter, r=ip-protection-reviewers,fchasen

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

Diffstat:
Mbrowser/components/ipprotection/IPPAutoStart.sys.mjs | 22++++++++++++++--------
Mbrowser/components/ipprotection/IPPProxyManager.sys.mjs | 13++++++++-----
Mbrowser/components/ipprotection/IPPStartupCache.sys.mjs | 20++++++++++++++++++++
Mbrowser/components/ipprotection/IPProtectionHelpers.sys.mjs | 2++
Mbrowser/components/ipprotection/IPProtectionServerlist.sys.mjs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mbrowser/components/ipprotection/IPProtectionService.sys.mjs | 4++++
Mbrowser/components/ipprotection/tests/browser/browser_IPPProxyManager.js | 5+++++
Mbrowser/components/ipprotection/tests/xpcshell/test_IPProtectionServerlist.js | 16+++++++++-------
Mbrowser/components/ipprotection/tests/xpcshell/test_IPProxyManager.js | 5+++++
9 files changed, 215 insertions(+), 64 deletions(-)

diff --git a/browser/components/ipprotection/IPPAutoStart.sys.mjs b/browser/components/ipprotection/IPPAutoStart.sys.mjs @@ -7,6 +7,8 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + IPProtectionServerlist: + "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs", IPProtectionService: "resource:///modules/ipprotection/IPProtectionService.sys.mjs", IPProtectionStates: @@ -20,13 +22,13 @@ const AUTOSTART_PREF = "browser.ipProtection.autoStartEnabled"; * calls `start()`. This is done only if the previous state was not a ACTIVE * because, in that case, more likely the VPN on/off state is an user decision. */ -class IPPAutoStart { +class IPPAutoStartSingleton { #shouldStartWhenReady = false; constructor() { XPCOMUtils.defineLazyPreferenceGetter( this, - "autoStart", + "autoStartPref", AUTOSTART_PREF, false, (_pref, _oldVal, featureEnabled) => { @@ -65,6 +67,12 @@ class IPPAutoStart { } } + get autoStart() { + // We activate the auto-start feature only if the pref is true and we have + // the serverlist already. + return this.autoStartPref && lazy.IPProtectionServerlist.hasList; + } + #handleEvent(_event) { switch (lazy.IPProtectionService.state) { case lazy.IPProtectionStates.UNINITIALIZED: @@ -87,6 +95,8 @@ class IPPAutoStart { } } +const IPPAutoStart = new IPPAutoStartSingleton(); + /** * This class monitors the startup phases and registers/unregisters the channel * filter to avoid data leak. The activation of the VPN is done by the @@ -97,11 +107,7 @@ class IPPEarlyStartupFilter { constructor() { this.handleEvent = this.#handleEvent.bind(this); - - this.#autoStartAndAtStartup = Services.prefs.getBoolPref( - AUTOSTART_PREF, - false - ); + this.#autoStartAndAtStartup = IPPAutoStart.autoStart; } init() { @@ -154,6 +160,6 @@ class IPPEarlyStartupFilter { } } -const IPPAutoStartHelpers = [new IPPAutoStart(), new IPPEarlyStartupFilter()]; +const IPPAutoStartHelpers = [IPPAutoStart, new IPPEarlyStartupFilter()]; export { IPPAutoStartHelpers }; diff --git a/browser/components/ipprotection/IPPProxyManager.sys.mjs b/browser/components/ipprotection/IPPProxyManager.sys.mjs @@ -11,9 +11,7 @@ ChromeUtils.defineESModuleGetters(lazy, { "resource:///modules/ipprotection/IPProtectionUsage.sys.mjs", IPPNetworkErrorObserver: "resource:///modules/ipprotection/IPPNetworkErrorObserver.sys.mjs", - getDefaultLocation: - "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs", - selectServer: + IPProtectionServerlist: "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs", }); @@ -117,8 +115,13 @@ class IPPProxyManager { this.#pass = await this.#getProxyPass(); } - const location = await lazy.getDefaultLocation(); - const server = await lazy.selectServer(location?.city); + const location = lazy.IPProtectionServerlist.getDefaultLocation(); + const server = lazy.IPProtectionServerlist.selectServer(location?.city); + if (!server) { + lazy.logConsole.error("No server found"); + throw new Error("No server found"); + } + lazy.logConsole.debug("Server:", server?.hostname); this.#connection.initialize( diff --git a/browser/components/ipprotection/IPPStartupCache.sys.mjs b/browser/components/ipprotection/IPPStartupCache.sys.mjs @@ -13,6 +13,7 @@ ChromeUtils.defineESModuleGetters(lazy, { const STATE_CACHE_PREF = "browser.ipProtection.stateCache"; const ENTITLEMENT_CACHE_PREF = "browser.ipProtection.entitlementCache"; +const LOCATIONLIST_CACHE_PREF = "browser.ipProtection.locationListCache"; /** * This class implements a cache for the IPP state machine. The cache is used @@ -118,6 +119,25 @@ class IPPStartupCacheSingleton { } } + storeLocationList(locationList) { + Services.prefs.setCharPref( + LOCATIONLIST_CACHE_PREF, + JSON.stringify(locationList) + ); + } + + get locationList() { + try { + const locationList = Services.prefs.getCharPref( + LOCATIONLIST_CACHE_PREF, + "" + ); + return JSON.parse(locationList); + } catch (e) { + return null; + } + } + #handleEvent(_event) { const state = lazy.IPProtectionService.state; if (this.#startupCompleted) { diff --git a/browser/components/ipprotection/IPProtectionHelpers.sys.mjs b/browser/components/ipprotection/IPProtectionHelpers.sys.mjs @@ -26,6 +26,7 @@ ChromeUtils.defineESModuleGetters(lazy, { import { IPPAutoStartHelpers } from "resource:///modules/ipprotection/IPPAutoStart.sys.mjs"; import { IPPEnrollHelper } from "resource:///modules/ipprotection/IPPEnrollHelper.sys.mjs"; import { IPPNimbusHelper } from "resource:///modules/ipprotection/IPPNimbusHelper.sys.mjs"; +import { IPProtectionServerlist } from "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs"; import { IPPSignInWatcher } from "resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs"; import { IPPStartupCache } from "resource:///modules/ipprotection/IPPStartupCache.sys.mjs"; @@ -150,6 +151,7 @@ const IPPHelpers = [ IPPStartupCache, IPPSignInWatcher, IPPEnrollHelper, + IPProtectionServerlist, new UIHelper(), new AccountResetHelper(), new VPNAddonHelper(), diff --git a/browser/components/ipprotection/IPProtectionServerlist.sys.mjs b/browser/components/ipprotection/IPProtectionServerlist.sys.mjs @@ -2,58 +2,26 @@ * 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/. */ -import { RemoteSettings } from "resource://services-settings/remote-settings.sys.mjs"; - /** * This file contains functions that work on top of the RemoteSettings * Bucket for the IP Protection server list. */ -/** - * Selects a default location - for alpha this is only the US. - * - * @param {RemoteSettingsClient} bucket - The RemoteSettings bucket to use - * @returns {{Country, City}} - The best country/city to use. - */ -export async function getDefaultLocation( - bucket = RemoteSettings("vpn-serverlist") -) { - const list = await bucket.get(); - /** @type {Country} */ - const usa = list.find(country => country.code === "US"); - if (!usa) { - return null; - } - const city = usa.cities.find(c => c.servers.length); - return { - city, - country: usa, - }; -} +const lazy = {}; -/** - * Given a city, it selects an available server. - * - * @param {City?} city - * @returns {Server|null} - */ -export async function selectServer(city) { - if (!city) { - return null; - } - const servers = city.servers.filter(server => !server.quarantined); - if (servers.length === 1) { - return servers[0]; - } else if (servers.length > 1) { - return servers[Math.floor(Math.random() * servers.length)]; - } - return null; -} +ChromeUtils.defineESModuleGetters(lazy, { + IPPStartupCache: "resource:///modules/ipprotection/IPPStartupCache.sys.mjs", + IPProtectionService: + "resource:///modules/ipprotection/IPProtectionService.sys.mjs", + IPProtectionStates: + "resource:///modules/ipprotection/IPProtectionService.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", +}); /** * Class representing a server. */ -export class Server { +class Server { /** * Port of the server * @@ -73,12 +41,18 @@ export class Server { * @type {boolean} */ quarantined = false; + + constructor(data) { + this.port = data.port || 443; + this.hostname = data.hostname || ""; + this.quarantined = !!data.quarantined; + } } /** * Class representing a city. */ -export class City { +class City { /** * Fallback name for the city if not available * @@ -98,12 +72,18 @@ export class City { * @type {Server[]} */ servers = []; + + constructor(data) { + this.name = data.name || ""; + this.code = data.code || ""; + this.servers = (data.servers || []).map(s => new Server(s)); + } } /** * Class representing a country. */ -export class Country { +class Country { /** * Fallback name for the country if not available * @@ -124,4 +104,128 @@ export class Country { * @type {City[]} */ cities; + + constructor(data) { + this.name = data.name || ""; + this.code = data.code || ""; + this.cities = (data.cities || []).map(c => new City(c)); + } +} + +/** + * + */ +class IPProtectionServerlistSingleton { + #list = null; + #runningPromise = null; + + constructor() { + this.handleEvent = this.#handleEvent.bind(this); + this.#list = IPProtectionServerlistSingleton.#dataToList( + lazy.IPPStartupCache.locationList + ); + } + + init() { + lazy.IPProtectionService.addEventListener( + "IPProtectionService:StateChanged", + this.handleEvent + ); + } + + async initOnStartupCompleted() {} + + uninit() { + lazy.IPProtectionService.removeEventListener( + "IPProtectionService:StateChanged", + this.handleEvent + ); + } + + #handleEvent(_event) { + if (lazy.IPProtectionService.state === lazy.IPProtectionStates.READY) { + this.maybeFetchList(); + } + } + + maybeFetchList() { + if (this.#list.length !== 0) { + return Promise.resolve(); + } + + if (this.#runningPromise) { + return this.#runningPromise; + } + + const fetchList = async () => { + const bucket = lazy.RemoteSettings("vpn-serverlist"); + this.#list = IPProtectionServerlistSingleton.#dataToList( + await bucket.get() + ); + }; + + this.#runningPromise = fetchList().finally( + () => (this.#runningPromise = null) + ); + + 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; + } + + 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)); + } } + +const IPProtectionServerlist = new IPProtectionServerlistSingleton(); + +export { IPProtectionServerlist }; diff --git a/browser/components/ipprotection/IPProtectionService.sys.mjs b/browser/components/ipprotection/IPProtectionService.sys.mjs @@ -12,6 +12,8 @@ ChromeUtils.defineESModuleGetters(lazy, { IPPHelpers: "resource:///modules/ipprotection/IPProtectionHelpers.sys.mjs", IPPNimbusHelper: "resource:///modules/ipprotection/IPPNimbusHelper.sys.mjs", IPPProxyManager: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs", + IPProtectionServerlist: + "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs", IPPSignInWatcher: "resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs", IPPStartupCache: "resource:///modules/ipprotection/IPPStartupCache.sys.mjs", SpecialMessageActions: @@ -203,6 +205,8 @@ class IPProtectionServiceSingleton extends EventTarget { * True if started by user action, false if system action */ async start(userAction = true) { + await lazy.IPProtectionServerlist.maybeFetchList(); + // Wait for enrollment to finish. const enrollData = await lazy.IPPEnrollHelper.maybeEnroll(); if (!enrollData || !enrollData.isEnrolled) { diff --git a/browser/components/ipprotection/tests/browser/browser_IPPProxyManager.js b/browser/components/ipprotection/tests/browser/browser_IPPProxyManager.js @@ -7,6 +7,9 @@ const { IPPProxyManager } = ChromeUtils.importESModule( "resource:///modules/ipprotection/IPPProxyManager.sys.mjs" ); +const { IPProtectionServerlist } = ChromeUtils.importESModule( + "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs" +); // Don't add an experiment so we can test adding and removing it. DEFAULT_EXPERIMENT = null; @@ -20,6 +23,8 @@ add_task(async function test_IPPProxyManager_handleProxyErrorEvent() { let proxyManager = new IPPProxyManager(IPProtectionService.guardian); + await IPProtectionServerlist.maybeFetchList(); + await proxyManager.start(); const cases = [ diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProtectionServerlist.js b/browser/components/ipprotection/tests/xpcshell/test_IPProtectionServerlist.js @@ -3,7 +3,7 @@ https://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -const { getDefaultLocation, selectServer } = ChromeUtils.importESModule( +const { IPProtectionServerlist } = ChromeUtils.importESModule( "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs" ); @@ -59,17 +59,19 @@ add_setup(async function () { await client.db.create(country); } await client.db.importChanges({}, Date.now()); + + await IPProtectionServerlist.maybeFetchList(); }); add_task(async function test_getDefaultLocation() { - const { country, city } = await getDefaultLocation(); + const { country, city } = IPProtectionServerlist.getDefaultLocation(); Assert.equal(country.code, "US", "The default country should be US"); Assert.deepEqual(city, TEST_US_CITY, "The correct city should be returned"); }); add_task(async function test_selectServer() { // Test with a city with multiple non-quarantined servers - let selected = await selectServer(TEST_US_CITY); + let selected = IPProtectionServerlist.selectServer(TEST_US_CITY); Assert.ok( [TEST_SERVER_1, TEST_SERVER_2].some(s => s.hostname === selected.hostname), "A valid server should be selected" @@ -81,7 +83,7 @@ add_task(async function test_selectServer() { code: "OSC", servers: [TEST_SERVER_1], }; - selected = await selectServer(cityWithOneServer); + selected = IPProtectionServerlist.selectServer(cityWithOneServer); Assert.deepEqual( selected, TEST_SERVER_1, @@ -94,7 +96,7 @@ add_task(async function test_selectServer() { code: "MSC", servers: [TEST_SERVER_1, TEST_SERVER_QUARANTINED], }; - selected = await selectServer(cityWithMixedServers); + selected = IPProtectionServerlist.selectServer(cityWithMixedServers); Assert.deepEqual( selected, TEST_SERVER_1, @@ -107,7 +109,7 @@ add_task(async function test_selectServer() { code: "QC", servers: [TEST_SERVER_QUARANTINED], }; - selected = await selectServer(cityWithQuarantinedServers); + selected = IPProtectionServerlist.selectServer(cityWithQuarantinedServers); Assert.equal(selected, null, "No server should be selected"); // Test with a city with no servers @@ -116,6 +118,6 @@ add_task(async function test_selectServer() { code: "NSC", servers: [], }; - selected = await selectServer(cityWithNoServers); + selected = IPProtectionServerlist.selectServer(cityWithNoServers); Assert.equal(selected, null, "No server should be selected"); }); diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProxyManager.js b/browser/components/ipprotection/tests/xpcshell/test_IPProxyManager.js @@ -7,6 +7,9 @@ const { IPPProxyManager } = ChromeUtils.importESModule( "resource:///modules/ipprotection/IPPProxyManager.sys.mjs" ); +const { IPProtectionServerlist } = ChromeUtils.importESModule( + "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs" +); const { GuardianClient } = ChromeUtils.importESModule( "resource:///modules/ipprotection/GuardianClient.sys.mjs" ); @@ -31,6 +34,8 @@ add_task(async function test_IPPProxyManager_start_stop_reset() { }, }); + await IPProtectionServerlist.maybeFetchList(); + let proxyManager = new IPPProxyManager(guardian); await proxyManager.start();