tor-browser

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

commit b7ffbcb1c046ad1b39117b2d277323d00f0b657f
parent b07229fd3cfe4de67ab5c097cfd5759c843c08c0
Author: Pier Angelo Vendrame <pierov@torproject.org>
Date:   Tue, 17 Feb 2026 09:16:34 +0100

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.

Diffstat:
Mtoolkit/components/tor-launcher/TorControlPort.sys.mjs | 8++------
Mtoolkit/components/tor-launcher/TorProvider.sys.mjs | 228++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
2 files changed, 148 insertions(+), 88 deletions(-)

diff --git a/toolkit/components/tor-launcher/TorControlPort.sys.mjs b/toolkit/components/tor-launcher/TorControlPort.sys.mjs @@ -1159,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); } @@ -1354,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 diff --git a/toolkit/components/tor-launcher/TorProvider.sys.mjs b/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 @@ -458,50 +473,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. * * @param {string} address The address of the onion service @@ -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