[tbb-commits] [Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 7 commits: fixup! Bug 40933: Add tor-launcher functionality
richard (@richard)
git at gitlab.torproject.org
Thu Jul 27 18:01:01 UTC 2023
richard pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
66229717 by Pier Angelo Vendrame at 2023-07-27T18:11:55+02:00
fixup! Bug 40933: Add tor-launcher functionality
Bug 41844: Added a couple of wrappers for Onion Auth on
TorProtocolService.
- - - - -
2cde9fc3 by Pier Angelo Vendrame at 2023-07-27T18:11:56+02:00
fixup! Bug 30237: Add v3 onion services client authentication prompt
Bug 41844: Stop using the control port directly
- - - - -
b2cd0ee8 by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00
fixup! Bug 40933: Add tor-launcher functionality
Small improvements on event registration.
- - - - -
26152fa9 by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00
fixup! Bug 40933: Add tor-launcher functionality
Bug 41844: Do not use a the control port directly.
Collect the bridge node for the about:preferences#connection page in
TorMonitorService.
Also, move parts of the circuit display to TorMonitorService and
TorProtocolService.
- - - - -
749aeaca by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00
fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 41844: Do not use the control port directly
Do not use the controller in the settings frontend.
Instead, let TorMonitorService collect the first node's fingerprint.
- - - - -
20641450 by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00
fixup! Bug 3455: Add DomainIsolator, for isolating circuit by domain.
Bug 41844: Do not use the control port directly.
Use TorDomainIsolator also as a backend for the circuit display.
- - - - -
1a8be7b1 by Pier Angelo Vendrame at 2023-07-27T18:11:59+02:00
fixup! Bug 41600: Add a tor circuit display panel.
Bug 41844: Have a separate backend for the tor circuits
Remove the backend stuff from the circuit display.
- - - - -
11 changed files:
- browser/base/content/browser.js
- browser/components/onionservices/content/authPrompt.js
- browser/components/onionservices/content/savedKeysDialog.js
- browser/components/torcircuit/content/torCircuitPanel.js
- browser/components/torpreferences/content/connectionPane.js
- toolkit/components/tor-launcher/TorDomainIsolator.jsm → toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
- toolkit/components/tor-launcher/TorMonitorService.sys.mjs
- toolkit/components/tor-launcher/TorParsers.sys.mjs
- toolkit/components/tor-launcher/TorProtocolService.sys.mjs
- toolkit/components/tor-launcher/TorStartupService.sys.mjs
- toolkit/components/tor-launcher/moz.build
Changes:
=====================================
browser/base/content/browser.js
=====================================
@@ -66,6 +66,7 @@ ChromeUtils.defineESModuleGetters(this, {
TabsSetupFlowManager:
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs",
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
+ TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
UITour: "resource:///modules/UITour.sys.mjs",
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
@@ -100,7 +101,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
TorConnect: "resource:///modules/TorConnect.jsm",
TorConnectState: "resource:///modules/TorConnect.jsm",
TorConnectTopics: "resource:///modules/TorConnect.jsm",
- TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.jsm",
Translation: "resource:///modules/translation/TranslationParent.jsm",
webrtcUI: "resource:///modules/webrtcUI.jsm",
ZoomUI: "resource:///modules/ZoomUI.jsm",
=====================================
browser/components/onionservices/content/authPrompt.js
=====================================
@@ -7,6 +7,7 @@
XPCOMUtils.defineLazyModuleGetters(this, {
OnionAuthUtil: "chrome://browser/content/onionservices/authUtil.jsm",
CommonUtils: "resource://services-common/utils.js",
+ TorProtocolService: "resource://gre/modules/TorProtocolService.jsm",
TorStrings: "resource:///modules/TorStrings.jsm",
});
@@ -192,10 +193,6 @@ const OnionAuthPrompt = (function () {
let controllerFailureMsg =
TorStrings.onionServices.authPrompt.failedToSetKey;
try {
- let { controller } = ChromeUtils.import(
- "resource://torbutton/modules/tor-control-port.js"
- );
- let torController = await controller();
// ^(subdomain.)*onionserviceid.onion$ (case-insensitive)
const onionServiceIdRegExp =
/^(.*\.)*(?<onionServiceId>[a-z2-7]{56})\.onion$/i;
@@ -206,8 +203,7 @@ const OnionAuthPrompt = (function () {
let checkboxElem = this._getCheckboxElement();
let isPermanent = checkboxElem && checkboxElem.checked;
- torController
- .onionAuthAdd(onionServiceId, base64key, isPermanent)
+ TorProtocolService.onionAuthAdd(onionServiceId, base64key, isPermanent)
.then(aResponse => {
// Success! Reload the page.
this._browser.sendMessageToActor(
=====================================
browser/components/onionservices/content/savedKeysDialog.js
=====================================
@@ -10,8 +10,8 @@ ChromeUtils.defineModuleGetter(
ChromeUtils.defineModuleGetter(
this,
- "controller",
- "resource://torbutton/modules/tor-control-port.js"
+ "TorProtocolService",
+ "resource://gre/modules/TorProtocolService.jsm"
);
var gOnionServicesSavedKeysDialog = {
@@ -49,11 +49,9 @@ var gOnionServicesSavedKeysDialog = {
const controllerFailureMsg =
TorStrings.onionServices.authPreferences.failedToRemoveKey;
try {
- const torController = await controller();
-
// Remove in reverse index order to avoid issues caused by index changes.
for (let i = indexesToDelete.length - 1; i >= 0; --i) {
- await this._deleteOneKey(torController, indexesToDelete[i]);
+ await this._deleteOneKey(indexesToDelete[i]);
}
} catch (e) {
if (e.torMessage) {
@@ -127,8 +125,7 @@ var gOnionServicesSavedKeysDialog = {
try {
this._tree.view = this;
- const torController = await controller();
- const keyInfoList = await torController.onionAuthViewKeys();
+ const keyInfoList = await TorProtocolService.onionAuthViewKeys();
if (keyInfoList) {
// Filter out temporary keys.
this._keyInfoList = keyInfoList.filter(aKeyInfo => {
@@ -165,9 +162,9 @@ var gOnionServicesSavedKeysDialog = {
},
// This method may throw; callers should catch errors.
- async _deleteOneKey(aTorController, aIndex) {
+ async _deleteOneKey(aIndex) {
const keyInfoObj = this._keyInfoList[aIndex];
- await aTorController.onionAuthRemove(keyInfoObj.hsAddress);
+ await TorProtocolService.onionAuthRemove(keyInfoObj.hsAddress);
this._tree.view.selection.clearRange(aIndex, aIndex);
this._keyInfoList.splice(aIndex, 1);
this._tree.rowCountChanged(aIndex + 1, -1);
=====================================
browser/components/torcircuit/content/torCircuitPanel.js
=====================================
@@ -1,18 +1,5 @@
/* eslint-env mozilla/browser-window */
-/**
- * Stores the data associated with a circuit node.
- *
- * @typedef NodeData
- * @property {string[]} ipAddrs - The ip addresses associated with this node.
- * @property {string?} bridgeType - The bridge type for this node, or "" if the
- * node is a bridge but the type is unknown, or null if this is not a bridge
- * node.
- * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for
- * the first ip address, or null if there is no region. This should also be a
- * valid BCP47 Region subtag.
- */
-
/**
* Data about the current domain and circuit for a xul:browser.
*
@@ -35,29 +22,6 @@ var gTorCircuitPanel = {
* @type {Element}
*/
toolbarButton: null,
- /**
- * A list of IDs for "mature" circuits (those that have conveyed a stream).
- *
- * @type {string[]}
- */
- _knownCircuitIDs: [],
- /**
- * Stores the circuit nodes for each SOCKS username/password pair. The keys
- * are of the form "<username>|<password>".
- *
- * @type {Map<string, NodeData[]>}
- */
- _credentialsToCircuitNodes: new Map(),
- /**
- * Browser data for their currently shown page.
- *
- * This data may be stale for a given browser since we only update this data
- * when loading a new page in the currently selected browser, when switching
- * tabs, or if we find a new circuit for the current browser.
- *
- * @type {WeakMap<MozBrowser, BrowserCircuitData>}
- */
- _browserData: new WeakMap(),
/**
* The data for the currently shown browser.
*
@@ -71,6 +35,13 @@ var gTorCircuitPanel = {
*/
_isActive: false,
+ /**
+ * The topic on which circuit changes are broadcast.
+ *
+ * @type {string}
+ */
+ TOR_CIRCUIT_TOPIC: "TorCircuitChange",
+
/**
* Initialize the panel.
*/
@@ -86,31 +57,6 @@ var gTorCircuitPanel = {
maxLogLevelPref: "browser.torcircuitpanel.loglevel",
});
- const { wait_for_controller } = ChromeUtils.import(
- "resource://torbutton/modules/tor-control-port.js"
- );
- wait_for_controller().then(
- controller => {
- if (!this._isActive) {
- // uninit() was called before resolution.
- return;
- }
- // FIXME: We should be using some dedicated integrated back end to
- // store circuit information, rather than collecting it all here in the
- // front end. See tor-browser#41700.
- controller.watchEvent(
- "STREAM",
- streamEvent => streamEvent.StreamStatus === "SENTCONNECT",
- streamEvent => this._collectCircuit(controller, streamEvent)
- );
- },
- error => {
- this._log.error(
- `Not collecting circuits because of an error: ${error.message}`
- );
- }
- );
-
this.panel = document.getElementById("tor-circuit-panel");
this._panelElements = {
heading: document.getElementById("tor-circuit-heading"),
@@ -245,6 +191,9 @@ var gTorCircuitPanel = {
// Notified of new locations for the currently selected browser (tab) *and*
// switching selected browser.
gBrowser.addProgressListener(this._locationListener);
+
+ // Get notifications for circuit changes.
+ Services.obs.addObserver(this, this.TOR_CIRCUIT_TOPIC);
},
/**
@@ -253,6 +202,17 @@ var gTorCircuitPanel = {
uninit() {
this._isActive = false;
gBrowser.removeProgressListener(this._locationListener);
+ Services.obs.removeObserver(this, this.TOR_CIRCUIT_TOPIC);
+ },
+
+ /**
+ * Observe circuit changes.
+ */
+ observe(subject, topic, data) {
+ if (topic === this.TOR_CIRCUIT_TOPIC) {
+ // TODO: Maybe check if we actually need to do something earlier.
+ this._updateCurrentBrowser();
+ }
},
/**
@@ -286,109 +246,6 @@ var gTorCircuitPanel = {
window.openWebLinkIn(this._panelElements.aliasLink.href, where);
},
- /**
- * Collect circuit data for the found circuits, to be used later for display.
- *
- * @param {controller} controller - The tor controller.
- * @param {object} streamEvent - The streamEvent for the new circuit.
- */
- async _collectCircuit(controller, streamEvent) {
- const id = streamEvent.CircuitID;
- if (this._knownCircuitIDs.includes(id)) {
- return;
- }
- this._log.debug(`New streamEvent.CircuitID: ${id}.`);
- // FIXME: This list grows and is never freed. See tor-browser#41700.
- this._knownCircuitIDs.push(id);
- const circuitStatus = (await controller.getInfo("circuit-status"))?.find(
- circuit => circuit.id === id
- );
- if (!circuitStatus?.SOCKS_USERNAME || !circuitStatus?.SOCKS_PASSWORD) {
- return;
- }
- const nodes = await Promise.all(
- circuitStatus.circuit.map(names =>
- this._nodeDataForCircuit(controller, names)
- )
- );
- // Remove quotes from the strings.
- const username = circuitStatus.SOCKS_USERNAME.replace(/^"(.*)"$/, "$1");
- const password = circuitStatus.SOCKS_PASSWORD.replace(/^"(.*)"$/, "$1");
- const credentials = `${username}|${password}`;
- // FIXME: This map grows and is never freed. We cannot simply request this
- // information when needed because it is no longer available once the
- // circuit is dropped, even if the web page is still displayed.
- // See tor-browser#41700.
- this._credentialsToCircuitNodes.set(credentials, nodes);
- // Update the circuit in case the current page gains a new circuit whilst
- // the popup is still open.
- this._updateCurrentBrowser(credentials);
- },
-
- /**
- * Fetch the node data for the given circuit node.
- *
- * @param {controller} controller - The tor controller.
- * @param {string[]} circuitNodeNames - The names for the circuit node. Only
- * the first name, the node id, will be used.
- *
- * @returns {NodeData} - The data for this circuit node.
- */
- async _nodeDataForCircuit(controller, circuitNodeNames) {
- // The first "name" in circuitNodeNames is the id.
- // Remove the leading '$' if present.
- const id = circuitNodeNames[0].replace(/^\$/, "");
- let result = { ipAddrs: [], bridgeType: null, regionCode: null };
- const bridge = (await controller.getConf("bridge"))?.find(
- foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase()
- );
- const addrRe = /^\[?([^\]]+)\]?:\d+$/;
- if (bridge) {
- result.bridgeType = bridge.type ?? "";
- // Attempt to get an IP address from bridge address string.
- const ip = bridge.address.match(addrRe)?.[1];
- if (ip && !ip.startsWith("0.")) {
- result.ipAddrs.push(ip);
- }
- } else {
- // Either dealing with a relay, or a bridge whose fingerprint is not saved
- // in torrc.
- let statusMap;
- try {
- statusMap = await controller.getInfo("ns/id/" + id);
- } catch {
- // getInfo will throw if the given id is not a relay.
- // This probably means we are dealing with a user-provided bridge with
- // no fingerprint.
- // We don't know the ip/ipv6 or type, so leave blank.
- result.bridgeType = "";
- return result;
- }
- if (statusMap.IP && !statusMap.IP.startsWith("0.")) {
- result.ipAddrs.push(statusMap.IP);
- }
- const ip6 = statusMap.IPv6?.match(addrRe)?.[1];
- if (ip6) {
- result.ipAddrs.push(ip6);
- }
- }
- if (result.ipAddrs.length) {
- // Get the country code for the node's IP address.
- let regionCode;
- try {
- // Expect a 2-letter ISO3166-1 code, which should also be a valid BCP47
- // Region subtag.
- regionCode = await controller.getInfo(
- "ip-to-country/" + result.ipAddrs[0]
- );
- } catch {}
- if (regionCode && regionCode !== "??") {
- result.regionCode = regionCode.toUpperCase();
- }
- }
- return result;
- },
-
/**
* A list of schemes to never show the circuit display for.
*
@@ -398,71 +255,50 @@ var gTorCircuitPanel = {
*
* @type {string[]}
*/
- // FIXME: Have a back end that handles this instead. See tor-browser#41700.
+ // FIXME: Check if we find a UX to handle some of these cases, and if we
+ // manage to solve some technical issues.
+ // See tor-browser#41700 and tor-browser!699.
_ignoredSchemes: ["about", "file", "chrome", "resource"],
/**
* Update the current circuit and domain data for the currently selected
* browser, possibly changing the UI.
- *
- * @param {string?} [matchingCredentials=null] - If given, only update the
- * current browser data if the current browser's credentials match.
*/
- _updateCurrentBrowser(matchingCredentials = null) {
+ _updateCurrentBrowser() {
const browser = gBrowser.selectedBrowser;
const domain = TorDomainIsolator.getDomainForBrowser(browser);
+ const nodes = TorDomainIsolator.getCircuit(
+ browser,
+ domain,
+ browser.contentPrincipal.originAttributes.userContextId
+ );
// We choose the currentURI, which matches what is shown in the URL bar and
// will match up with the domain.
// In contrast, documentURI corresponds to the shown page. E.g. it could
// point to "about:certerror".
const scheme = browser.currentURI?.scheme;
- let credentials = TorDomainIsolator.getSocksProxyCredentials(
- domain,
- browser.contentPrincipal.originAttributes.userContextId
- );
- if (credentials) {
- credentials = `${credentials.username}|${credentials.password}`;
- }
-
- if (matchingCredentials && matchingCredentials !== credentials) {
- // This update was triggered by the circuit update for some other browser
- // or process.
- return;
- }
-
- let nodes = this._credentialsToCircuitNodes.get(credentials) ?? [];
-
- const prevData = this._browserData.get(browser);
- if (
- prevData &&
- prevData.domain &&
- prevData.domain === domain &&
- prevData.scheme === scheme &&
- prevData.nodes.length &&
- !nodes.length
- ) {
- // Since this is the same domain, for the same browser, and we used to
- // have circuit nodes, we *assume* we are re-generating a circuit. So we
- // keep the old circuit data around for the time being.
- // FIXME: Have a back end that makes this explicit, rather than an
- // assumption. See tor-browser#41700.
- nodes = prevData.nodes;
- this._log.debug(`Keeping old circuit for ${domain}.`);
- }
-
- this._browserData.set(browser, { domain, scheme, nodes });
if (
this._currentBrowserData &&
this._currentBrowserData.domain === domain &&
this._currentBrowserData.scheme === scheme &&
- this._currentBrowserData.nodes === nodes
+ this._currentBrowserData.nodes.length === nodes.length &&
+ // If non-null, the fingerprints of the nodes match.
+ (!nodes ||
+ nodes.every(
+ (n, index) =>
+ n.fingerprint === this._currentBrowserData.nodes[index].fingerprint
+ ))
) {
// No change.
+ this._log.debug(
+ "Skipping browser update because the data is already up to date."
+ );
return;
}
- this._currentBrowserData = this._browserData.get(browser);
+ this._currentBrowserData = { domain, scheme, nodes };
+ this._log.debug("Updating current browser.", this._currentBrowserData);
if (
// Schemes where we always want to hide the display.
=====================================
browser/components/torpreferences/content/connectionPane.js
=====================================
@@ -17,6 +17,9 @@ const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource } =
const { TorProtocolService } = ChromeUtils.import(
"resource://gre/modules/TorProtocolService.jsm"
);
+const { TorMonitorService, TorMonitorTopics } = ChromeUtils.import(
+ "resource://gre/modules/TorMonitorService.jsm"
+);
const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } =
ChromeUtils.import("resource:///modules/TorConnect.jsm");
@@ -144,8 +147,6 @@ const gConnectionPane = (function () {
_internetStatus: InternetStatus.Unknown,
- _controller: null,
-
_currentBridgeId: null,
// populate xul with strings and cache the relevant elements
@@ -727,9 +728,10 @@ const gConnectionPane = (function () {
};
// Use a promise to avoid blocking the population of the page
// FIXME: Stop using a JSON file, and switch to properties
- fetch(
+ const annotationPromise = fetch(
"chrome://browser/content/torpreferences/bridgemoji/annotations.json"
- ).then(async res => {
+ );
+ annotationPromise.then(async res => {
const annotations = await res.json();
const bcp47 = Services.locale.appLocaleAsBCP47;
const dash = bcp47.indexOf("-");
@@ -749,6 +751,7 @@ const gConnectionPane = (function () {
".currently-connected"
)) {
card.classList.remove("currently-connected");
+ card.querySelector(selectors.bridges.cardQrGrid).style.height = "";
}
if (!this._currentBridgeId) {
return;
@@ -769,72 +772,17 @@ const gConnectionPane = (function () {
placeholder.replaceWith(...cards);
this._checkBridgeCardsHeight();
};
- try {
- const { controller } = ChromeUtils.import(
- "resource://torbutton/modules/tor-control-port.js"
- );
- // Avoid the cache because we set our custom event watcher, and at the
- // moment, watchers cannot be removed from a controller.
- controller(true).then(aController => {
- this._controller = aController;
- // Getting the circuits may be enough, if we have bootstrapped for a
- // while, but at the beginning it gives many bridges as connected,
- // because tor pokes all the bridges to find the best one.
- // Also, watching circuit events does not work, at the moment, but in
- // any case, checking the stream has the advantage that we can see if
- // it really used for a connection, rather than tor having created
- // this circuit to check if the bridge can be used. We do this by
- // checking if the stream has SOCKS username, which actually contains
- // the destination of the stream.
- // FIXME: We only know the currentBridge *after* a circuit event, but
- // if the circuit event is sent *before* about:torpreferences is
- // opened we will miss it. Therefore this approach only works if a
- // circuit is created after opening about:torconnect. A dedicated
- // backend outside of about:preferences would help, and could be
- // shared with gTorCircuitPanel. See tor-browser#41700.
- this._controller.watchEvent(
- "STREAM",
- event =>
- event.StreamStatus === "SUCCEEDED" && "SOCKS_USERNAME" in event,
- async event => {
- const circuitStatuses = await this._controller.getInfo(
- "circuit-status"
- );
- if (!circuitStatuses) {
- return;
- }
- for (const status of circuitStatuses) {
- if (status.id === event.CircuitID && status.circuit.length) {
- // The id in the circuit begins with a $ sign.
- const id = status.circuit[0][0].replace(/^\$/, "");
- if (id !== this._currentBridgeId) {
- const bridge = (
- await this._controller.getConf("bridge")
- )?.find(
- foundBridge =>
- foundBridge.ID?.toUpperCase() === id.toUpperCase()
- );
- if (!bridge) {
- // Either there is no bridge, or bridge with no
- // fingerprint.
- this._currentBridgeId = null;
- } else {
- this._currentBridgeId = id;
- }
- this._updateConnectedBridges();
- }
- break;
- }
- }
- }
- );
- });
- } catch (err) {
- console.warn(
- "We could not load torbutton, bridge statuses will not be updated",
- err
- );
- }
+ this._checkConnectedBridge = () => {
+ // TODO: We could make sure TorSettings is in sync by monitoring also
+ // changes of settings. At that point, we could query it, instead of
+ // doing a query over the control port.
+ const bridge = TorMonitorService.currentBridge;
+ if (bridge?.fingerprint !== this._currentBridgeId) {
+ this._currentBridgeId = bridge?.fingerprint ?? null;
+ this._updateConnectedBridges();
+ }
+ };
+ annotationPromise.then(this._checkConnectedBridge.bind(this));
// Add a new bridge
prefpane.querySelector(selectors.bridges.addHeader).textContent =
@@ -927,6 +875,7 @@ const gConnectionPane = (function () {
});
Services.obs.addObserver(this, TorConnectTopics.StateChange);
+ Services.obs.addObserver(this, TorMonitorTopics.BridgeChanged);
},
init() {
@@ -950,11 +899,7 @@ const gConnectionPane = (function () {
// unregister our observer topics
Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged);
Services.obs.removeObserver(this, TorConnectTopics.StateChange);
-
- if (this._controller !== null) {
- this._controller.close();
- this._controller = null;
- }
+ Services.obs.removeObserver(this, TorMonitorTopics.BridgeChanged);
},
// whether the page should be present in about:preferences
@@ -985,6 +930,12 @@ const gConnectionPane = (function () {
this.onStateChange();
break;
}
+ case TorMonitorTopics.BridgeChanged: {
+ if (data?.fingerprint !== this._currentBridgeId) {
+ this._checkConnectedBridge();
+ }
+ break;
+ }
}
},
@@ -1028,7 +979,7 @@ const gConnectionPane = (function () {
onRemoveAllBridges() {
TorSettings.bridges.enabled = false;
TorSettings.bridges.bridge_strings = "";
- if (TorSettings.bridges.source == TorBridgeSource.BuiltIn) {
+ if (TorSettings.bridges.source === TorBridgeSource.BuiltIn) {
TorSettings.bridges.builtin_type = "";
}
TorSettings.saveToPrefs();
=====================================
toolkit/components/tor-launcher/TorDomainIsolator.jsm → toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
=====================================
@@ -1,13 +1,14 @@
-// A component for Tor Browser that puts requests from different
-// first party domains on separate Tor circuits.
-
-var EXPORTED_SYMBOLS = ["TorDomainIsolator"];
+/**
+ * A component for Tor Browser that puts requests from different first party
+ * domains on separate Tor circuits.
+ */
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { XPCOMUtils } = ChromeUtils.import(
- "resource://gre/modules/XPCOMUtils.jsm"
-);
-const { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
+import {
+ clearInterval,
+ setInterval,
+} from "resource://gre/modules/Timer.sys.mjs";
const lazy = {};
@@ -18,11 +19,10 @@ XPCOMUtils.defineLazyServiceGetters(lazy, {
],
});
-ChromeUtils.defineModuleGetter(
- lazy,
- "TorProtocolService",
- "resource://gre/modules/TorProtocolService.jsm"
-);
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorMonitorTopics: "resource://gre/modules/TorMonitorService.sys.mjs",
+ TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
+});
const logger = new ConsoleAPI({
prefix: "TorDomainIsolator",
@@ -33,6 +33,12 @@ const logger = new ConsoleAPI({
// The string to use instead of the domain when it is not known.
const CATCHALL_DOMAIN = "--unknown--";
+// The maximum lifetime for the catch-all circuit in milliseconds.
+// When the catch-all circuit is needed, we check if more than this amount of
+// time has passed since we last changed it nonce, and in case we change it
+// again.
+const CATCHALL_MAX_LIFETIME = 600_000;
+
// The preference to observe, to know whether isolation should be enabled or
// disabled.
const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy";
@@ -40,23 +46,92 @@ const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy";
// The topic of new identity, to observe to cleanup all the nonces.
const NEW_IDENTITY_TOPIC = "new-identity-requested";
+// The topic on which we broacast circuit change notifications.
+const TOR_CIRCUIT_TOPIC = "TorCircuitChange";
+
+// We have an interval to delete circuits that are not reclaimed by any browser.
+const CLEAR_TIMEOUT = 600_000;
+
+/**
+ * @typedef {string} CircuitId A string that we use to identify a circuit.
+ * Currently, it is a string that combines SOCKS credentials, to make it easier
+ * to use as a map key.
+ * It is not related to Tor's CircuitIDs.
+ */
+/**
+ * @typedef {number} BrowserId
+ */
+/**
+ * @typedef {NodeData[]} CircuitData The data about the nodes, ordered from
+ * guard (or bridge) to exit.
+ */
+/**
+ * @typedef BrowserCircuits Circuits related to a certain combination of
+ * isolators (first-party domain and user context ID, currently).
+ * @property {CircuitId} current The id of the last known circuit that has been
+ * used to fetch data for the isolated context.
+ * @property {CircuitId?} pending The id of the last used circuit for this
+ * isolation context. We might or might not know data about it, yet. But if we
+ * know it, we should move this id into current.
+ */
+
class TorDomainIsolatorImpl {
- // A mutable map that records what nonce we are using for each domain.
+ /**
+ * A mutable map that records what nonce we are using for each domain.
+ *
+ * @type {Map<string, string>}
+ */
#noncesForDomains = new Map();
- // A mutable map that records what nonce we are using for each tab container.
+ /**
+ * A mutable map that records what nonce we are using for each tab container.
+ *
+ * @type {Map<string, string>}
+ */
#noncesForUserContextId = new Map();
- // A bool that controls if we use SOCKS auth for isolation or not.
+ /**
+ * Tell whether we use SOCKS auth for isolation or not.
+ *
+ * @type {boolean}
+ */
#isolationEnabled = true;
- // Specifies when the current catch-all circuit was first used
+ /**
+ * Specifies when the current catch-all circuit was first used.
+ *
+ * @type {integer}
+ */
#catchallDirtySince = Date.now();
+ /**
+ * A map that associates circuit ids to the circuit information.
+ *
+ * @type {Map<CircuitId, CircuitData>}
+ */
+ #knownCircuits = new Map();
+
+ /**
+ * A map that associates a certain browser to all the circuits it used or it
+ * is going to use.
+ * The circuits are keyed on the SOCKS username, which we take for granted
+ * being a combination of the first-party domain and the user context id.
+ *
+ * @type {Map<BrowserId, Map<string, BrowserCircuits>>}
+ */
+ #browsers = new Map();
+
+ /**
+ * The handle of the interval we use to cleanup old circuit data.
+ *
+ * @type {number?}
+ */
+ #cleanupIntervalId = null;
+
/**
* Initialize the domain isolator.
- * This function will setup the proxy filter that injects the credentials and
- * register some observers.
+ * This function will setup the proxy filter that injects the credentials,
+ * register some observers, and setup the cleaning interval.
*/
init() {
logger.info("Setup circuit isolation by domain and user context");
@@ -68,14 +143,25 @@ class TorDomainIsolatorImpl {
Services.prefs.addObserver(NON_TOR_PROXY_PREF, this);
Services.obs.addObserver(this, NEW_IDENTITY_TOPIC);
+ Services.obs.addObserver(this, lazy.TorMonitorTopics.StreamSucceeded);
+
+ this.#cleanupIntervalId = setInterval(
+ this.#clearKnownCircuits.bind(this),
+ CLEAR_TIMEOUT
+ );
}
/**
- * Removes the observers added in the initialization.
+ * Removes the observers added in the initialization and stops the cleaning
+ * interval.
*/
uninit() {
Services.prefs.removeObserver(NON_TOR_PROXY_PREF, this);
Services.obs.removeObserver(this, NEW_IDENTITY_TOPIC);
+ Services.obs.removeObserver(this, lazy.TorMonitorTopics.StreamSucceeded);
+ clearInterval(this.#cleanupIntervalId);
+ this.#cleanupIntervalId = null;
+ this.clearIsolation();
}
enable() {
@@ -89,52 +175,52 @@ class TorDomainIsolatorImpl {
}
/**
- * Return the credentials to use as username and password for the SOCKS proxy,
- * given a certain domain and userContextId. Optionally, create them.
+ * Get the last circuit used in a certain browser.
+ * The returned data is created when the circuit is first seen, therefore it
+ * could be stale (i.e., the circuit might not be available anymore).
*
- * @param {string} firstPartyDomain The first party domain associated to the requests
- * @param {string} userContextId The context ID associated to the request
- * @param {bool} create Whether to create the nonce, if it is not available
- * @returns {object|null} Either the credential, or null if we do not have them and create is
- * false.
+ * @param {MozBrowser} browser The browser to get data for
+ * @param {string} domain The first party domain we want to get the circuit
+ * for
+ * @param {number} userContextId The user context domain we want to get the
+ * circuit for
+ * @returns {NodeData[]} The node data, or an empty array if we do not have
+ * data for the requested key.
*/
- getSocksProxyCredentials(firstPartyDomain, userContextId, create = false) {
- if (!this.#noncesForDomains.has(firstPartyDomain)) {
- if (!create) {
- return null;
- }
- const nonce = this.#nonce();
- logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`);
- this.#noncesForDomains.set(firstPartyDomain, nonce);
+ getCircuit(browser, domain, userContextId) {
+ const username = this.#makeUsername(domain, userContextId);
+ const circuits = this.#browsers.get(browser.browserId)?.get(username);
+ // This is the only place where circuit data can go out, so the only place
+ // where it makes a difference to check whether the pending circuit is still
+ // pending, or it has actually got data.
+ const pending = this.#knownCircuits.get(circuits?.pending);
+ if (pending?.length) {
+ circuits.current = circuits.pending;
+ circuits.pending = null;
+ return pending;
}
- if (!this.#noncesForUserContextId.has(userContextId)) {
- if (!create) {
- return null;
- }
- const nonce = this.#nonce();
- logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`);
- this.#noncesForUserContextId.set(userContextId, nonce);
- }
- return {
- username: this.#makeUsername(firstPartyDomain, userContextId),
- password:
- this.#noncesForDomains.get(firstPartyDomain) +
- this.#noncesForUserContextId.get(userContextId),
- };
+ // TODO: At this point we already know if we expect a circuit change for
+ // this key: (circuit?.pending && !pending). However, we do not consume this
+ // data yet in the frontend, so do not send it for now.
+ return this.#knownCircuits.get(circuits?.current) ?? [];
}
/**
* Create a new nonce for the FP domain of the selected browser and reload the
* tab with a new circuit.
*
- * @param {object} browser Should be the gBrowser from the context of the
- * caller
+ * @param {object} globalBrowser Should be the gBrowser from the context of
+ * the caller
*/
- newCircuitForBrowser(browser) {
- const firstPartyDomain = getDomainForBrowser(browser.selectedBrowser);
+ newCircuitForBrowser(globalBrowser) {
+ const browser = globalBrowser.selectedBrowser;
+ const firstPartyDomain = getDomainForBrowser(browser);
this.#newCircuitForDomain(firstPartyDomain);
- // TODO: How to properly handle the user context? Should we use
- // (domain, userContextId) pairs, instead of concatenating nonces?
+ const { username, password } = this.#getSocksProxyCredentials(
+ firstPartyDomain,
+ browser.contentPrincipal.originAttributes.userContextId
+ );
+ this.#trackBrowser(browser, username, password);
browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
}
@@ -147,12 +233,15 @@ class TorDomainIsolatorImpl {
// Per-domain and per contextId nonces are stored in maps, so simply clear
// them.
+ // Notice that the catch-all circuit is included in #noncesForDomains, so we
+ // are implicilty cleaning it. Should this change, we should change its
+ // nonce explicitly here.
this.#noncesForDomains.clear();
this.#noncesForUserContextId.clear();
+ this.#catchallDirtySince = Date.now();
- // Force a rotation on the next catch-all circuit use by setting the
- // creation time to the epoch.
- this.#catchallDirtySince = 0;
+ this.#knownCircuits.clear();
+ this.#browsers.clear();
}
async observe(subject, topic, data) {
@@ -173,55 +262,20 @@ class TorDomainIsolatorImpl {
logger.error("Could not send the newnym command", e);
// TODO: What UX to use here? See tor-browser#41708
}
+ } else if (topic === lazy.TorMonitorTopics.StreamSucceeded) {
+ const { username, password, circuit } = subject.wrappedJSObject;
+ this.#updateCircuit(username, password, circuit);
}
}
/**
- * Setup a filter that for every HTTPChannel, replaces the default SOCKS proxy
- * with one that authenticates to the SOCKS server (the tor client process)
- * with a username (the first party domain and userContextId) and a nonce
- * password.
- * Tor provides a separate circuit for each username+password combination.
+ * Setup a filter that for every HTTPChannel.
*/
#setupProxyFilter() {
- const filterFunction = (aChannel, aProxy) => {
- if (!this.#isolationEnabled) {
- return aProxy;
- }
- try {
- const channel = aChannel.QueryInterface(Ci.nsIChannel);
- let firstPartyDomain =
- channel.loadInfo.originAttributes.firstPartyDomain;
- const userContextId = channel.loadInfo.originAttributes.userContextId;
- if (firstPartyDomain === "") {
- firstPartyDomain = CATCHALL_DOMAIN;
- if (Date.now() - this.#catchallDirtySince > 1000 * 10 * 60) {
- logger.info(
- "tor catchall circuit has been dirty for over 10 minutes. Rotating."
- );
- this.#newCircuitForDomain(CATCHALL_DOMAIN);
- this.#catchallDirtySince = Date.now();
- }
- }
- const replacementProxy = this.#applySocksProxyCredentials(
- aProxy,
- firstPartyDomain,
- userContextId
- );
- logger.debug(
- `Requested ${channel.URI.spec} via ${replacementProxy.username}:${replacementProxy.password}`
- );
- return replacementProxy;
- } catch (e) {
- logger.error("Error while setting a new proxy", e);
- return null;
- }
- };
-
lazy.ProtocolProxyService.registerChannelFilter(
{
- applyFilter(aChannel, aProxy, aCallback) {
- aCallback.onProxyFilterResult(filterFunction(aChannel, aProxy));
+ applyFilter: (aChannel, aProxy, aCallback) => {
+ aCallback.onProxyFilterResult(this.#proxyFilter(aChannel, aProxy));
},
},
0
@@ -229,33 +283,96 @@ class TorDomainIsolatorImpl {
}
/**
- * Takes a proxyInfo object (originalProxy) and returns a new proxyInfo
- * object with the same properties, except the username is set to the
- * the domain and userContextId, and the password is a nonce.
+ * Replaces the default SOCKS proxy with one that authenticates to the SOCKS
+ * server (the tor client process) with a username (the first party domain and
+ * userContextId) and a nonce password.
+ * Tor provides a separate circuit for each username+password combination.
+ *
+ * @param {nsIChannel} aChannel The channel we are setting the proxy for
+ * @param {nsIProxyInfo} aProxy The original proxy
+ * @returns {nsIProxyInfo} The new proxy to use
*/
- #applySocksProxyCredentials(originalProxy, domain, userContextId) {
- const proxy = originalProxy.QueryInterface(Ci.nsIProxyInfo);
- const { username, password } = this.getSocksProxyCredentials(
- domain,
- userContextId,
- true
- );
- return lazy.ProtocolProxyService.newProxyInfoWithAuth(
- "socks",
- proxy.host,
- proxy.port,
- username,
- password,
- "", // aProxyAuthorizationHeader
- "", // aConnectionIsolationKey
- proxy.flags,
- proxy.failoverTimeout,
- proxy.failoverProxy
- );
+ #proxyFilter(aChannel, aProxy) {
+ if (!this.#isolationEnabled) {
+ return aProxy;
+ }
+ try {
+ const channel = aChannel.QueryInterface(Ci.nsIChannel);
+ let firstPartyDomain = channel.loadInfo.originAttributes.firstPartyDomain;
+ const userContextId = channel.loadInfo.originAttributes.userContextId;
+ if (!firstPartyDomain) {
+ firstPartyDomain = CATCHALL_DOMAIN;
+ if (Date.now() - this.#catchallDirtySince > CATCHALL_MAX_LIFETIME) {
+ logger.info(
+ "tor catchall circuit has reached its maximum lifetime. Rotating."
+ );
+ this.#newCircuitForDomain(CATCHALL_DOMAIN);
+ }
+ }
+ const { username, password } = this.#getSocksProxyCredentials(
+ firstPartyDomain,
+ userContextId
+ );
+ const browser = this.#getBrowserForChannel(channel);
+ if (browser) {
+ this.#trackBrowser(browser, username, password);
+ }
+ logger.debug(`Requested ${channel.URI.spec} via ${username}:${password}`);
+ const proxy = aProxy.QueryInterface(Ci.nsIProxyInfo);
+ return lazy.ProtocolProxyService.newProxyInfoWithAuth(
+ "socks",
+ proxy.host,
+ proxy.port,
+ username,
+ password,
+ "", // aProxyAuthorizationHeader
+ "", // aConnectionIsolationKey
+ proxy.flags,
+ proxy.failoverTimeout,
+ proxy.failoverProxy
+ );
+ } catch (e) {
+ logger.error("Error while setting a new proxy", e);
+ return null;
+ }
+ }
+
+ /**
+ * Return the credentials to use as username and password for the SOCKS proxy,
+ * given a certain domain and userContextId.
+ * A new random password will be created if not available yet.
+ *
+ * @param {string} firstPartyDomain The first party domain associated to the
+ * requests
+ * @param {number} userContextId The context ID associated to the request
+ * @returns {object} The credentials
+ */
+ #getSocksProxyCredentials(firstPartyDomain, userContextId) {
+ if (!this.#noncesForDomains.has(firstPartyDomain)) {
+ const nonce = this.#nonce();
+ logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`);
+ this.#noncesForDomains.set(firstPartyDomain, nonce);
+ }
+ if (!this.#noncesForUserContextId.has(userContextId)) {
+ const nonce = this.#nonce();
+ logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`);
+ this.#noncesForUserContextId.set(userContextId, nonce);
+ }
+ // TODO: How to properly handle the user-context? Should we use
+ // (domain, userContextId) pairs, instead of concatenating nonces?
+ return {
+ username: this.#makeUsername(firstPartyDomain, userContextId),
+ password:
+ this.#noncesForDomains.get(firstPartyDomain) +
+ this.#noncesForUserContextId.get(userContextId),
+ };
}
/**
* Combine the needed data into a username for the proxy.
+ *
+ * @param {string} domain The first-party domain associated to the request
+ * @param {integer} userContextId The userContextId associated to the request
*/
#makeUsername(domain, userContextId) {
if (!domain) {
@@ -264,12 +381,26 @@ class TorDomainIsolatorImpl {
return `${domain}:${userContextId}`;
}
+ /**
+ * Combine SOCKS username and password into a string to use as ID.
+ *
+ * @param {string} username The SOCKS username
+ * @param {string} password The SOCKS password
+ * @returns {CircuitId} A string that combines username and password and can
+ * be used for map lookups.
+ */
+ #credentialsToId(username, password) {
+ return `${username}|${password}`;
+ }
+
/**
* Generate a new 128 bit random tag.
*
* Strictly speaking both using a cryptographic entropy source and using 128
* bits of entropy for the tag are likely overkill, as correct behavior only
* depends on how unlikely it is for there to be a collision.
+ *
+ * @returns {string} The random nonce
*/
#nonce() {
return Array.from(crypto.getRandomValues(new Uint8Array(16)), byte =>
@@ -279,12 +410,18 @@ class TorDomainIsolatorImpl {
/**
* Re-generate the nonce for a certain domain.
+ *
+ * @param {string?} domain The first-party domain to re-create the nonce for.
+ * If empty or null, the catchall domain will be used.
*/
#newCircuitForDomain(domain) {
if (!domain) {
domain = CATCHALL_DOMAIN;
}
this.#noncesForDomains.set(domain, this.#nonce());
+ if (domain === CATCHALL_DOMAIN) {
+ this.#catchallDirtySince = Date.now();
+ }
logger.info(
`New domain isolation for ${domain}: ${this.#noncesForDomains.get(
domain
@@ -296,6 +433,8 @@ class TorDomainIsolatorImpl {
* Re-generate the nonce for a userContextId.
*
* Currently, this function is not hooked to anything.
+ *
+ * @param {integer} userContextId The userContextId to re-create the nonce for
*/
#newCircuitForUserContextId(userContextId) {
this.#noncesForUserContextId.set(userContextId, this.#nonce());
@@ -305,13 +444,182 @@ class TorDomainIsolatorImpl {
)}`
);
}
+
+ /**
+ * Try to extract a browser from a channel.
+ *
+ * @param {nsIChannel} channel The channel to extract the browser from
+ * @returns {MozBrowser?} The browser the channel is associated to
+ */
+ #getBrowserForChannel(channel) {
+ const browsers =
+ channel.loadInfo.browsingContext?.topChromeWindow?.gBrowser.browsers;
+ if (!browsers || !channel.loadInfo.browsingContext?.browserId) {
+ return null;
+ }
+ for (const browser of browsers) {
+ if (browser.browserId === channel.loadInfo.browsingContext.browserId) {
+ logger.debug(
+ "Matched browser with browserId",
+ channel.loadInfo.browsingContext.browserId
+ );
+ return browser;
+ }
+ }
+ // Expected to arrive here for example for the update checker.
+ // If we find a way to check that, we could raise the level to a warn.
+ logger.debug("Browser not matched", channel);
+ return null;
+ }
+
+ /**
+ * Associate the SOCKS credentials to a browser.
+ * If needed (the browser is associated for the first time, or it was already
+ * known but its credential changed), notify the related circuit display.
+ *
+ * @param {MozBrowser} browser The browser to track
+ * @param {string} username The SOCKS username
+ * @param {string} password The SOCKS password
+ */
+ #trackBrowser(browser, username, password) {
+ let browserCircuits = this.#browsers.get(browser.browserId);
+ if (!browserCircuits) {
+ browserCircuits = new Map();
+ this.#browsers.set(browser.browserId, browserCircuits);
+ }
+ const circuitIds = browserCircuits.get(username) ?? {};
+ const id = this.#credentialsToId(username, password);
+ if (circuitIds.current === id) {
+ // The circuit with these credentials was already built (we already knew
+ // its nodes, or we would not have promoted it to the current circuit).
+ // We do not need to do anything else, because we cannot detect a change
+ // of nodes here.
+ return;
+ }
+
+ logger.debug(
+ `Found new credentials ${username} ${password} for browser`,
+ browser
+ );
+ const circuit = this.#knownCircuits.get(id);
+ if (circuit?.length) {
+ circuitIds.current = id;
+ if (circuitIds.pending === id) {
+ circuitIds.pending = null;
+ }
+ browserCircuits.set(username, circuitIds);
+ // FIXME: We only notify the circuit display when we have a change that
+ // involves circuits whose nodes are known, for now. We need to resolve a
+ // few other techical problems (e.g., associate the circuit to the
+ // document?) and develop a UX with some animation to notify the circuit
+ // display more often.
+ // See tor-browser#41700 and tor-browser!699.
+ // In any case, notify the circuit display only after the internal map has
+ // been updated.
+ this.#notifyCircuitDisplay();
+ } else if (circuitIds.pending !== id) {
+ // We do not have node data, so we store that we might need to track this.
+ // Otherwise, when a circuit is ready, we do not know which browser was it
+ // used for.
+ circuitIds.pending = id;
+ browserCircuits.set(username, circuitIds);
+ }
+ }
+
+ /**
+ * Update a circuit, and notify the related circuit displays if it changed.
+ *
+ * This function is called when a certain stream has succeeded and so we can
+ * associate its SOCKS credential to the circuit it is using.
+ * We receive only the fingerprints of the circuit nodes, but they are enough
+ * to check if the circuit has changed. If it has, we also get the nodes'
+ * information through the control port.
+ *
+ * @param {string} username The SOCKS username
+ * @param {string} password The SOCKS password
+ * @param {NodeFingerprint[]} circuit The fingerprints of the nodes that
+ * compose the circuit
+ */
+ async #updateCircuit(username, password, circuit) {
+ const id = this.#credentialsToId(username, password);
+ let data = this.#knownCircuits.get(id) ?? [];
+ // Should we modify the lower layer to send a circuit identifier, instead?
+ if (
+ circuit.length === data.length &&
+ circuit.every((id, index) => id === data[index].fingerprint)
+ ) {
+ return;
+ }
+
+ data = await Promise.all(
+ circuit.map(fingerprint =>
+ lazy.TorProtocolService.getNodeInfo(fingerprint)
+ )
+ );
+ this.#knownCircuits.set(id, data);
+ // We know that something changed, but we cannot know if anyone is
+ // interested in this change. So, we have to notify all the possible
+ // consumers of the data in any case.
+ // Not being specific and let them check if they need to do something allows
+ // us to keep a simpler structure.
+ this.#notifyCircuitDisplay();
+ }
+
+ /**
+ * Broadcast a notification when a circuit changed, or a browser is changing
+ * circuit (which might happen also in case of navigation).
+ */
+ #notifyCircuitDisplay() {
+ Services.obs.notifyObservers(null, TOR_CIRCUIT_TOPIC);
+ }
+
+ /**
+ * Clear the known circuit information, when they are not needed anymore.
+ *
+ * We keep circuit data around for a while. We decouple it from the underlying
+ * tor circuit management in case the user clicks on the circuit display when
+ * circuit has long gone.
+ * However, data accumulate during a session. So, since we store all the
+ * browsers that used a circuit anyway, every now and then we check if we
+ * still know browsers using a certain circuits. If there are not, we forget
+ * about it.
+ *
+ * This function is run by an interval.
+ */
+ #clearKnownCircuits() {
+ logger.info("Running the circuit cleanup");
+ const windows = [];
+ const enumerator = Services.wm.getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements()) {
+ windows.push(enumerator.getNext());
+ }
+ const browsers = windows
+ .flatMap(win => win.gBrowser.browsers.map(b => b.browserId))
+ .filter(id => this.#browsers.has(id));
+ this.#browsers = new Map(browsers.map(id => [id, this.#browsers.get(id)]));
+ this.#knownCircuits = new Map(
+ Array.from(this.#browsers.values(), circuits =>
+ Array.from(circuits.values(), ids => {
+ const r = [];
+ const current = this.#knownCircuits.get(ids.current);
+ if (current) {
+ r.push([ids.current, current]);
+ }
+ const pending = this.#knownCircuits.get(ids.pending);
+ if (pending) {
+ r.push([ids.pending, pending]);
+ }
+ return r;
+ })
+ ).flat(2)
+ );
+ }
}
/**
* Get the first party domain for a certain browser.
*
- * @param browser The browser to get the FP-domain for.
- *
+ * @param {MozBrowser} browser The browser to get the FP-domain for.
* Please notice that it should be gBrowser.selectedBrowser, because
* browser.documentURI is the actual shown page, and might be an error page.
* In this case, we rely on currentURI, which for gBrowser is an alias of
@@ -358,6 +666,6 @@ function getDomainForBrowser(browser) {
return fpd;
}
-const TorDomainIsolator = new TorDomainIsolatorImpl();
+export const TorDomainIsolator = new TorDomainIsolatorImpl();
// Reduce global vars pollution
TorDomainIsolator.getDomainForBrowser = getDomainForBrowser;
=====================================
toolkit/components/tor-launcher/TorMonitorService.sys.mjs
=====================================
@@ -19,6 +19,10 @@ ChromeUtils.defineModuleGetter(
"resource://torbutton/modules/tor-control-port.js"
);
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
+});
+
const logger = new ConsoleAPI({
maxLogLevel: "warn",
maxLogLevelPref: "browser.tor_monitor_service.log_level",
@@ -37,12 +41,34 @@ const TorTopics = Object.freeze({
ProcessRestarted: "TorProcessRestarted",
});
+export const TorMonitorTopics = Object.freeze({
+ BridgeChanged: "TorBridgeChanged",
+ StreamSucceeded: "TorStreamSucceeded",
+});
+
const ControlConnTimings = Object.freeze({
initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect
maxRetryMS: 10000, // Retry at most every 10 seconds
timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start
});
+/**
+ * From control-spec.txt:
+ * CircuitID = 1*16 IDChar
+ * IDChar = ALPHA / DIGIT
+ * Currently, Tor only uses digits, but this may change.
+ *
+ * @typedef {string} CircuitID
+ */
+/**
+ * The fingerprint of a node.
+ * From control-spec.txt:
+ * Fingerprint = "$" 40*HEXDIG
+ * However, we do not keep the $ in our structures.
+ *
+ * @typedef {string} NodeFingerprint
+ */
+
/**
* This service monitors an existing Tor instance, or starts one, if needed, and
* then starts monitoring it.
@@ -52,7 +78,7 @@ const ControlConnTimings = Object.freeze({
*/
export const TorMonitorService = {
_connection: null,
- _eventsToMonitor: Object.freeze(["STATUS_CLIENT", "NOTICE", "WARN", "ERR"]),
+ _eventHandlers: {},
_torLog: [], // Array of objects with date, type, and msg properties.
_startTimeout: null,
@@ -64,6 +90,28 @@ export const TorMonitorService = {
_inited: false,
+ /**
+ * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node
+ * fingerprints.
+ *
+ * Theoretically, we could hook this map up to the new identity notification,
+ * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM
+ * signal does not affect them. So, we might end up using a circuit that was
+ * built before the new identity but not yet used. If we cleaned the map, we
+ * risked of not having the data about it.
+ *
+ * @type {Map<CircuitID, NodeFingerprint[]>}
+ */
+ _circuits: new Map(),
+ /**
+ * The last used bridge, or null if bridges are not in use or if it was not
+ * possible to detect the bridge. This needs the user to have specified bridge
+ * lines with fingerprints to work.
+ *
+ * @type {NodeFingerprint?}
+ */
+ _currentBridge: null,
+
// Public methods
// Starts Tor, if needed, and starts monitoring for events
@@ -72,14 +120,28 @@ export const TorMonitorService = {
return;
}
this._inited = true;
+
+ // We always liten to these events, because they are needed for the circuit
+ // display.
+ this._eventHandlers = new Map([
+ ["CIRC", this._processCircEvent.bind(this)],
+ ["STREAM", this._processStreamEvent.bind(this)],
+ ]);
+
if (this.ownsTorDaemon) {
+ // When we own the tor daemon, we listen to more events, that are used
+ // for about:torconnect or for showing the logs in the settings page.
+ this._eventHandlers.set("STATUS_CLIENT", (_eventType, lines) =>
+ this._processBootstrapStatus(lines[0], false)
+ );
+ this._eventHandlers.set("NOTICE", this._processLog.bind(this));
+ this._eventHandlers.set("WARN", this._processLog.bind(this));
+ this._eventHandlers.set("ERR", this._processLog.bind(this));
this._controlTor();
} else {
- logger.info(
- "Not starting the event monitor, as we do not own the Tor daemon."
- );
+ this._startEventMonitor();
}
- logger.debug("TorMonitorService initialized");
+ logger.info("TorMonitorService initialized");
},
// Closes the connection that monitors for events.
@@ -153,6 +215,18 @@ export const TorMonitorService = {
return !!this._connection;
},
+ /**
+ * Return the data about the current bridge, if any, or null.
+ * We can detect bridge only when the configured bridge lines include the
+ * fingerprints.
+ *
+ * @returns {NodeData?} The node information, or null if the first node
+ * is not a bridge, or no circuit has been opened, yet.
+ */
+ get currentBridge() {
+ return this._currentBridge;
+ },
+
// Private methods
async _startProcess() {
@@ -272,7 +346,7 @@ export const TorMonitorService = {
// TODO: optionally monitor INFO and DEBUG log messages.
let reply = await conn.sendCommand(
- "SETEVENTS " + this._eventsToMonitor.join(" ")
+ "SETEVENTS " + Array.from(this._eventHandlers.keys()).join(" ")
);
reply = TorParsers.parseCommandResponse(reply);
if (!TorParsers.commandSucceeded(reply)) {
@@ -281,14 +355,10 @@ export const TorMonitorService = {
return false;
}
- // FIXME: At the moment it is not possible to start the event monitor
- // when we do start the tor process. So, does it make sense to keep this
- // control?
if (this._torProcess) {
this._torProcess.connectionWorked();
}
-
- if (!TorLauncherUtil.shouldOnlyConfigureTor) {
+ if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
try {
await this._takeTorOwnership(conn);
} catch (e) {
@@ -297,7 +367,31 @@ export const TorMonitorService = {
}
this._connection = conn;
- this._waitForEventData();
+
+ for (const [type, callback] of this._eventHandlers.entries()) {
+ this._monitorEvent(type, callback);
+ }
+
+ // Populate the circuit map already, in case we are connecting to an
+ // external tor daemon.
+ try {
+ const reply = await this._connection.sendCommand(
+ "GETINFO circuit-status"
+ );
+ const lines = reply.split(/\r?\n/);
+ if (lines.shift() === "250+circuit-status=") {
+ for (const line of lines) {
+ if (line === ".") {
+ break;
+ }
+ // _processCircEvent processes only one line at a time
+ this._processCircEvent("CIRC", [line]);
+ }
+ }
+ } catch (e) {
+ logger.warn("Could not populate the initial circuit map", e);
+ }
+
return true;
},
@@ -318,65 +412,49 @@ export const TorMonitorService = {
}
},
- _waitForEventData() {
- if (!this._connection) {
- return;
- }
- logger.debug("Start watching events:", this._eventsToMonitor);
+ _monitorEvent(type, callback) {
+ logger.info(`Watching events of type ${type}.`);
let replyObj = {};
- for (const torEvent of this._eventsToMonitor) {
- this._connection.watchEvent(
- torEvent,
- null,
- line => {
- if (!line) {
- return;
- }
- logger.debug("Event response: ", line);
- const isComplete = TorParsers.parseReplyLine(line, replyObj);
- if (isComplete) {
- this._processEventReply(replyObj);
- replyObj = {};
- }
- },
- true
- );
- }
+ this._connection.watchEvent(
+ type,
+ null,
+ line => {
+ if (!line) {
+ return;
+ }
+ logger.debug("Event response: ", line);
+ const isComplete = TorParsers.parseReplyLine(line, replyObj);
+ if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) {
+ return;
+ }
+ const reply = replyObj;
+ replyObj = {};
+ if (reply.statusCode !== TorStatuses.EventNotification) {
+ logger.error("Unexpected event status code:", reply.statusCode);
+ return;
+ }
+ if (!reply.lineArray[0].startsWith(`${type} `)) {
+ logger.error("Wrong format for the first line:", reply.lineArray[0]);
+ return;
+ }
+ reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1);
+ try {
+ callback(type, reply.lineArray);
+ } catch (e) {
+ logger.error("Exception while handling an event", reply, e);
+ }
+ },
+ true
+ );
},
- _processEventReply(aReply) {
- if (aReply._parseError || !aReply.lineArray.length) {
- return;
- }
-
- if (aReply.statusCode !== TorStatuses.EventNotification) {
- logger.warn("Unexpected event status code:", aReply.statusCode);
- return;
- }
-
- // TODO: do we need to handle multiple lines?
- const s = aReply.lineArray[0];
- const idx = s.indexOf(" ");
- if (idx === -1) {
- return;
- }
- const eventType = s.substring(0, idx);
- const msg = s.substring(idx + 1).trim();
-
- if (eventType === "STATUS_CLIENT") {
- this._processBootstrapStatus(msg, false);
- return;
- } else if (!this._eventsToMonitor.includes(eventType)) {
- logger.debug(`Dropping unlistened event ${eventType}`);
- return;
- }
-
- if (eventType === "WARN" || eventType === "ERR") {
+ _processLog(type, lines) {
+ if (type === "WARN" || type === "ERR") {
// Notify so that Copy Log can be enabled.
Services.obs.notifyObservers(null, TorTopics.HasWarnOrErr);
}
- const now = new Date();
+ const date = new Date();
const maxEntries = Services.prefs.getIntPref(
"extensions.torlauncher.max_tor_log_entries",
1000
@@ -384,8 +462,10 @@ export const TorMonitorService = {
if (maxEntries > 0 && this._torLog.length >= maxEntries) {
this._torLog.splice(0, 1);
}
- this._torLog.push({ date: now, type: eventType, msg });
- const logString = `Tor ${eventType}: ${msg}`;
+
+ const msg = lines.join("\n");
+ this._torLog.push({ date, type, msg });
+ const logString = `Tor ${type}: ${msg}`;
logger.info(logString);
},
@@ -461,8 +541,108 @@ export const TorMonitorService = {
}
},
+ async _processCircEvent(_type, lines) {
+ const builtEvent =
+ /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
+ lines[0]
+ );
+ const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]);
+ if (builtEvent) {
+ const fp = /\$([0-9a-fA-F]{40})/g;
+ const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
+ g[1].toUpperCase()
+ );
+ this._circuits.set(builtEvent.groups.CircuitID, nodes);
+ // Ignore circuits of length 1, that are used, for example, to probe
+ // bridges. So, only store them, since we might see streams that use them,
+ // but then early-return.
+ if (nodes.length === 1) {
+ return;
+ }
+ // In some cases, we might already receive SOCKS credentials in the line.
+ // However, this might be a problem with onion services: we get also a
+ // 4-hop circuit that we likely do not want to show to the user,
+ // especially because it is used only temporarily, and it would need a
+ // technical explaination.
+ // this._checkCredentials(lines[0], nodes);
+ if (this._currentBridge?.fingerprint !== nodes[0]) {
+ const nodeInfo = await lazy.TorProtocolService.getNodeInfo(nodes[0]);
+ let notify = false;
+ if (nodeInfo?.bridgeType) {
+ logger.info(`Bridge changed to ${nodes[0]}`);
+ this._currentBridge = nodeInfo;
+ notify = true;
+ } else if (this._currentBridge) {
+ logger.info("Bridges disabled");
+ this._currentBridge = null;
+ notify = true;
+ }
+ if (notify) {
+ Services.obs.notifyObservers(
+ null,
+ TorMonitorTopics.BridgeChanged,
+ this._currentBridge
+ );
+ }
+ }
+ } else if (closedEvent) {
+ this._circuits.delete(closedEvent.groups.ID);
+ }
+ },
+
+ _processStreamEvent(_type, lines) {
+ // The first block is the stream ID, which we do not need at the moment.
+ const succeeedEvent =
+ /^[a-zA-Z0-9]{1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
+ lines[0]
+ );
+ if (!succeeedEvent) {
+ return;
+ }
+ const circuit = this._circuits.get(succeeedEvent.groups.CircuitID);
+ if (!circuit) {
+ logger.error(
+ "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers.",
+ lines[0]
+ );
+ return;
+ }
+ this._checkCredentials(lines[0], circuit);
+ },
+
+ /**
+ * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
+ * SOCKS_PASSWORD. In case, notify observers that we could associate a certain
+ * circuit to these credentials.
+ *
+ * @param {string} line The circ or stream line to check
+ * @param {NodeFingerprint[]} circuit The fingerprints of the nodes in the
+ * circuit.
+ */
+ _checkCredentials(line, circuit) {
+ const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
+ const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
+ if (!username || !password) {
+ return;
+ }
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: {
+ username: TorParsers.unescapeString(username[1]),
+ password: TorParsers.unescapeString(password[1]),
+ circuit,
+ },
+ },
+ TorMonitorTopics.StreamSucceeded
+ );
+ },
+
_shutDownEventMonitor() {
- this._connection?.close();
+ try {
+ this._connection?.close();
+ } catch (e) {
+ logger.error("Could not close the connection to the control port", e);
+ }
this._connection = null;
if (this._startTimeout !== null) {
clearTimeout(this._startTimeout);
=====================================
toolkit/components/tor-launcher/TorParsers.sys.mjs
=====================================
@@ -181,12 +181,12 @@ export const TorParsers = Object.freeze({
return aStr;
}
const escaped = aStr
- .replace("\\", "\\\\")
- .replace('"', '\\"')
- .replace("\n", "\\n")
- .replace("\r", "\\r")
- .replace("\t", "\\t")
- .replace(/[^\x20-\x7e]+/g, text => {
+ .replaceAll("\\", "\\\\")
+ .replaceAll('"', '\\"')
+ .replaceAll("\n", "\\n")
+ .replaceAll("\r", "\\r")
+ .replaceAll("\t", "\\t")
+ .replaceAll(/[^\x20-\x7e]+/g, text => {
const encoder = new TextEncoder();
return Array.from(
encoder.encode(text),
=====================================
toolkit/components/tor-launcher/TorProtocolService.sys.mjs
=====================================
@@ -40,6 +40,20 @@ const logger = new ConsoleAPI({
prefix: "TorProtocolService",
});
+/**
+ * Stores the data associated with a circuit node.
+ *
+ * @typedef NodeData
+ * @property {string} fingerprint The node fingerprint.
+ * @property {string[]} ipAddrs - The ip addresses associated with this node.
+ * @property {string?} bridgeType - The bridge type for this node, or "" if the
+ * node is a bridge but the type is unknown, or null if this is not a bridge
+ * node.
+ * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for
+ * the first ip address, or null if there is no region. This should also be a
+ * valid BCP47 Region subtag.
+ */
+
// Manage the connection to tor's control port, to update its settings and query
// other useful information.
//
@@ -188,6 +202,89 @@ export const TorProtocolService = {
return TorParsers.parseReply(cmd, keyword, response);
},
+ async getBridges() {
+ // Ideally, we would not need this function, because we should be the one
+ // setting them with TorSettings. However, TorSettings is not notified of
+ // change of settings. So, asking tor directly with the control connection
+ // is the most reliable way of getting the configured bridges, at the
+ // moment. Also, we are using this for the circuit display, which should
+ // work also when we are not configuring the tor daemon, but just using it.
+ return this._withConnection(conn => {
+ return conn.getConf("bridge");
+ });
+ },
+
+ /**
+ * Returns tha data about a relay or a bridge.
+ *
+ * @param {string} id The fingerprint of the node to get data about
+ * @returns {NodeData}
+ */
+ async getNodeInfo(id) {
+ return this._withConnection(async conn => {
+ const node = {
+ fingerprint: id,
+ ipAddrs: [],
+ bridgeType: null,
+ regionCode: null,
+ };
+ const bridge = (await conn.getConf("bridge"))?.find(
+ foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase()
+ );
+ const addrRe = /^\[?([^\]]+)\]?:\d+$/;
+ if (bridge) {
+ node.bridgeType = bridge.type ?? "";
+ // Attempt to get an IP address from bridge address string.
+ const ip = bridge.address.match(addrRe)?.[1];
+ if (ip && !ip.startsWith("0.")) {
+ node.ipAddrs.push(ip);
+ }
+ } else {
+ // Either dealing with a relay, or a bridge whose fingerprint is not
+ // saved in torrc.
+ const info = await conn.getInfo(`ns/id/${id}`);
+ if (info.IP && !info.IP.startsWith("0.")) {
+ node.ipAddrs.push(info.IP);
+ }
+ const ip6 = info.IPv6?.match(addrRe)?.[1];
+ if (ip6) {
+ node.ipAddrs.push(ip6);
+ }
+ }
+ if (node.ipAddrs.length) {
+ // Get the country code for the node's IP address.
+ let regionCode;
+ try {
+ // Expect a 2-letter ISO3166-1 code, which should also be a valid
+ // BCP47 Region subtag.
+ regionCode = await conn.getInfo("ip-to-country/" + node.ipAddrs[0]);
+ } catch {}
+ if (regionCode && regionCode !== "??") {
+ node.regionCode = regionCode.toUpperCase();
+ }
+ }
+ return node;
+ });
+ },
+
+ async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) {
+ return this._withConnection(conn => {
+ return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent);
+ });
+ },
+
+ async onionAuthRemove(hsAddress) {
+ return this._withConnection(conn => {
+ return conn.onionAuthRemove(hsAddress);
+ });
+ },
+
+ async onionAuthViewKeys() {
+ return this._withConnection(conn => {
+ return conn.onionAuthViewKeys();
+ });
+ },
+
// TODO: transform the following 4 functions in getters. At the moment they
// are also used in torbutton.
@@ -630,6 +727,16 @@ export const TorProtocolService = {
}
},
+ async _withConnection(func) {
+ // TODO: Make more robust?
+ const conn = await this._getConnection();
+ try {
+ return await func(conn);
+ } finally {
+ this._returnConnection();
+ }
+ },
+
// If aConn is omitted, the cached connection is closed.
_closeConnection() {
if (this._controlConnection) {
=====================================
toolkit/components/tor-launcher/TorStartupService.sys.mjs
=====================================
@@ -3,6 +3,7 @@ const lazy = {};
// We will use the modules only when the profile is loaded, so prefer lazy
// loading
ChromeUtils.defineESModuleGetters(lazy, {
+ TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
TorMonitorService: "resource://gre/modules/TorMonitorService.sys.mjs",
TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
@@ -19,12 +20,6 @@ ChromeUtils.defineModuleGetter(
"resource:///modules/TorSettings.jsm"
);
-ChromeUtils.defineModuleGetter(
- lazy,
- "TorDomainIsolator",
- "resource://gre/modules/TorDomainIsolator.jsm"
-);
-
/* Browser observer topis */
const BrowserTopics = Object.freeze({
ProfileAfterChange: "profile-after-change",
=====================================
toolkit/components/tor-launcher/moz.build
=====================================
@@ -1,6 +1,6 @@
EXTRA_JS_MODULES += [
"TorBootstrapRequest.sys.mjs",
- "TorDomainIsolator.jsm",
+ "TorDomainIsolator.sys.mjs",
"TorLauncherUtil.sys.mjs",
"TorMonitorService.sys.mjs",
"TorParsers.sys.mjs",
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/bdda467fbaec13a0dc9ad40997d0261f7396f5da...1a8be7b1e2fd495282857ea7b17972ed8892d133
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/bdda467fbaec13a0dc9ad40997d0261f7396f5da...1a8be7b1e2fd495282857ea7b17972ed8892d133
You're receiving this email because of your account on gitlab.torproject.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tbb-commits/attachments/20230727/11259267/attachment-0001.htm>
More information about the tbb-commits
mailing list