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 fixup! TB 40933: Add tor-launcher functionality TB 44635: Gather conflux information on circuits. - - - - - b7ffbcb1 by Pier Angelo Vendrame at 2026-03-02T17:49:55+01:00 fixup! TB 40933: Add tor-launcher functionality TB 44635: Gather conflux information on circuits. Proactively gather data about the circuits in TorProvider, and send the complete information about a circuit, not only its node fingerprints. Also, gather conflux sets, and send both conflux circuits to the circuit display backend. - - - - - 13a9f1cc by Pier Angelo Vendrame at 2026-03-02T17:49:55+01:00 fixup! TB 3455: Add DomainIsolator, for isolating circuit by domain. TB 44635: Gather conflux information on circuits. Reword CircuitID to IsolationKey, for better clarity. - - - - - 66986d27 by Pier Angelo Vendrame at 2026-03-02T17:49:56+01:00 fixup! TB 3455: Add DomainIsolator, for isolating circuit by domain. TB 44635: Gather conflux information on circuits. Relay information collection now happens at the tor provider level. So, adapt the code of TorDomainIsolator to take the data alrady prepared. - - - - - ef0640a3 by Pier Angelo Vendrame at 2026-03-02T17:49:56+01:00 fixup! TB 41600: Add a tor circuit display panel. TB 44635: Gather conflux information on circuits. Consume only the first circuit on the circuit display. - - - - - 61cf191b by Pier Angelo Vendrame at 2026-03-02T17:49:56+01:00 fixup! TB 42247: Android helpers for the TorProvider TB 44635: Gather conflux information on circuits. Update the getCircuit name to getCircuits. - - - - - 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: ===================================== browser/components/torcircuit/content/torCircuitPanel.js ===================================== @@ -293,11 +293,14 @@ var gTorCircuitPanel = { _updateCurrentBrowser() { const browser = gBrowser.selectedBrowser; const domain = TorDomainIsolator.getDomainForBrowser(browser); - const nodes = TorDomainIsolator.getCircuit( + const circuits = TorDomainIsolator.getCircuits( browser, domain, browser.contentPrincipal.originAttributes.userContextId ); + // TODO: Handle multiple circuits (for conflux). Only show the primary + // circuit until the UI for that is developed. + const nodes = circuits.length ? circuits[0] : []; // 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 ===================================== mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java ===================================== @@ -2760,8 +2760,8 @@ public class GeckoSession { * @return The circuit information as a {@link GeckoResult} object. */ @AnyThread - public @NonNull GeckoResult<GeckoBundle> getTorCircuit() { - return mEventDispatcher.queryBundle("GeckoView:GetTorCircuit"); + public @NonNull GeckoResult<GeckoBundle> getTorCircuits() { + return mEventDispatcher.queryBundle("GeckoView:GetTorCircuits"); } /** ===================================== mobile/shared/modules/geckoview/GeckoViewContent.sys.mjs ===================================== @@ -297,8 +297,8 @@ export class GeckoViewContent extends GeckoViewModule { case "GeckoView:HasCookieBannerRuleForBrowsingContextTree": this._hasCookieBannerRuleForBrowsingContextTree(aCallback); break; - case "GeckoView:GetTorCircuit": - this._getTorCircuit(aCallback); + case "GeckoView:GetTorCircuits": + this._getTorCircuits(aCallback); break; case "GeckoView:NewTorCircuit": this._newTorCircuit(aCallback); @@ -472,15 +472,15 @@ export class GeckoViewContent extends GeckoViewModule { } } - _getTorCircuit(aCallback) { + _getTorCircuits(aCallback) { if (this.browser && aCallback) { const domain = lazy.TorDomainIsolator.getDomainForBrowser(this.browser); - const nodes = lazy.TorDomainIsolator.getCircuit( + const circuits = lazy.TorDomainIsolator.getCircuits( this.browser, domain, this.browser.contentPrincipal.originAttributes.userContextId ); - aCallback?.onSuccess({ domain, nodes }); + aCallback?.onSuccess({ domain, circuits }); } else { aCallback?.onSuccess(null); } ===================================== toolkit/components/tor-launcher/TorControlPort.sys.mjs ===================================== @@ -248,16 +248,23 @@ class AsyncSocket { */ /** * The ID of a circuit. - * From control-spec.txt: + * From the control port specs: * CircuitID = 1*16 IDChar * IDChar = ALPHA / DIGIT * Currently, Tor only uses digits, but this may change. * * @typedef {string} CircuitID */ +/** + * The ID to match paired conflux circuits. + * From the control port specs: + * ConfluxID = 32*HEXDIG + * + * @typedef {string} ConfluxID + */ /** * The ID of a stream. - * From control-spec.txt: + * From the control port specs: * CircuitID = 1*16 IDChar * IDChar = ALPHA / DIGIT * Currently, Tor only uses digits, but this may change. @@ -266,7 +273,7 @@ class AsyncSocket { */ /** * The fingerprint of a node. - * From control-spec.txt: + * From the control port specs: * Fingerprint = "$" 40*HEXDIG * However, we do not keep the $ in our structures. * @@ -275,7 +282,10 @@ class AsyncSocket { /** * @typedef {object} CircuitInfo * @property {CircuitID} id The ID of a circuit - * @property {NodeFingerprint[]} nodes List of node fingerprints + * @property {NodeFingerprint[]} nodes List of node fingerprints, ordered from + * guard/bridge to exit. + * @property {ConfluxID} [confluxId] The conflux ID, for associating conflux + * circuits. */ /** * @typedef {object} Bridge @@ -823,8 +833,8 @@ export class TorController { } const cmd = `GETCONF ${key}`; const reply = await this.#sendCommand(cmd); - // From control-spec.txt: a 'default' value semantically different from an - // empty string will not have an equal sign, just `250 $key`. + // From the control port specs: a 'default' value semantically different + // from an empty string will not have an equal sign, just `250 $key`. const defaultRe = new RegExp(`^250[-\\s]${key}$`, "gim"); if (reply.match(defaultRe)) { return []; @@ -1149,10 +1159,7 @@ export class TorController { data.groups.data ); if (maybeCircuit) { - this.#eventHandler.onCircuitBuilt( - maybeCircuit.id, - maybeCircuit.nodes - ); + this.#eventHandler.onCircuitBuilt(maybeCircuit); } else if (closedEvent) { this.#eventHandler.onCircuitClosed(closedEvent.groups.ID); } @@ -1222,7 +1229,7 @@ export class TorController { */ #parseCircBuilt(line) { const builtEvent = - /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?\$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)/.exec( + /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?\$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)(?<details>.*)/.exec( line ); if (!builtEvent) { @@ -1232,6 +1239,8 @@ export class TorController { const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g => g[1].toUpperCase() ); + const circuit = { id: builtEvent.groups.ID, nodes }; + // 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 @@ -1239,7 +1248,22 @@ export class TorController { // need a technical explaination. // So we do not try to extract them for now. Otherwise, we could do // const credentials = this.#parseCredentials(line); - return { id: builtEvent.groups.ID, nodes }; + + // NOTE: We use a greedy leading ".*" to skip over previous fields that + // can contain arbitrary strings, like SOCKS_USERNAME and SOCKS_PASSWORD, + // which allows them to contain " CONFLUX_ID=" within their values. + // Although such a value is not expected from the usernames and passwords we + // set in Tor Browser, it may be set by an external tor user. + // NOTE: This assumes there is no other arbitrary string field after + // CONFLUX_ID. + const maybeConfluxId = builtEvent.groups.details.match( + /.* CONFLUX_ID=([0-9a-fA-F]{32,})(?:$| )/ + ); + if (maybeConfluxId) { + circuit.confluxId = maybeConfluxId[1]; + } + + return circuit; } /** @@ -1327,8 +1351,7 @@ export class TorController { /** * @callback OnCircuitBuilt * - * @param {CircuitID} id The id of the circuit that has been built - * @param {NodeFingerprint[]} nodes The onion routers composing the circuit + * @param {CircuitInfo} circuit The information about the circuit */ /** * @callback OnCircuitClosed ===================================== toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs ===================================== @@ -12,6 +12,7 @@ import { const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs", }); @@ -51,26 +52,21 @@ const TOR_CIRCUIT_TOPIC = "TorCircuitChange"; 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 {string} IsolationKey A string that we use to identify an isolation + * key. Currently, it is a string that combines SOCKS credentials. + * Each isolation key is used to identify a set of circuits. */ /** * @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. + * @property {IsolationKey} current The last isolation key for which circuit + * information is known. + * @property {IsolationKey?} pending The last used isolation key. + * We might or might not know data about it, yet. But if we know it, we should + * move this key into current, and pending should be made null. */ /** @@ -112,9 +108,16 @@ class TorDomainIsolatorImpl { #catchallDirtySince = Date.now(); /** - * A map that associates circuit ids to the circuit information. + * A map that associates an isolation context to its circuits. * - * @type {Map<CircuitId, CircuitData>} + * The circuits are represented with a multidimensional array. + * The outer layer contains an array for each circuit of the isolation context + * (when conflux is in use, a certain isolation context might use more than a + * circuit). + * The inner layer contains the data about a certain circuit, ordered from + * guard/bridge to exit. + * + * @type {Map<IsolationKey, NodeData[][]>} */ #knownCircuits = new Map(); @@ -191,8 +194,11 @@ class TorDomainIsolatorImpl { } /** - * Get the last circuit used in a certain browser. - * The returned data is created when the circuit is first seen, therefore it + * Get the circuits being used for a certain browser. There may be multiple + * if conflux is being used. + * + * Note, this returns the last known circuits for the browser. In particular, + * 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 {MozBrowser} browser The browser to get data for @@ -200,10 +206,12 @@ class TorDomainIsolatorImpl { * 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. + * @returns {NodeData[][]} An array of all the circuits being used for this + * context. Each circuit is represented by an array of nodes, ordered from + * the guard/bridge to the exit. If the context has no known circuits, then + * this will be an empty array. */ - getCircuit(browser, domain, userContextId) { + getCircuits(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 @@ -278,8 +286,8 @@ class TorDomainIsolatorImpl { // TODO: What UX to use here? See tor-browser#41708 } } else if (topic === lazy.TorProviderTopics.CircuitCredentialsMatched) { - const { username, password, circuit } = subject.wrappedJSObject; - this.#updateCircuit(username, password, circuit); + const { username, password, circuits } = subject.wrappedJSObject; + this.#updateCircuits(username, password, circuits); } } @@ -421,10 +429,10 @@ class TorDomainIsolatorImpl { * * @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. + * @returns {IsolationKey} A string that combines username and password and + * can be used as a key for maps. */ - #credentialsToId(username, password) { + #credentialsToKey(username, password) { return `${username}|${password}`; } @@ -540,7 +548,7 @@ class TorDomainIsolatorImpl { this.#browsers.set(browser.browserId, browserCircuits); } const circuitIds = browserCircuits.get(username) ?? {}; - const id = this.#credentialsToId(username, password); + const id = this.#credentialsToKey(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). @@ -580,36 +588,25 @@ class TorDomainIsolatorImpl { } /** - * Update a circuit, and notify the related circuit displays if it changed. + * Update the circuits associated to a certain isolation context, and notify + * the related circuit displays if they 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. + * associate its SOCKS credentials to the circuit it is using. * * @param {string} username The SOCKS username * @param {string} password The SOCKS password - * @param {NodeFingerprint[]} circuit The fingerprints of the nodes that - * compose the circuit + * @param {NodeData[][]} circuits The circuits being used for this isolation + * context. Each is represented by an array of nodes. */ - 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((fp, index) => fp === data[index].fingerprint) - ) { + async #updateCircuits(username, password, circuits) { + const key = this.#credentialsToKey(username, password); + let current = this.#knownCircuits.get(key); + if (lazy.ObjectUtils.deepEqual(current, circuits)) { return; } - - const provider = await lazy.TorProviderBuilder.build(); - data = await Promise.all( - circuit.map(fingerprint => provider.getNodeInfo(fingerprint)) - ); - logger.debug(`Updating circuit ${id}`, data); - this.#knownCircuits.set(id, data); + logger.info(`Updating circuits for ${key}`, circuits); + this.#knownCircuits.set(key, circuits); // 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. ===================================== toolkit/components/tor-launcher/TorProvider.sys.mjs ===================================== @@ -154,9 +154,24 @@ export class TorProvider { * 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, Promise<NodeFingerprint[]>>} + * @type {Map<CircuitID, CircuitInfo>} */ #circuits = new Map(); + + /** + * Cache with node information. + * + * As a matter of fact, the circuit display backend continuously ask for + * information about the same nodes (e.g., the guards/bridges, and the exit + * for conflux circuits). + * Therefore, we can keep a cache of them to avoid a few control port lookups. + * And since it is likely we will get asked information about all nodes that + * appear in circuits, we can build this cache proactively. + * + * @type {Map<NodeFingerprint, Promise<NodeData>>} + */ + #nodeInfo = 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 @@ -457,50 +472,6 @@ export class TorProvider { ); } - /** - * Returns tha data about a relay or a bridge. - * - * @param {string} id The fingerprint of the node to get data about - * @returns {Promise<NodeData>} - */ - async getNodeInfo(id) { - const node = { - fingerprint: id, - ipAddrs: [], - bridgeType: null, - regionCode: null, - }; - const bridge = (await this.#controller.getBridges())?.find( - foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase() - ); - if (bridge) { - node.bridgeType = bridge.transport ?? ""; - // Attempt to get an IP address from bridge address string. - const ip = bridge.addr.match(/^\[?([^\]]+)\]?:\d+$/)?.[1]; - if (ip && !ip.startsWith("0.")) { - node.ipAddrs.push(ip); - } - } else { - node.ipAddrs = await this.#controller.getNodeAddresses(id); - } - // tor-browser#43116, tor-browser-build#41224: on Android, we do not ship - // the GeoIP databases to save some space. So skip it for now. - if (node.ipAddrs.length && !TorLauncherUtil.isAndroid) { - // Get the country code for the node's IP address. - try { - // Expect a 2-letter ISO3166-1 code, which should also be a valid - // BCP47 Region subtag. - const regionCode = await this.#controller.getIPCountry(node.ipAddrs[0]); - if (regionCode && regionCode !== "??") { - node.regionCode = regionCode.toUpperCase(); - } - } catch (e) { - logger.warn(`Cannot get a country for IP ${node.ipAddrs[0]}`, e); - } - } - return node; - } - /** * Add a private key to the Tor configuration. * @@ -936,14 +907,76 @@ export class TorProvider { return crypto.getRandomValues(new Uint8Array(kPasswordLen)); } + // Circuit handling. + /** * Ask Tor the circuits it already knows to populate our circuit map with the * circuits that were already open before we started listening for events. */ async #fetchCircuits() { - for (const { id, nodes } of await this.#controller.getCircuits()) { - this.onCircuitBuilt(id, nodes); + for (const circuit of await this.#controller.getCircuits()) { + this.onCircuitBuilt(circuit); + } + } + + /** + * Returns tha data about a relay or a bridge. + * + * @param {string} id The fingerprint of the node to get data about + * @returns {Promise<NodeData>} + */ + #getNodeInfo(id) { + // This is an async method, so it will not insert the result, but a promise. + // However, this is what we want. + const info = this.#nodeInfo.getOrInsertComputed(id, async () => { + const node = { + fingerprint: id, + ipAddrs: [], + bridgeType: null, + regionCode: null, + }; + + const bridge = (await this.#controller.getBridges())?.find( + foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase() + ); + if (bridge) { + node.bridgeType = bridge.transport ?? ""; + // Attempt to get an IP address from bridge address string. + const ip = bridge.addr.match(/^\[?([^\]]+)\]?:\d+$/)?.[1]; + if (ip && !ip.startsWith("0.")) { + node.ipAddrs.push(ip); + } + } else { + node.ipAddrs = await this.#controller.getNodeAddresses(id); + } + + // tor-browser#43116, tor-browser-build#41224: on Android, we do not ship + // the GeoIP databases to save some space. So skip it for now. + if (node.ipAddrs.length && !TorLauncherUtil.isAndroid) { + // Get the country code for the node's IP address. + try { + // Expect a 2-letter ISO3166-1 code, which should also be a valid + // BCP47 Region subtag. + const regionCode = await this.#controller.getIPCountry( + node.ipAddrs[0] + ); + if (regionCode && regionCode !== "??") { + node.regionCode = regionCode.toUpperCase(); + } + } catch (e) { + logger.warn(`Cannot get a country for IP ${node.ipAddrs[0]}`, e); + } + } + return node; + }); + + const MAX_NODES = 300; + while (this.#nodeInfo.size > MAX_NODES) { + const oldestKey = this.#nodeInfo.keys().next().value; + this.#nodeInfo.delete(oldestKey); } + + return info; } // Notification handlers @@ -1046,24 +1079,43 @@ export class TorProvider { * If a change of bridge is detected (including a change from bridge to a * normal guard), a notification is broadcast. * - * @param {CircuitID} id The circuit ID - * @param {NodeFingerprint[]} nodes The nodes that compose the circuit + * @param {CircuitInfo} circuit The information about the circuit */ - async onCircuitBuilt(id, nodes) { - this.#circuits.set(id, nodes); - logger.debug(`Built tor circuit ${id}`, nodes); + onCircuitBuilt(circuit) { + logger.debug(`Built tor circuit ${circuit.id}`, circuit); + // 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) { + if (circuit.nodes.length === 1) { return; } - if (this.#currentBridge?.fingerprint !== nodes[0]) { - const nodeInfo = await this.getNodeInfo(nodes[0]); + this.#circuits.set(circuit.id, circuit); + + for (const fingerprint of circuit.nodes) { + // To make the pending onStreamSentConnect call for this circuit faster, + // we pre-fetch the node data, which should be cached by the time it is + // called. No need to await here. + this.#getNodeInfo(fingerprint); + } + + this.#maybeBridgeChanged(circuit); + } + + /** + * Broadcast a bridge change, if needed. + * + * @param {CircuitInfo} circuit The information about the circuit + */ + #maybeBridgeChanged(circuit) { + if (this.#currentBridge?.fingerprint === circuit.nodes[0]) { + return; + } + this.#getNodeInfo(circuit.nodes[0]).then(nodeInfo => { let notify = false; if (nodeInfo?.bridgeType) { - logger.info(`Bridge changed to ${nodes[0]}`); + logger.info(`Bridge changed to ${circuit.nodes[0]}`); this.#currentBridge = nodeInfo; notify = true; } else if (this.#currentBridge) { @@ -1074,7 +1126,7 @@ export class TorProvider { if (notify) { Services.obs.notifyObservers(null, TorProviderTopics.BridgeChanged); } - } + }); } /** @@ -1091,48 +1143,60 @@ export class TorProvider { /** * Handle a notification about a stream switching to the sentconnect status. * - * @param {StreamID} streamId The ID of the stream that switched to the + * @param {StreamID} _streamId The ID of the stream that switched to the * sentconnect status. * @param {CircuitID} circuitId The ID of the circuit used by the stream * @param {string} username The SOCKS username * @param {string} password The SOCKS password */ - async onStreamSentConnect(streamId, circuitId, username, password) { + async onStreamSentConnect(_streamId, circuitId, username, password) { if (!username || !password) { return; } logger.debug("Stream sentconnect event", username, password, circuitId); - let circuit = this.#circuits.get(circuitId); - if (!circuit) { - circuit = new Promise((resolve, reject) => { - this.#controlConnection.getCircuits().then(circuits => { - for (const { id, nodes } of circuits) { - if (id === circuitId) { - resolve(nodes); - return; - } - // Opportunistically collect circuits, since we are iterating them. - this.#circuits.set(id, nodes); - } - logger.error( - `Seen a STREAM SENTCONNECT with circuit ${circuitId}, but Tor did not send information about it.` - ); - reject(); - }); - }); - this.#circuits.set(circuitId, circuit); + if (!this.#circuits.has(circuitId)) { + // tor-browser#42132: When using onion-grater (e.g., in Tails), we might + // not receive the CIRC BUILT event, as it is impossible to know whether + // that circuit will be the browser's at that point. So, we will have to + // poll circuits and wait for that to finish to be able to get the data. + try { + await this.#fetchCircuits(); + } catch { + return; + } } - try { - circuit = await circuit; - } catch { + + const primaryCircuit = this.#circuits.get(circuitId); + if (!primaryCircuit) { + logger.error( + `Seen a STREAM SENTCONNECT with circuit ${circuitId}, but Tor did not send information about it.` + ); return; } + + const circuitIds = [circuitId]; + if (primaryCircuit.confluxId) { + circuitIds.push( + ...this.#circuits + .entries() + .filter( + ([id, circ]) => + circ.confluxId === primaryCircuit.confluxId && id != circuitId + ) + .map(([id]) => id) + ); + } + const circuits = await Promise.all( + circuitIds.map(id => + Promise.all(this.#circuits.get(id).nodes.map(n => this.#getNodeInfo(n))) + ) + ); Services.obs.notifyObservers( { wrappedJSObject: { username, password, - circuit, + circuits, }, }, TorProviderTopics.CircuitCredentialsMatched View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/3848baf... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/3848baf... You're receiving this email because of your account on gitlab.torproject.org.