Pier Angelo Vendrame pushed to branch tor-browser-148.0a1-16.0-2 at The Tor Project / Applications / Tor Browser
Commits:
-
b07229fd
by Pier Angelo Vendrame at 2026-03-02T17:49:55+01:00
-
b7ffbcb1
by Pier Angelo Vendrame at 2026-03-02T17:49:55+01:00
-
13a9f1cc
by Pier Angelo Vendrame at 2026-03-02T17:49:55+01:00
-
66986d27
by Pier Angelo Vendrame at 2026-03-02T17:49:56+01:00
-
ef0640a3
by Pier Angelo Vendrame at 2026-03-02T17:49:56+01:00
-
61cf191b
by Pier Angelo Vendrame at 2026-03-02T17:49:56+01:00
6 changed files:
- browser/components/torcircuit/content/torCircuitPanel.js
- mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
- mobile/shared/modules/geckoview/GeckoViewContent.sys.mjs
- toolkit/components/tor-launcher/TorControlPort.sys.mjs
- toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
- toolkit/components/tor-launcher/TorProvider.sys.mjs
Changes:
| ... | ... | @@ -293,11 +293,14 @@ var gTorCircuitPanel = { |
| 293 | 293 | _updateCurrentBrowser() {
|
| 294 | 294 | const browser = gBrowser.selectedBrowser;
|
| 295 | 295 | const domain = TorDomainIsolator.getDomainForBrowser(browser);
|
| 296 | - const nodes = TorDomainIsolator.getCircuit(
|
|
| 296 | + const circuits = TorDomainIsolator.getCircuits(
|
|
| 297 | 297 | browser,
|
| 298 | 298 | domain,
|
| 299 | 299 | browser.contentPrincipal.originAttributes.userContextId
|
| 300 | 300 | );
|
| 301 | + // TODO: Handle multiple circuits (for conflux). Only show the primary
|
|
| 302 | + // circuit until the UI for that is developed.
|
|
| 303 | + const nodes = circuits.length ? circuits[0] : [];
|
|
| 301 | 304 | // We choose the currentURI, which matches what is shown in the URL bar and
|
| 302 | 305 | // will match up with the domain.
|
| 303 | 306 | // In contrast, documentURI corresponds to the shown page. E.g. it could
|
| ... | ... | @@ -2760,8 +2760,8 @@ public class GeckoSession { |
| 2760 | 2760 | * @return The circuit information as a {@link GeckoResult} object.
|
| 2761 | 2761 | */
|
| 2762 | 2762 | @AnyThread
|
| 2763 | - public @NonNull GeckoResult<GeckoBundle> getTorCircuit() {
|
|
| 2764 | - return mEventDispatcher.queryBundle("GeckoView:GetTorCircuit");
|
|
| 2763 | + public @NonNull GeckoResult<GeckoBundle> getTorCircuits() {
|
|
| 2764 | + return mEventDispatcher.queryBundle("GeckoView:GetTorCircuits");
|
|
| 2765 | 2765 | }
|
| 2766 | 2766 | |
| 2767 | 2767 | /**
|
| ... | ... | @@ -297,8 +297,8 @@ export class GeckoViewContent extends GeckoViewModule { |
| 297 | 297 | case "GeckoView:HasCookieBannerRuleForBrowsingContextTree":
|
| 298 | 298 | this._hasCookieBannerRuleForBrowsingContextTree(aCallback);
|
| 299 | 299 | break;
|
| 300 | - case "GeckoView:GetTorCircuit":
|
|
| 301 | - this._getTorCircuit(aCallback);
|
|
| 300 | + case "GeckoView:GetTorCircuits":
|
|
| 301 | + this._getTorCircuits(aCallback);
|
|
| 302 | 302 | break;
|
| 303 | 303 | case "GeckoView:NewTorCircuit":
|
| 304 | 304 | this._newTorCircuit(aCallback);
|
| ... | ... | @@ -472,15 +472,15 @@ export class GeckoViewContent extends GeckoViewModule { |
| 472 | 472 | }
|
| 473 | 473 | }
|
| 474 | 474 | |
| 475 | - _getTorCircuit(aCallback) {
|
|
| 475 | + _getTorCircuits(aCallback) {
|
|
| 476 | 476 | if (this.browser && aCallback) {
|
| 477 | 477 | const domain = lazy.TorDomainIsolator.getDomainForBrowser(this.browser);
|
| 478 | - const nodes = lazy.TorDomainIsolator.getCircuit(
|
|
| 478 | + const circuits = lazy.TorDomainIsolator.getCircuits(
|
|
| 479 | 479 | this.browser,
|
| 480 | 480 | domain,
|
| 481 | 481 | this.browser.contentPrincipal.originAttributes.userContextId
|
| 482 | 482 | );
|
| 483 | - aCallback?.onSuccess({ domain, nodes });
|
|
| 483 | + aCallback?.onSuccess({ domain, circuits });
|
|
| 484 | 484 | } else {
|
| 485 | 485 | aCallback?.onSuccess(null);
|
| 486 | 486 | }
|
| ... | ... | @@ -248,16 +248,23 @@ class AsyncSocket { |
| 248 | 248 | */
|
| 249 | 249 | /**
|
| 250 | 250 | * The ID of a circuit.
|
| 251 | - * From control-spec.txt:
|
|
| 251 | + * From the control port specs:
|
|
| 252 | 252 | * CircuitID = 1*16 IDChar
|
| 253 | 253 | * IDChar = ALPHA / DIGIT
|
| 254 | 254 | * Currently, Tor only uses digits, but this may change.
|
| 255 | 255 | *
|
| 256 | 256 | * @typedef {string} CircuitID
|
| 257 | 257 | */
|
| 258 | +/**
|
|
| 259 | + * The ID to match paired conflux circuits.
|
|
| 260 | + * From the control port specs:
|
|
| 261 | + * ConfluxID = 32*HEXDIG
|
|
| 262 | + *
|
|
| 263 | + * @typedef {string} ConfluxID
|
|
| 264 | + */
|
|
| 258 | 265 | /**
|
| 259 | 266 | * The ID of a stream.
|
| 260 | - * From control-spec.txt:
|
|
| 267 | + * From the control port specs:
|
|
| 261 | 268 | * CircuitID = 1*16 IDChar
|
| 262 | 269 | * IDChar = ALPHA / DIGIT
|
| 263 | 270 | * Currently, Tor only uses digits, but this may change.
|
| ... | ... | @@ -266,7 +273,7 @@ class AsyncSocket { |
| 266 | 273 | */
|
| 267 | 274 | /**
|
| 268 | 275 | * The fingerprint of a node.
|
| 269 | - * From control-spec.txt:
|
|
| 276 | + * From the control port specs:
|
|
| 270 | 277 | * Fingerprint = "$" 40*HEXDIG
|
| 271 | 278 | * However, we do not keep the $ in our structures.
|
| 272 | 279 | *
|
| ... | ... | @@ -275,7 +282,10 @@ class AsyncSocket { |
| 275 | 282 | /**
|
| 276 | 283 | * @typedef {object} CircuitInfo
|
| 277 | 284 | * @property {CircuitID} id The ID of a circuit
|
| 278 | - * @property {NodeFingerprint[]} nodes List of node fingerprints
|
|
| 285 | + * @property {NodeFingerprint[]} nodes List of node fingerprints, ordered from
|
|
| 286 | + * guard/bridge to exit.
|
|
| 287 | + * @property {ConfluxID} [confluxId] The conflux ID, for associating conflux
|
|
| 288 | + * circuits.
|
|
| 279 | 289 | */
|
| 280 | 290 | /**
|
| 281 | 291 | * @typedef {object} Bridge
|
| ... | ... | @@ -823,8 +833,8 @@ export class TorController { |
| 823 | 833 | }
|
| 824 | 834 | const cmd = `GETCONF ${key}`;
|
| 825 | 835 | const reply = await this.#sendCommand(cmd);
|
| 826 | - // From control-spec.txt: a 'default' value semantically different from an
|
|
| 827 | - // empty string will not have an equal sign, just `250 $key`.
|
|
| 836 | + // From the control port specs: a 'default' value semantically different
|
|
| 837 | + // from an empty string will not have an equal sign, just `250 $key`.
|
|
| 828 | 838 | const defaultRe = new RegExp(`^250[-\\s]${key}$`, "gim");
|
| 829 | 839 | if (reply.match(defaultRe)) {
|
| 830 | 840 | return [];
|
| ... | ... | @@ -1149,10 +1159,7 @@ export class TorController { |
| 1149 | 1159 | data.groups.data
|
| 1150 | 1160 | );
|
| 1151 | 1161 | if (maybeCircuit) {
|
| 1152 | - this.#eventHandler.onCircuitBuilt(
|
|
| 1153 | - maybeCircuit.id,
|
|
| 1154 | - maybeCircuit.nodes
|
|
| 1155 | - );
|
|
| 1162 | + this.#eventHandler.onCircuitBuilt(maybeCircuit);
|
|
| 1156 | 1163 | } else if (closedEvent) {
|
| 1157 | 1164 | this.#eventHandler.onCircuitClosed(closedEvent.groups.ID);
|
| 1158 | 1165 | }
|
| ... | ... | @@ -1222,7 +1229,7 @@ export class TorController { |
| 1222 | 1229 | */
|
| 1223 | 1230 | #parseCircBuilt(line) {
|
| 1224 | 1231 | const builtEvent =
|
| 1225 | - /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?\$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
|
|
| 1232 | + /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?\$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)(?<details>.*)/.exec(
|
|
| 1226 | 1233 | line
|
| 1227 | 1234 | );
|
| 1228 | 1235 | if (!builtEvent) {
|
| ... | ... | @@ -1232,6 +1239,8 @@ export class TorController { |
| 1232 | 1239 | const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
|
| 1233 | 1240 | g[1].toUpperCase()
|
| 1234 | 1241 | );
|
| 1242 | + const circuit = { id: builtEvent.groups.ID, nodes };
|
|
| 1243 | + |
|
| 1235 | 1244 | // In some cases, we might already receive SOCKS credentials in the
|
| 1236 | 1245 | // line. However, this might be a problem with Onion services: we get
|
| 1237 | 1246 | // also a 4-hop circuit that we likely do not want to show to the
|
| ... | ... | @@ -1239,7 +1248,22 @@ export class TorController { |
| 1239 | 1248 | // need a technical explaination.
|
| 1240 | 1249 | // So we do not try to extract them for now. Otherwise, we could do
|
| 1241 | 1250 | // const credentials = this.#parseCredentials(line);
|
| 1242 | - return { id: builtEvent.groups.ID, nodes };
|
|
| 1251 | + |
|
| 1252 | + // NOTE: We use a greedy leading ".*" to skip over previous fields that
|
|
| 1253 | + // can contain arbitrary strings, like SOCKS_USERNAME and SOCKS_PASSWORD,
|
|
| 1254 | + // which allows them to contain " CONFLUX_ID=" within their values.
|
|
| 1255 | + // Although such a value is not expected from the usernames and passwords we
|
|
| 1256 | + // set in Tor Browser, it may be set by an external tor user.
|
|
| 1257 | + // NOTE: This assumes there is no other arbitrary string field after
|
|
| 1258 | + // CONFLUX_ID.
|
|
| 1259 | + const maybeConfluxId = builtEvent.groups.details.match(
|
|
| 1260 | + /.* CONFLUX_ID=([0-9a-fA-F]{32,})(?:$| )/
|
|
| 1261 | + );
|
|
| 1262 | + if (maybeConfluxId) {
|
|
| 1263 | + circuit.confluxId = maybeConfluxId[1];
|
|
| 1264 | + }
|
|
| 1265 | + |
|
| 1266 | + return circuit;
|
|
| 1243 | 1267 | }
|
| 1244 | 1268 | |
| 1245 | 1269 | /**
|
| ... | ... | @@ -1327,8 +1351,7 @@ export class TorController { |
| 1327 | 1351 | /**
|
| 1328 | 1352 | * @callback OnCircuitBuilt
|
| 1329 | 1353 | *
|
| 1330 | - * @param {CircuitID} id The id of the circuit that has been built
|
|
| 1331 | - * @param {NodeFingerprint[]} nodes The onion routers composing the circuit
|
|
| 1354 | + * @param {CircuitInfo} circuit The information about the circuit
|
|
| 1332 | 1355 | */
|
| 1333 | 1356 | /**
|
| 1334 | 1357 | * @callback OnCircuitClosed
|
| ... | ... | @@ -12,6 +12,7 @@ import { |
| 12 | 12 | const lazy = {};
|
| 13 | 13 | |
| 14 | 14 | ChromeUtils.defineESModuleGetters(lazy, {
|
| 15 | + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
|
|
| 15 | 16 | TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
|
| 16 | 17 | TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
|
| 17 | 18 | });
|
| ... | ... | @@ -51,26 +52,21 @@ const TOR_CIRCUIT_TOPIC = "TorCircuitChange"; |
| 51 | 52 | const CLEAR_TIMEOUT = 600_000;
|
| 52 | 53 | |
| 53 | 54 | /**
|
| 54 | - * @typedef {string} CircuitId A string that we use to identify a circuit.
|
|
| 55 | - * Currently, it is a string that combines SOCKS credentials, to make it easier
|
|
| 56 | - * to use as a map key.
|
|
| 57 | - * It is not related to Tor's CircuitIDs.
|
|
| 55 | + * @typedef {string} IsolationKey A string that we use to identify an isolation
|
|
| 56 | + * key. Currently, it is a string that combines SOCKS credentials.
|
|
| 57 | + * Each isolation key is used to identify a set of circuits.
|
|
| 58 | 58 | */
|
| 59 | 59 | /**
|
| 60 | 60 | * @typedef {number} BrowserId
|
| 61 | 61 | */
|
| 62 | -/**
|
|
| 63 | - * @typedef {NodeData[]} CircuitData The data about the nodes, ordered from
|
|
| 64 | - * guard (or bridge) to exit.
|
|
| 65 | - */
|
|
| 66 | 62 | /**
|
| 67 | 63 | * @typedef BrowserCircuits Circuits related to a certain combination of
|
| 68 | 64 | * isolators (first-party domain and user context ID, currently).
|
| 69 | - * @property {CircuitId} current The id of the last known circuit that has been
|
|
| 70 | - * used to fetch data for the isolated context.
|
|
| 71 | - * @property {CircuitId?} pending The id of the last used circuit for this
|
|
| 72 | - * isolation context. We might or might not know data about it, yet. But if we
|
|
| 73 | - * know it, we should move this id into current.
|
|
| 65 | + * @property {IsolationKey} current The last isolation key for which circuit
|
|
| 66 | + * information is known.
|
|
| 67 | + * @property {IsolationKey?} pending The last used isolation key.
|
|
| 68 | + * We might or might not know data about it, yet. But if we know it, we should
|
|
| 69 | + * move this key into current, and pending should be made null.
|
|
| 74 | 70 | */
|
| 75 | 71 | |
| 76 | 72 | /**
|
| ... | ... | @@ -112,9 +108,16 @@ class TorDomainIsolatorImpl { |
| 112 | 108 | #catchallDirtySince = Date.now();
|
| 113 | 109 | |
| 114 | 110 | /**
|
| 115 | - * A map that associates circuit ids to the circuit information.
|
|
| 111 | + * A map that associates an isolation context to its circuits.
|
|
| 116 | 112 | *
|
| 117 | - * @type {Map<CircuitId, CircuitData>}
|
|
| 113 | + * The circuits are represented with a multidimensional array.
|
|
| 114 | + * The outer layer contains an array for each circuit of the isolation context
|
|
| 115 | + * (when conflux is in use, a certain isolation context might use more than a
|
|
| 116 | + * circuit).
|
|
| 117 | + * The inner layer contains the data about a certain circuit, ordered from
|
|
| 118 | + * guard/bridge to exit.
|
|
| 119 | + *
|
|
| 120 | + * @type {Map<IsolationKey, NodeData[][]>}
|
|
| 118 | 121 | */
|
| 119 | 122 | #knownCircuits = new Map();
|
| 120 | 123 | |
| ... | ... | @@ -191,8 +194,11 @@ class TorDomainIsolatorImpl { |
| 191 | 194 | }
|
| 192 | 195 | |
| 193 | 196 | /**
|
| 194 | - * Get the last circuit used in a certain browser.
|
|
| 195 | - * The returned data is created when the circuit is first seen, therefore it
|
|
| 197 | + * Get the circuits being used for a certain browser. There may be multiple
|
|
| 198 | + * if conflux is being used.
|
|
| 199 | + *
|
|
| 200 | + * Note, this returns the last known circuits for the browser. In particular,
|
|
| 201 | + * the returned data is created when the circuit is first seen, therefore it
|
|
| 196 | 202 | * could be stale (i.e., the circuit might not be available anymore).
|
| 197 | 203 | *
|
| 198 | 204 | * @param {MozBrowser} browser The browser to get data for
|
| ... | ... | @@ -200,10 +206,12 @@ class TorDomainIsolatorImpl { |
| 200 | 206 | * for
|
| 201 | 207 | * @param {number} userContextId The user context domain we want to get the
|
| 202 | 208 | * circuit for
|
| 203 | - * @returns {NodeData[]} The node data, or an empty array if we do not have
|
|
| 204 | - * data for the requested key.
|
|
| 209 | + * @returns {NodeData[][]} An array of all the circuits being used for this
|
|
| 210 | + * context. Each circuit is represented by an array of nodes, ordered from
|
|
| 211 | + * the guard/bridge to the exit. If the context has no known circuits, then
|
|
| 212 | + * this will be an empty array.
|
|
| 205 | 213 | */
|
| 206 | - getCircuit(browser, domain, userContextId) {
|
|
| 214 | + getCircuits(browser, domain, userContextId) {
|
|
| 207 | 215 | const username = this.#makeUsername(domain, userContextId);
|
| 208 | 216 | const circuits = this.#browsers.get(browser.browserId)?.get(username);
|
| 209 | 217 | // This is the only place where circuit data can go out, so the only place
|
| ... | ... | @@ -278,8 +286,8 @@ class TorDomainIsolatorImpl { |
| 278 | 286 | // TODO: What UX to use here? See tor-browser#41708
|
| 279 | 287 | }
|
| 280 | 288 | } else if (topic === lazy.TorProviderTopics.CircuitCredentialsMatched) {
|
| 281 | - const { username, password, circuit } = subject.wrappedJSObject;
|
|
| 282 | - this.#updateCircuit(username, password, circuit);
|
|
| 289 | + const { username, password, circuits } = subject.wrappedJSObject;
|
|
| 290 | + this.#updateCircuits(username, password, circuits);
|
|
| 283 | 291 | }
|
| 284 | 292 | }
|
| 285 | 293 | |
| ... | ... | @@ -421,10 +429,10 @@ class TorDomainIsolatorImpl { |
| 421 | 429 | *
|
| 422 | 430 | * @param {string} username The SOCKS username
|
| 423 | 431 | * @param {string} password The SOCKS password
|
| 424 | - * @returns {CircuitId} A string that combines username and password and can
|
|
| 425 | - * be used for map lookups.
|
|
| 432 | + * @returns {IsolationKey} A string that combines username and password and
|
|
| 433 | + * can be used as a key for maps.
|
|
| 426 | 434 | */
|
| 427 | - #credentialsToId(username, password) {
|
|
| 435 | + #credentialsToKey(username, password) {
|
|
| 428 | 436 | return `${username}|${password}`;
|
| 429 | 437 | }
|
| 430 | 438 | |
| ... | ... | @@ -540,7 +548,7 @@ class TorDomainIsolatorImpl { |
| 540 | 548 | this.#browsers.set(browser.browserId, browserCircuits);
|
| 541 | 549 | }
|
| 542 | 550 | const circuitIds = browserCircuits.get(username) ?? {};
|
| 543 | - const id = this.#credentialsToId(username, password);
|
|
| 551 | + const id = this.#credentialsToKey(username, password);
|
|
| 544 | 552 | if (circuitIds.current === id) {
|
| 545 | 553 | // The circuit with these credentials was already built (we already knew
|
| 546 | 554 | // its nodes, or we would not have promoted it to the current circuit).
|
| ... | ... | @@ -580,36 +588,25 @@ class TorDomainIsolatorImpl { |
| 580 | 588 | }
|
| 581 | 589 | |
| 582 | 590 | /**
|
| 583 | - * Update a circuit, and notify the related circuit displays if it changed.
|
|
| 591 | + * Update the circuits associated to a certain isolation context, and notify
|
|
| 592 | + * the related circuit displays if they changed.
|
|
| 584 | 593 | *
|
| 585 | 594 | * This function is called when a certain stream has succeeded and so we can
|
| 586 | - * associate its SOCKS credential to the circuit it is using.
|
|
| 587 | - * We receive only the fingerprints of the circuit nodes, but they are enough
|
|
| 588 | - * to check if the circuit has changed. If it has, we also get the nodes'
|
|
| 589 | - * information through the control port.
|
|
| 595 | + * associate its SOCKS credentials to the circuit it is using.
|
|
| 590 | 596 | *
|
| 591 | 597 | * @param {string} username The SOCKS username
|
| 592 | 598 | * @param {string} password The SOCKS password
|
| 593 | - * @param {NodeFingerprint[]} circuit The fingerprints of the nodes that
|
|
| 594 | - * compose the circuit
|
|
| 599 | + * @param {NodeData[][]} circuits The circuits being used for this isolation
|
|
| 600 | + * context. Each is represented by an array of nodes.
|
|
| 595 | 601 | */
|
| 596 | - async #updateCircuit(username, password, circuit) {
|
|
| 597 | - const id = this.#credentialsToId(username, password);
|
|
| 598 | - let data = this.#knownCircuits.get(id) ?? [];
|
|
| 599 | - // Should we modify the lower layer to send a circuit identifier, instead?
|
|
| 600 | - if (
|
|
| 601 | - circuit.length === data.length &&
|
|
| 602 | - circuit.every((fp, index) => fp === data[index].fingerprint)
|
|
| 603 | - ) {
|
|
| 602 | + async #updateCircuits(username, password, circuits) {
|
|
| 603 | + const key = this.#credentialsToKey(username, password);
|
|
| 604 | + let current = this.#knownCircuits.get(key);
|
|
| 605 | + if (lazy.ObjectUtils.deepEqual(current, circuits)) {
|
|
| 604 | 606 | return;
|
| 605 | 607 | }
|
| 606 | - |
|
| 607 | - const provider = await lazy.TorProviderBuilder.build();
|
|
| 608 | - data = await Promise.all(
|
|
| 609 | - circuit.map(fingerprint => provider.getNodeInfo(fingerprint))
|
|
| 610 | - );
|
|
| 611 | - logger.debug(`Updating circuit ${id}`, data);
|
|
| 612 | - this.#knownCircuits.set(id, data);
|
|
| 608 | + logger.info(`Updating circuits for ${key}`, circuits);
|
|
| 609 | + this.#knownCircuits.set(key, circuits);
|
|
| 613 | 610 | // We know that something changed, but we cannot know if anyone is
|
| 614 | 611 | // interested in this change. So, we have to notify all the possible
|
| 615 | 612 | // consumers of the data in any case.
|
| ... | ... | @@ -154,9 +154,24 @@ export class TorProvider { |
| 154 | 154 | * built before the new identity but not yet used. If we cleaned the map, we
|
| 155 | 155 | * risked of not having the data about it.
|
| 156 | 156 | *
|
| 157 | - * @type {Map<CircuitID, Promise<NodeFingerprint[]>>}
|
|
| 157 | + * @type {Map<CircuitID, CircuitInfo>}
|
|
| 158 | 158 | */
|
| 159 | 159 | #circuits = new Map();
|
| 160 | + |
|
| 161 | + /**
|
|
| 162 | + * Cache with node information.
|
|
| 163 | + *
|
|
| 164 | + * As a matter of fact, the circuit display backend continuously ask for
|
|
| 165 | + * information about the same nodes (e.g., the guards/bridges, and the exit
|
|
| 166 | + * for conflux circuits).
|
|
| 167 | + * Therefore, we can keep a cache of them to avoid a few control port lookups.
|
|
| 168 | + * And since it is likely we will get asked information about all nodes that
|
|
| 169 | + * appear in circuits, we can build this cache proactively.
|
|
| 170 | + *
|
|
| 171 | + * @type {Map<NodeFingerprint, Promise<NodeData>>}
|
|
| 172 | + */
|
|
| 173 | + #nodeInfo = new Map();
|
|
| 174 | + |
|
| 160 | 175 | /**
|
| 161 | 176 | * The last used bridge, or null if bridges are not in use or if it was not
|
| 162 | 177 | * possible to detect the bridge. This needs the user to have specified bridge
|
| ... | ... | @@ -457,50 +472,6 @@ export class TorProvider { |
| 457 | 472 | );
|
| 458 | 473 | }
|
| 459 | 474 | |
| 460 | - /**
|
|
| 461 | - * Returns tha data about a relay or a bridge.
|
|
| 462 | - *
|
|
| 463 | - * @param {string} id The fingerprint of the node to get data about
|
|
| 464 | - * @returns {Promise<NodeData>}
|
|
| 465 | - */
|
|
| 466 | - async getNodeInfo(id) {
|
|
| 467 | - const node = {
|
|
| 468 | - fingerprint: id,
|
|
| 469 | - ipAddrs: [],
|
|
| 470 | - bridgeType: null,
|
|
| 471 | - regionCode: null,
|
|
| 472 | - };
|
|
| 473 | - const bridge = (await this.#controller.getBridges())?.find(
|
|
| 474 | - foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase()
|
|
| 475 | - );
|
|
| 476 | - if (bridge) {
|
|
| 477 | - node.bridgeType = bridge.transport ?? "";
|
|
| 478 | - // Attempt to get an IP address from bridge address string.
|
|
| 479 | - const ip = bridge.addr.match(/^\[?([^\]]+)\]?:\d+$/)?.[1];
|
|
| 480 | - if (ip && !ip.startsWith("0.")) {
|
|
| 481 | - node.ipAddrs.push(ip);
|
|
| 482 | - }
|
|
| 483 | - } else {
|
|
| 484 | - node.ipAddrs = await this.#controller.getNodeAddresses(id);
|
|
| 485 | - }
|
|
| 486 | - // tor-browser#43116, tor-browser-build#41224: on Android, we do not ship
|
|
| 487 | - // the GeoIP databases to save some space. So skip it for now.
|
|
| 488 | - if (node.ipAddrs.length && !TorLauncherUtil.isAndroid) {
|
|
| 489 | - // Get the country code for the node's IP address.
|
|
| 490 | - try {
|
|
| 491 | - // Expect a 2-letter ISO3166-1 code, which should also be a valid
|
|
| 492 | - // BCP47 Region subtag.
|
|
| 493 | - const regionCode = await this.#controller.getIPCountry(node.ipAddrs[0]);
|
|
| 494 | - if (regionCode && regionCode !== "??") {
|
|
| 495 | - node.regionCode = regionCode.toUpperCase();
|
|
| 496 | - }
|
|
| 497 | - } catch (e) {
|
|
| 498 | - logger.warn(`Cannot get a country for IP ${node.ipAddrs[0]}`, e);
|
|
| 499 | - }
|
|
| 500 | - }
|
|
| 501 | - return node;
|
|
| 502 | - }
|
|
| 503 | - |
|
| 504 | 475 | /**
|
| 505 | 476 | * Add a private key to the Tor configuration.
|
| 506 | 477 | *
|
| ... | ... | @@ -936,14 +907,76 @@ export class TorProvider { |
| 936 | 907 | return crypto.getRandomValues(new Uint8Array(kPasswordLen));
|
| 937 | 908 | }
|
| 938 | 909 | |
| 910 | + // Circuit handling.
|
|
| 911 | + |
|
| 939 | 912 | /**
|
| 940 | 913 | * Ask Tor the circuits it already knows to populate our circuit map with the
|
| 941 | 914 | * circuits that were already open before we started listening for events.
|
| 942 | 915 | */
|
| 943 | 916 | async #fetchCircuits() {
|
| 944 | - for (const { id, nodes } of await this.#controller.getCircuits()) {
|
|
| 945 | - this.onCircuitBuilt(id, nodes);
|
|
| 917 | + for (const circuit of await this.#controller.getCircuits()) {
|
|
| 918 | + this.onCircuitBuilt(circuit);
|
|
| 919 | + }
|
|
| 920 | + }
|
|
| 921 | + |
|
| 922 | + /**
|
|
| 923 | + * Returns tha data about a relay or a bridge.
|
|
| 924 | + *
|
|
| 925 | + * @param {string} id The fingerprint of the node to get data about
|
|
| 926 | + * @returns {Promise<NodeData>}
|
|
| 927 | + */
|
|
| 928 | + #getNodeInfo(id) {
|
|
| 929 | + // This is an async method, so it will not insert the result, but a promise.
|
|
| 930 | + // However, this is what we want.
|
|
| 931 | + const info = this.#nodeInfo.getOrInsertComputed(id, async () => {
|
|
| 932 | + const node = {
|
|
| 933 | + fingerprint: id,
|
|
| 934 | + ipAddrs: [],
|
|
| 935 | + bridgeType: null,
|
|
| 936 | + regionCode: null,
|
|
| 937 | + };
|
|
| 938 | + |
|
| 939 | + const bridge = (await this.#controller.getBridges())?.find(
|
|
| 940 | + foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase()
|
|
| 941 | + );
|
|
| 942 | + if (bridge) {
|
|
| 943 | + node.bridgeType = bridge.transport ?? "";
|
|
| 944 | + // Attempt to get an IP address from bridge address string.
|
|
| 945 | + const ip = bridge.addr.match(/^\[?([^\]]+)\]?:\d+$/)?.[1];
|
|
| 946 | + if (ip && !ip.startsWith("0.")) {
|
|
| 947 | + node.ipAddrs.push(ip);
|
|
| 948 | + }
|
|
| 949 | + } else {
|
|
| 950 | + node.ipAddrs = await this.#controller.getNodeAddresses(id);
|
|
| 951 | + }
|
|
| 952 | + |
|
| 953 | + // tor-browser#43116, tor-browser-build#41224: on Android, we do not ship
|
|
| 954 | + // the GeoIP databases to save some space. So skip it for now.
|
|
| 955 | + if (node.ipAddrs.length && !TorLauncherUtil.isAndroid) {
|
|
| 956 | + // Get the country code for the node's IP address.
|
|
| 957 | + try {
|
|
| 958 | + // Expect a 2-letter ISO3166-1 code, which should also be a valid
|
|
| 959 | + // BCP47 Region subtag.
|
|
| 960 | + const regionCode = await this.#controller.getIPCountry(
|
|
| 961 | + node.ipAddrs[0]
|
|
| 962 | + );
|
|
| 963 | + if (regionCode && regionCode !== "??") {
|
|
| 964 | + node.regionCode = regionCode.toUpperCase();
|
|
| 965 | + }
|
|
| 966 | + } catch (e) {
|
|
| 967 | + logger.warn(`Cannot get a country for IP ${node.ipAddrs[0]}`, e);
|
|
| 968 | + }
|
|
| 969 | + }
|
|
| 970 | + return node;
|
|
| 971 | + });
|
|
| 972 | + |
|
| 973 | + const MAX_NODES = 300;
|
|
| 974 | + while (this.#nodeInfo.size > MAX_NODES) {
|
|
| 975 | + const oldestKey = this.#nodeInfo.keys().next().value;
|
|
| 976 | + this.#nodeInfo.delete(oldestKey);
|
|
| 946 | 977 | }
|
| 978 | + |
|
| 979 | + return info;
|
|
| 947 | 980 | }
|
| 948 | 981 | |
| 949 | 982 | // Notification handlers
|
| ... | ... | @@ -1046,24 +1079,43 @@ export class TorProvider { |
| 1046 | 1079 | * If a change of bridge is detected (including a change from bridge to a
|
| 1047 | 1080 | * normal guard), a notification is broadcast.
|
| 1048 | 1081 | *
|
| 1049 | - * @param {CircuitID} id The circuit ID
|
|
| 1050 | - * @param {NodeFingerprint[]} nodes The nodes that compose the circuit
|
|
| 1082 | + * @param {CircuitInfo} circuit The information about the circuit
|
|
| 1051 | 1083 | */
|
| 1052 | - async onCircuitBuilt(id, nodes) {
|
|
| 1053 | - this.#circuits.set(id, nodes);
|
|
| 1054 | - logger.debug(`Built tor circuit ${id}`, nodes);
|
|
| 1084 | + onCircuitBuilt(circuit) {
|
|
| 1085 | + logger.debug(`Built tor circuit ${circuit.id}`, circuit);
|
|
| 1086 | + |
|
| 1055 | 1087 | // Ignore circuits of length 1, that are used, for example, to probe
|
| 1056 | 1088 | // bridges. So, only store them, since we might see streams that use them,
|
| 1057 | 1089 | // but then early-return.
|
| 1058 | - if (nodes.length === 1) {
|
|
| 1090 | + if (circuit.nodes.length === 1) {
|
|
| 1059 | 1091 | return;
|
| 1060 | 1092 | }
|
| 1061 | 1093 | |
| 1062 | - if (this.#currentBridge?.fingerprint !== nodes[0]) {
|
|
| 1063 | - const nodeInfo = await this.getNodeInfo(nodes[0]);
|
|
| 1094 | + this.#circuits.set(circuit.id, circuit);
|
|
| 1095 | + |
|
| 1096 | + for (const fingerprint of circuit.nodes) {
|
|
| 1097 | + // To make the pending onStreamSentConnect call for this circuit faster,
|
|
| 1098 | + // we pre-fetch the node data, which should be cached by the time it is
|
|
| 1099 | + // called. No need to await here.
|
|
| 1100 | + this.#getNodeInfo(fingerprint);
|
|
| 1101 | + }
|
|
| 1102 | + |
|
| 1103 | + this.#maybeBridgeChanged(circuit);
|
|
| 1104 | + }
|
|
| 1105 | + |
|
| 1106 | + /**
|
|
| 1107 | + * Broadcast a bridge change, if needed.
|
|
| 1108 | + *
|
|
| 1109 | + * @param {CircuitInfo} circuit The information about the circuit
|
|
| 1110 | + */
|
|
| 1111 | + #maybeBridgeChanged(circuit) {
|
|
| 1112 | + if (this.#currentBridge?.fingerprint === circuit.nodes[0]) {
|
|
| 1113 | + return;
|
|
| 1114 | + }
|
|
| 1115 | + this.#getNodeInfo(circuit.nodes[0]).then(nodeInfo => {
|
|
| 1064 | 1116 | let notify = false;
|
| 1065 | 1117 | if (nodeInfo?.bridgeType) {
|
| 1066 | - logger.info(`Bridge changed to ${nodes[0]}`);
|
|
| 1118 | + logger.info(`Bridge changed to ${circuit.nodes[0]}`);
|
|
| 1067 | 1119 | this.#currentBridge = nodeInfo;
|
| 1068 | 1120 | notify = true;
|
| 1069 | 1121 | } else if (this.#currentBridge) {
|
| ... | ... | @@ -1074,7 +1126,7 @@ export class TorProvider { |
| 1074 | 1126 | if (notify) {
|
| 1075 | 1127 | Services.obs.notifyObservers(null, TorProviderTopics.BridgeChanged);
|
| 1076 | 1128 | }
|
| 1077 | - }
|
|
| 1129 | + });
|
|
| 1078 | 1130 | }
|
| 1079 | 1131 | |
| 1080 | 1132 | /**
|
| ... | ... | @@ -1091,48 +1143,60 @@ export class TorProvider { |
| 1091 | 1143 | /**
|
| 1092 | 1144 | * Handle a notification about a stream switching to the sentconnect status.
|
| 1093 | 1145 | *
|
| 1094 | - * @param {StreamID} streamId The ID of the stream that switched to the
|
|
| 1146 | + * @param {StreamID} _streamId The ID of the stream that switched to the
|
|
| 1095 | 1147 | * sentconnect status.
|
| 1096 | 1148 | * @param {CircuitID} circuitId The ID of the circuit used by the stream
|
| 1097 | 1149 | * @param {string} username The SOCKS username
|
| 1098 | 1150 | * @param {string} password The SOCKS password
|
| 1099 | 1151 | */
|
| 1100 | - async onStreamSentConnect(streamId, circuitId, username, password) {
|
|
| 1152 | + async onStreamSentConnect(_streamId, circuitId, username, password) {
|
|
| 1101 | 1153 | if (!username || !password) {
|
| 1102 | 1154 | return;
|
| 1103 | 1155 | }
|
| 1104 | 1156 | logger.debug("Stream sentconnect event", username, password, circuitId);
|
| 1105 | - let circuit = this.#circuits.get(circuitId);
|
|
| 1106 | - if (!circuit) {
|
|
| 1107 | - circuit = new Promise((resolve, reject) => {
|
|
| 1108 | - this.#controlConnection.getCircuits().then(circuits => {
|
|
| 1109 | - for (const { id, nodes } of circuits) {
|
|
| 1110 | - if (id === circuitId) {
|
|
| 1111 | - resolve(nodes);
|
|
| 1112 | - return;
|
|
| 1113 | - }
|
|
| 1114 | - // Opportunistically collect circuits, since we are iterating them.
|
|
| 1115 | - this.#circuits.set(id, nodes);
|
|
| 1116 | - }
|
|
| 1117 | - logger.error(
|
|
| 1118 | - `Seen a STREAM SENTCONNECT with circuit ${circuitId}, but Tor did not send information about it.`
|
|
| 1119 | - );
|
|
| 1120 | - reject();
|
|
| 1121 | - });
|
|
| 1122 | - });
|
|
| 1123 | - this.#circuits.set(circuitId, circuit);
|
|
| 1157 | + if (!this.#circuits.has(circuitId)) {
|
|
| 1158 | + // tor-browser#42132: When using onion-grater (e.g., in Tails), we might
|
|
| 1159 | + // not receive the CIRC BUILT event, as it is impossible to know whether
|
|
| 1160 | + // that circuit will be the browser's at that point. So, we will have to
|
|
| 1161 | + // poll circuits and wait for that to finish to be able to get the data.
|
|
| 1162 | + try {
|
|
| 1163 | + await this.#fetchCircuits();
|
|
| 1164 | + } catch {
|
|
| 1165 | + return;
|
|
| 1166 | + }
|
|
| 1124 | 1167 | }
|
| 1125 | - try {
|
|
| 1126 | - circuit = await circuit;
|
|
| 1127 | - } catch {
|
|
| 1168 | + |
|
| 1169 | + const primaryCircuit = this.#circuits.get(circuitId);
|
|
| 1170 | + if (!primaryCircuit) {
|
|
| 1171 | + logger.error(
|
|
| 1172 | + `Seen a STREAM SENTCONNECT with circuit ${circuitId}, but Tor did not send information about it.`
|
|
| 1173 | + );
|
|
| 1128 | 1174 | return;
|
| 1129 | 1175 | }
|
| 1176 | + |
|
| 1177 | + const circuitIds = [circuitId];
|
|
| 1178 | + if (primaryCircuit.confluxId) {
|
|
| 1179 | + circuitIds.push(
|
|
| 1180 | + ...this.#circuits
|
|
| 1181 | + .entries()
|
|
| 1182 | + .filter(
|
|
| 1183 | + ([id, circ]) =>
|
|
| 1184 | + circ.confluxId === primaryCircuit.confluxId && id != circuitId
|
|
| 1185 | + )
|
|
| 1186 | + .map(([id]) => id)
|
|
| 1187 | + );
|
|
| 1188 | + }
|
|
| 1189 | + const circuits = await Promise.all(
|
|
| 1190 | + circuitIds.map(id =>
|
|
| 1191 | + Promise.all(this.#circuits.get(id).nodes.map(n => this.#getNodeInfo(n)))
|
|
| 1192 | + )
|
|
| 1193 | + );
|
|
| 1130 | 1194 | Services.obs.notifyObservers(
|
| 1131 | 1195 | {
|
| 1132 | 1196 | wrappedJSObject: {
|
| 1133 | 1197 | username,
|
| 1134 | 1198 | password,
|
| 1135 | - circuit,
|
|
| 1199 | + circuits,
|
|
| 1136 | 1200 | },
|
| 1137 | 1201 | },
|
| 1138 | 1202 | TorProviderTopics.CircuitCredentialsMatched
|