Verified Commit 26152fa9 authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame 🎃
Browse files

fixup! Bug 40933: Add tor-launcher functionality

Bug 41844: Do not use a the control port directly.

Collect the bridge node for the about:preferences#connection page in
TorMonitorService.

Also, move parts of the circuit display to TorMonitorService and
TorProtocolService.
parent b2cd0ee8
Loading
Loading
Loading
Loading
+202 −19
Original line number Diff line number Diff line
@@ -19,6 +19,10 @@ ChromeUtils.defineModuleGetter(
  "resource://torbutton/modules/tor-control-port.js"
);

ChromeUtils.defineESModuleGetters(lazy, {
  TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
});

const logger = new ConsoleAPI({
  maxLogLevel: "warn",
  maxLogLevelPref: "browser.tor_monitor_service.log_level",
@@ -37,12 +41,34 @@ const TorTopics = Object.freeze({
  ProcessRestarted: "TorProcessRestarted",
});

export const TorMonitorTopics = Object.freeze({
  BridgeChanged: "TorBridgeChanged",
  StreamSucceeded: "TorStreamSucceeded",
});

const ControlConnTimings = Object.freeze({
  initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect
  maxRetryMS: 10000, // Retry at most every 10 seconds
  timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start
});

/**
 * From control-spec.txt:
 *   CircuitID = 1*16 IDChar
 *   IDChar = ALPHA / DIGIT
 *   Currently, Tor only uses digits, but this may change.
 *
 * @typedef {string} CircuitID
 */
/**
 * The fingerprint of a node.
 * From control-spec.txt:
 *   Fingerprint = "$" 40*HEXDIG
 * However, we do not keep the $ in our structures.
 *
 * @typedef {string} NodeFingerprint
 */

/**
 * This service monitors an existing Tor instance, or starts one, if needed, and
 * then starts monitoring it.
@@ -64,6 +90,28 @@ export const TorMonitorService = {

  _inited: false,

  /**
   * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node
   * fingerprints.
   *
   * Theoretically, we could hook this map up to the new identity notification,
   * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM
   * signal does not affect them. So, we might end up using a circuit that was
   * built before the new identity but not yet used. If we cleaned the map, we
   * risked of not having the data about it.
   *
   * @type {Map<CircuitID, NodeFingerprint[]>}
   */
  _circuits: new Map(),
  /**
   * The last used bridge, or null if bridges are not in use or if it was not
   * possible to detect the bridge. This needs the user to have specified bridge
   * lines with fingerprints to work.
   *
   * @type {NodeFingerprint?}
   */
  _currentBridge: null,

  // Public methods

  // Starts Tor, if needed, and starts monitoring for events
@@ -73,24 +121,27 @@ export const TorMonitorService = {
    }
    this._inited = true;

    // We always liten to these events, because they are needed for the circuit
    // display.
    this._eventHandlers = new Map([
      [
        "STATUS_CLIENT",
        (_eventType, lines) => this._processBootstrapStatus(lines[0], false),
      ],
      ["NOTICE", this._processLog.bind(this)],
      ["WARN", this._processLog.bind(this)],
      ["ERR", this._processLog.bind(this)],
      ["CIRC", this._processCircEvent.bind(this)],
      ["STREAM", this._processStreamEvent.bind(this)],
    ]);

    if (this.ownsTorDaemon) {
      // When we own the tor daemon, we listen to more events, that are used
      // for about:torconnect or for showing the logs in the settings page.
      this._eventHandlers.set("STATUS_CLIENT", (_eventType, lines) =>
        this._processBootstrapStatus(lines[0], false)
      );
      this._eventHandlers.set("NOTICE", this._processLog.bind(this));
      this._eventHandlers.set("WARN", this._processLog.bind(this));
      this._eventHandlers.set("ERR", this._processLog.bind(this));
      this._controlTor();
    } else {
      logger.info(
        "Not starting the event monitor, as we do not own the Tor daemon."
      );
      this._startEventMonitor();
    }
    logger.debug("TorMonitorService initialized");
    logger.info("TorMonitorService initialized");
  },

  // Closes the connection that monitors for events.
@@ -164,6 +215,18 @@ export const TorMonitorService = {
    return !!this._connection;
  },

  /**
   * Return the data about the current bridge, if any, or null.
   * We can detect bridge only when the configured bridge lines include the
   * fingerprints.
   *
   * @returns {NodeData?} The node information, or null if the first node
   * is not a bridge, or no circuit has been opened, yet.
   */
  get currentBridge() {
    return this._currentBridge;
  },

  // Private methods

  async _startProcess() {
@@ -292,14 +355,10 @@ export const TorMonitorService = {
      return false;
    }

    // FIXME: At the moment it is not possible to start the event monitor
    // when we do start the tor process. So, does it make sense to keep this
    // control?
    if (this._torProcess) {
      this._torProcess.connectionWorked();
    }

    if (!TorLauncherUtil.shouldOnlyConfigureTor) {
    if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
      try {
        await this._takeTorOwnership(conn);
      } catch (e) {
@@ -313,6 +372,26 @@ export const TorMonitorService = {
      this._monitorEvent(type, callback);
    }

    // Populate the circuit map already, in case we are connecting to an
    // external tor daemon.
    try {
      const reply = await this._connection.sendCommand(
        "GETINFO circuit-status"
      );
      const lines = reply.split(/\r?\n/);
      if (lines.shift() === "250+circuit-status=") {
        for (const line of lines) {
          if (line === ".") {
            break;
          }
          // _processCircEvent processes only one line at a time
          this._processCircEvent("CIRC", [line]);
        }
      }
    } catch (e) {
      logger.warn("Could not populate the initial circuit map", e);
    }

    return true;
  },

@@ -334,7 +413,7 @@ export const TorMonitorService = {
  },

  _monitorEvent(type, callback) {
    logger.debug(`Watching events of type ${type}.`);
    logger.info(`Watching events of type ${type}.`);
    let replyObj = {};
    this._connection.watchEvent(
      type,
@@ -359,7 +438,11 @@ export const TorMonitorService = {
          return;
        }
        reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1);
        try {
          callback(type, reply.lineArray);
        } catch (e) {
          logger.error("Exception while handling an event", reply, e);
        }
      },
      true
    );
@@ -458,8 +541,108 @@ export const TorMonitorService = {
    }
  },

  async _processCircEvent(_type, lines) {
    const builtEvent =
      /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
        lines[0]
      );
    const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]);
    if (builtEvent) {
      const fp = /\$([0-9a-fA-F]{40})/g;
      const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
        g[1].toUpperCase()
      );
      this._circuits.set(builtEvent.groups.CircuitID, nodes);
      // Ignore circuits of length 1, that are used, for example, to probe
      // bridges. So, only store them, since we might see streams that use them,
      // but then early-return.
      if (nodes.length === 1) {
        return;
      }
      // In some cases, we might already receive SOCKS credentials in the line.
      // However, this might be a problem with onion services: we get also a
      // 4-hop circuit that we likely do not want to show to the user,
      // especially because it is used only temporarily, and it would need a
      // technical explaination.
      // this._checkCredentials(lines[0], nodes);
      if (this._currentBridge?.fingerprint !== nodes[0]) {
        const nodeInfo = await lazy.TorProtocolService.getNodeInfo(nodes[0]);
        let notify = false;
        if (nodeInfo?.bridgeType) {
          logger.info(`Bridge changed to ${nodes[0]}`);
          this._currentBridge = nodeInfo;
          notify = true;
        } else if (this._currentBridge) {
          logger.info("Bridges disabled");
          this._currentBridge = null;
          notify = true;
        }
        if (notify) {
          Services.obs.notifyObservers(
            null,
            TorMonitorTopics.BridgeChanged,
            this._currentBridge
          );
        }
      }
    } else if (closedEvent) {
      this._circuits.delete(closedEvent.groups.ID);
    }
  },

  _processStreamEvent(_type, lines) {
    // The first block is the stream ID, which we do not need at the moment.
    const succeeedEvent =
      /^[a-zA-Z0-9]{1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
        lines[0]
      );
    if (!succeeedEvent) {
      return;
    }
    const circuit = this._circuits.get(succeeedEvent.groups.CircuitID);
    if (!circuit) {
      logger.error(
        "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers.",
        lines[0]
      );
      return;
    }
    this._checkCredentials(lines[0], circuit);
  },

  /**
   * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
   * SOCKS_PASSWORD. In case, notify observers that we could associate a certain
   * circuit to these credentials.
   *
   * @param {string} line The circ or stream line to check
   * @param {NodeFingerprint[]} circuit The fingerprints of the nodes in the
   * circuit.
   */
  _checkCredentials(line, circuit) {
    const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
    const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
    if (!username || !password) {
      return;
    }
    Services.obs.notifyObservers(
      {
        wrappedJSObject: {
          username: TorParsers.unescapeString(username[1]),
          password: TorParsers.unescapeString(password[1]),
          circuit,
        },
      },
      TorMonitorTopics.StreamSucceeded
    );
  },

  _shutDownEventMonitor() {
    try {
      this._connection?.close();
    } catch (e) {
      logger.error("Could not close the connection to the control port", e);
    }
    this._connection = null;
    if (this._startTimeout !== null) {
      clearTimeout(this._startTimeout);
+79 −0
Original line number Diff line number Diff line
@@ -40,6 +40,20 @@ const logger = new ConsoleAPI({
  prefix: "TorProtocolService",
});

/**
 * Stores the data associated with a circuit node.
 *
 * @typedef NodeData
 * @property {string} fingerprint The node fingerprint.
 * @property {string[]} ipAddrs - The ip addresses associated with this node.
 * @property {string?} bridgeType - The bridge type for this node, or "" if the
 *   node is a bridge but the type is unknown, or null if this is not a bridge
 *   node.
 * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for
 *   the first ip address, or null if there is no region. This should also be a
 *   valid BCP47 Region subtag.
 */

// Manage the connection to tor's control port, to update its settings and query
// other useful information.
//
@@ -188,6 +202,71 @@ export const TorProtocolService = {
    return TorParsers.parseReply(cmd, keyword, response);
  },

  async getBridges() {
    // Ideally, we would not need this function, because we should be the one
    // setting them with TorSettings. However, TorSettings is not notified of
    // change of settings. So, asking tor directly with the control connection
    // is the most reliable way of getting the configured bridges, at the
    // moment. Also, we are using this for the circuit display, which should
    // work also when we are not configuring the tor daemon, but just using it.
    return this._withConnection(conn => {
      return conn.getConf("bridge");
    });
  },

  /**
   * Returns tha data about a relay or a bridge.
   *
   * @param {string} id The fingerprint of the node to get data about
   * @returns {NodeData}
   */
  async getNodeInfo(id) {
    return this._withConnection(async conn => {
      const node = {
        fingerprint: id,
        ipAddrs: [],
        bridgeType: null,
        regionCode: null,
      };
      const bridge = (await conn.getConf("bridge"))?.find(
        foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase()
      );
      const addrRe = /^\[?([^\]]+)\]?:\d+$/;
      if (bridge) {
        node.bridgeType = bridge.type ?? "";
        // Attempt to get an IP address from bridge address string.
        const ip = bridge.address.match(addrRe)?.[1];
        if (ip && !ip.startsWith("0.")) {
          node.ipAddrs.push(ip);
        }
      } else {
        // Either dealing with a relay, or a bridge whose fingerprint is not
        // saved in torrc.
        const info = await conn.getInfo(`ns/id/${id}`);
        if (info.IP && !info.IP.startsWith("0.")) {
          node.ipAddrs.push(info.IP);
        }
        const ip6 = info.IPv6?.match(addrRe)?.[1];
        if (ip6) {
          node.ipAddrs.push(ip6);
        }
      }
      if (node.ipAddrs.length) {
        // Get the country code for the node's IP address.
        let regionCode;
        try {
          // Expect a 2-letter ISO3166-1 code, which should also be a valid
          // BCP47 Region subtag.
          regionCode = await conn.getInfo("ip-to-country/" + node.ipAddrs[0]);
        } catch {}
        if (regionCode && regionCode !== "??") {
          node.regionCode = regionCode.toUpperCase();
        }
      }
      return node;
    });
  },

  async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) {
    return this._withConnection(conn => {
      return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent);