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:
Changes:
browser/components/torcircuit/content/torCircuitPanel.js
| ... |
... |
@@ -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
|
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
| ... |
... |
@@ -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
|
/**
|
mobile/shared/modules/geckoview/GeckoViewContent.sys.mjs
| ... |
... |
@@ -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
|
}
|
toolkit/components/tor-launcher/TorControlPort.sys.mjs
| ... |
... |
@@ -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
|
toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
| ... |
... |
@@ -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.
|
toolkit/components/tor-launcher/TorProvider.sys.mjs
| ... |
... |
@@ -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
|
|