[tor-commits] [tor-browser] 68/72: Bug 27476: Implement about:torconnect captive portal within Tor Browser

gitolite role git at cupani.torproject.org
Wed Jun 1 02:59:28 UTC 2022


This is an automated email from the git hooks/post-receive script.

richard pushed a commit to branch tor-browser-91.10.0esr-11.0-1
in repository tor-browser.

commit 9c9d344d2aaf518f54c6bc5a6b191ff7b5ca92d3
Author: Richard Pospesel <richard at torproject.org>
AuthorDate: Wed Apr 28 23:09:34 2021 -0500

    Bug 27476: Implement about:torconnect captive portal within Tor Browser
    
    - implements new about:torconnect page as tor-launcher replacement
    - adds tor connection status to url bar and tweaks UX when not online
    - adds new torconnect component to browser
    - tor process management functionality remains implemented in tor-launcher through the TorProtocolService module
    - the onion pattern from about:tor migrated to an .inc.xhtml file now used by both about:tor and about:torconnect
    - various design tweaks and resusability fixes to onion pattern
    - adds warning/error box to about:preferences#tor when not connected to tor
    - explicitly allows about:torconnect URIs to ignore Resist Fingerprinting (RFP)
    - various tweaks to info-pages.inc.css for about:torconnect (also affects other firefox info pages)
---
 browser/actors/NetErrorParent.jsm                  |   8 +
 browser/base/content/browser-siteIdentity.js       |   5 +-
 browser/base/content/browser.js                    |  66 ++-
 browser/base/content/browser.xhtml                 |   2 +
 browser/base/content/certerror/aboutNetError.js    |  12 +-
 browser/base/content/navigator-toolbox.inc.xhtml   |   1 +
 browser/base/content/utilityOverlay.js             |   8 +
 browser/branding/alpha/content/jar.mn              |   2 +
 browser/branding/alpha/content/tor-styles.css      |  13 +
 browser/branding/nightly/content/jar.mn            |   1 +
 browser/branding/nightly/content/tor-styles.css    |  13 +
 browser/branding/official/content/jar.mn           |   1 +
 browser/branding/official/content/tor-styles.css   |  14 +
 browser/branding/tor-styles.inc.css                |  87 ++++
 browser/components/BrowserGlue.jsm                 |  37 +-
 browser/components/about/AboutRedirector.cpp       |   4 +
 browser/components/about/components.conf           |   1 +
 browser/components/moz.build                       |   1 +
 .../onionservices/HttpsEverywhereControl.jsm       |  17 +-
 browser/components/sessionstore/SessionStore.jsm   |   4 +
 browser/components/torconnect/TorConnectChild.jsm  |   9 +
 browser/components/torconnect/TorConnectParent.jsm | 144 ++++++
 .../torconnect/content/aboutTorConnect.css         | 180 ++++++++
 .../torconnect/content/aboutTorConnect.js          | 302 ++++++++++++
 .../torconnect/content/aboutTorConnect.xhtml       |  45 ++
 .../components/torconnect/content/onion-slash.svg  |   5 +
 browser/components/torconnect/content/onion.svg    |   4 +
 .../torconnect/content/torBootstrapUrlbar.js       |  93 ++++
 .../torconnect/content/torconnect-urlbar.css       |  57 +++
 .../torconnect/content/torconnect-urlbar.inc.xhtml |  10 +
 browser/components/torconnect/jar.mn               |   7 +
 browser/components/torconnect/moz.build            |   6 +
 .../components/torpreferences/content/torPane.js   | 123 +++++
 .../torpreferences/content/torPane.xhtml           |  34 ++
 .../torpreferences/content/torPreferences.css      | 112 +++++
 browser/components/urlbar/UrlbarInput.jsm          |  32 ++
 browser/modules/TorConnect.jsm                     | 506 +++++++++++++++++++++
 browser/modules/TorProcessService.jsm              |  12 +
 browser/modules/TorProtocolService.jsm             |  58 ++-
 browser/modules/TorStrings.jsm                     |  80 ++++
 browser/modules/moz.build                          |   2 +
 .../shared/identity-block/identity-block.inc.css   |   7 +-
 browser/themes/shared/jar.inc.mn                   |   2 +
 browser/themes/shared/onionPattern.css             |  31 ++
 browser/themes/shared/onionPattern.inc.xhtml       |  12 +
 browser/themes/shared/onionPattern.svg             |  22 +
 browser/themes/shared/urlbar-searchbar.inc.css     |   2 +
 dom/base/Document.cpp                              |  51 ++-
 dom/base/nsGlobalWindowOuter.cpp                   |   2 +
 .../processsingleton/MainProcessSingleton.jsm      |   5 +
 toolkit/modules/AsyncPrefs.jsm                     |   1 +
 toolkit/modules/RemotePageAccessManager.jsm        |  16 +
 toolkit/mozapps/update/UpdateService.jsm           |  68 ++-
 .../lib/environments/browser-window.js             |   4 +
 54 files changed, 2299 insertions(+), 42 deletions(-)

diff --git a/browser/actors/NetErrorParent.jsm b/browser/actors/NetErrorParent.jsm
index 3472c68f664a4..13afbbbfd4a8b 100644
--- a/browser/actors/NetErrorParent.jsm
+++ b/browser/actors/NetErrorParent.jsm
@@ -21,6 +21,10 @@ const { TelemetryController } = ChromeUtils.import(
   "resource://gre/modules/TelemetryController.jsm"
 );
 
+const { TorConnect } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+
 const PREF_SSL_IMPACT_ROOTS = [
   "security.tls.version.",
   "security.ssl3.",
@@ -350,6 +354,10 @@ class NetErrorParent extends JSWindowActorParent {
             break;
           }
         }
+        break;
+      case "ShouldShowTorConnect":
+        return TorConnect.shouldShowTorConnect;
     }
+    return undefined;
   }
 }
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
index 1b2c7bcb22cf5..6dcfbd1e491fc 100644
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -57,7 +57,10 @@ var gIdentityHandler = {
    * RegExp used to decide if an about url should be shown as being part of
    * the browser UI.
    */
-  _secureInternalPages: (AppConstants.TOR_BROWSER_UPDATE ? /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion|tor|tbupdate)(?:[?#]|$)/i : /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion|tor)(?:[?#]|$)/i),
+  _secureInternalPages: (AppConstants.TOR_BROWSER_UPDATE ?
+                        /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion|tor|torconnect|tbupdate)(?:[?#]|$)/i :
+                        /^(?:accounts|addons|cache|certificate|config|crashes|downloads|license|logins|preferences|protections|rights|sessionrestore|support|welcomeback|ion|tor|torconnect)(?:[?#]|$)/i),
+
 
   /**
    * Whether the established HTTPS connection is considered "broken".
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index ac429023ab35d..16123f02ff49b 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -80,6 +80,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   TabModalPrompt: "chrome://global/content/tabprompts.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
+  TorConnect: "resource:///modules/TorConnect.jsm",
   Translation: "resource:///modules/translation/TranslationParent.jsm",
   OnionAliasStore: "resource:///modules/OnionAliasStore.jsm",
   UITour: "resource:///modules/UITour.jsm",
@@ -645,6 +646,7 @@ var gPageIcons = {
 
 var gInitialPages = [
   "about:tor",
+  "about:torconnect",
   "about:blank",
   "about:newtab",
   "about:home",
@@ -1859,6 +1861,8 @@ var gBrowserInit = {
     }
 
     this._loadHandled = true;
+
+    TorBootstrapUrlbar.init();
   },
 
   _cancelDelayedStartup() {
@@ -2409,32 +2413,48 @@ var gBrowserInit = {
 
       let defaultArgs = BrowserHandler.defaultArgs;
 
-      // If the given URI is different from the homepage, we want to load it.
-      if (uri != defaultArgs) {
-        AboutNewTab.noteNonDefaultStartup();
+      // figure out which URI to actually load (or a Promise to get the uri)
+      uri = ((uri) => {
+        // If the given URI is different from the homepage, we want to load it.
+        if (uri != defaultArgs) {
+          AboutNewTab.noteNonDefaultStartup();
+
+          if (uri instanceof Ci.nsIArray) {
+            // Transform the nsIArray of nsISupportsString's into a JS Array of
+            // JS strings.
+            return Array.from(
+              uri.enumerate(Ci.nsISupportsString),
+              supportStr => supportStr.data
+            );
+          } else if (uri instanceof Ci.nsISupportsString) {
+            return uri.data;
+          }
+          return uri;
+        }
 
-        if (uri instanceof Ci.nsIArray) {
-          // Transform the nsIArray of nsISupportsString's into a JS Array of
-          // JS strings.
-          return Array.from(
-            uri.enumerate(Ci.nsISupportsString),
-            supportStr => supportStr.data
-          );
-        } else if (uri instanceof Ci.nsISupportsString) {
-          return uri.data;
+        // The URI appears to be the the homepage. We want to load it only if
+        // session restore isn't about to override the homepage.
+        let willOverride = SessionStartup.willOverrideHomepage;
+        if (typeof willOverride == "boolean") {
+          return willOverride ? null : uri;
         }
-        return uri;
-      }
+        return willOverride.then(willOverrideHomepage =>
+          willOverrideHomepage ? null : uri
+        );
+      })(uri);
+
+      // if using TorConnect, convert these uris to redirects
+      if (TorConnect.shouldShowTorConnect) {
+        return Promise.resolve(uri).then((uri) => {
+          if (uri == null) {
+            uri  = [];
+          }
 
-      // The URI appears to be the the homepage. We want to load it only if
-      // session restore isn't about to override the homepage.
-      let willOverride = SessionStartup.willOverrideHomepage;
-      if (typeof willOverride == "boolean") {
-        return willOverride ? null : uri;
+          uri = TorConnect.getURIsToLoad(uri);
+          return uri;
+        });
       }
-      return willOverride.then(willOverrideHomepage =>
-        willOverrideHomepage ? null : uri
-      );
+      return uri;
     })());
   },
 
@@ -2501,6 +2521,8 @@ var gBrowserInit = {
 
     OnionAuthPrompt.uninit();
 
+    TorBootstrapUrlbar.uninit();
+
     gAccessibilityServiceIndicator.uninit();
 
     if (gToolbarKeyNavEnabled) {
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
index 65445a0991488..394a464140186 100644
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -10,6 +10,7 @@
      override rules using selectors with the same specificity. This applies to
      both "content" and "skin" packages, which bug 1385444 will unify later. -->
 <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://branding/content/tor-styles.css" type="text/css"?>
 
 <!-- While these stylesheets are defined in Toolkit, they are only used in the
      main browser window, so we can load them here. Bug 1474241 is on file to
@@ -113,6 +114,7 @@
   Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
   Services.scriptloader.loadSubScript("chrome://torbutton/content/tor-circuit-display.js", this);
   Services.scriptloader.loadSubScript("chrome://torbutton/content/torbutton.js", this);
+  Services.scriptloader.loadSubScript("chrome://browser/content/torconnect/torBootstrapUrlbar.js", this);
 
   window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
   window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
diff --git a/browser/base/content/certerror/aboutNetError.js b/browser/base/content/certerror/aboutNetError.js
index e5b223025a8b3..60f602ba65303 100644
--- a/browser/base/content/certerror/aboutNetError.js
+++ b/browser/base/content/certerror/aboutNetError.js
@@ -240,7 +240,7 @@ function setErrorPageStrings(err) {
   document.l10n.setAttributes(titleElement, title);
 }
 
-function initPage() {
+async function initPage() {
   // We show an offline support page in case of a system-wide error,
   // when a user cannot connect to the internet and access the SUMO website.
   // For example, clock error, which causes certerrors across the web or
@@ -259,6 +259,16 @@ function initPage() {
   }
 
   var err = getErrorCode();
+
+  // proxyConnectFailure because no-tor running daemon would return this error
+  if (
+     (err === "proxyConnectFailure") &&
+     (await RPMSendQuery("ShouldShowTorConnect"))
+  ) {
+     // pass orginal destination as redirect param
+     const encodedRedirect = encodeURIComponent(document.location.href);
+     document.location.replace(`about:torconnect?redirect=${encodedRedirect}`);
+  }
   // List of error pages with an illustration.
   let illustratedErrors = [
     "malformedURI",
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index 1aad36ab3bfc7..b881492864ae9 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -331,6 +331,7 @@
                    data-l10n-id="urlbar-go-button"/>
             <hbox id="page-action-buttons" context="pageActionContextMenu">
               <toolbartabstop/>
+#include ../../components/torconnect/content/torconnect-urlbar.inc.xhtml
               <hbox id="contextual-feature-recommendation" role="button" hidden="true">
                 <hbox id="cfr-label-container">
                   <label id="cfr-label"/>
diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js
index 7b6dc5b921de6..298c8d85f5609 100644
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -21,6 +21,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   ExtensionSettingsStore: "resource://gre/modules/ExtensionSettingsStore.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   ShellService: "resource:///modules/ShellService.jsm",
+  TorConnect: "resource:///modules/TorConnect.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(this, "ReferrerInfo", () =>
@@ -258,6 +259,13 @@ function openUILinkIn(
   aPostData,
   aReferrerInfo
 ) {
+
+  // make sure users are not faced with the scary red 'tor isn't working' screen
+  // if they navigate to about:tor before bootstrapped
+  if (url === "about:tor" && TorConnect.shouldShowTorConnect) {
+    url = `about:torconnect?redirect=${encodeURIComponent("about:tor")}`;
+  }
+
   var params;
 
   if (arguments.length == 3 && typeof arguments[2] == "object") {
diff --git a/browser/branding/alpha/content/jar.mn b/browser/branding/alpha/content/jar.mn
index 6db01f74fd204..93ff6ecf736b3 100644
--- a/browser/branding/alpha/content/jar.mn
+++ b/browser/branding/alpha/content/jar.mn
@@ -18,4 +18,6 @@ browser.jar:
   content/branding/icon128.png                   (../default128.png)
   content/branding/icon256.png                   (../default256.png)
   content/branding/icon512.png                   (../default512.png)
+  content/branding/identity-icons-brand.svg
   content/branding/aboutDialog.css
+* content/branding/tor-styles.css
diff --git a/browser/branding/alpha/content/tor-styles.css b/browser/branding/alpha/content/tor-styles.css
new file mode 100644
index 0000000000000..14c1915ef871d
--- /dev/null
+++ b/browser/branding/alpha/content/tor-styles.css
@@ -0,0 +1,13 @@
+%include ../../tor-styles.inc.css
+
+/* default theme*/
+:root,
+/* light theme*/
+:root:-moz-lwtheme-darktext {
+    --tor-branding-color: var(--teal-70);
+}
+
+/* dark theme */
+:root:-moz-lwtheme-brighttext {
+    --tor-branding-color: var(--teal-60);
+}
\ No newline at end of file
diff --git a/browser/branding/nightly/content/jar.mn b/browser/branding/nightly/content/jar.mn
index 713d4c4924756..93ff6ecf736b3 100644
--- a/browser/branding/nightly/content/jar.mn
+++ b/browser/branding/nightly/content/jar.mn
@@ -20,3 +20,4 @@ browser.jar:
   content/branding/icon512.png                   (../default512.png)
   content/branding/identity-icons-brand.svg
   content/branding/aboutDialog.css
+* content/branding/tor-styles.css
diff --git a/browser/branding/nightly/content/tor-styles.css b/browser/branding/nightly/content/tor-styles.css
new file mode 100644
index 0000000000000..52e1761e54598
--- /dev/null
+++ b/browser/branding/nightly/content/tor-styles.css
@@ -0,0 +1,13 @@
+%include ../../tor-styles.inc.css
+
+/* default theme*/
+:root,
+/* light theme*/
+:root:-moz-lwtheme-darktext {
+    --tor-branding-color: var(--blue-60);
+}
+
+/* dark theme */
+:root:-moz-lwtheme-brighttext {
+    --tor-branding-color: var(--blue-40);
+}
\ No newline at end of file
diff --git a/browser/branding/official/content/jar.mn b/browser/branding/official/content/jar.mn
index 713d4c4924756..93ff6ecf736b3 100644
--- a/browser/branding/official/content/jar.mn
+++ b/browser/branding/official/content/jar.mn
@@ -20,3 +20,4 @@ browser.jar:
   content/branding/icon512.png                   (../default512.png)
   content/branding/identity-icons-brand.svg
   content/branding/aboutDialog.css
+* content/branding/tor-styles.css
diff --git a/browser/branding/official/content/tor-styles.css b/browser/branding/official/content/tor-styles.css
new file mode 100644
index 0000000000000..e4ccb5c767a90
--- /dev/null
+++ b/browser/branding/official/content/tor-styles.css
@@ -0,0 +1,14 @@
+%include ../../tor-styles.inc.css
+
+/* default theme*/
+:root,
+/* light theme*/
+:root:-moz-lwtheme-darktext {
+    --tor-branding-color: var(--purple-60);
+}
+
+/* dark theme */
+:root:-moz-lwtheme-brighttext {
+    --tor-branding-color: var(--purple-30);
+}
+
diff --git a/browser/branding/tor-styles.inc.css b/browser/branding/tor-styles.inc.css
new file mode 100644
index 0000000000000..55dc9b6238b3a
--- /dev/null
+++ b/browser/branding/tor-styles.inc.css
@@ -0,0 +1,87 @@
+:root {
+  /* photon colors, not all of them are available for whatever reason
+     in firefox, so here they are */
+
+  --magenta-50: #ff1ad9;
+  --magenta-60: #ed00b5;
+  --magenta-70: #b5007f;
+  --magenta-80: #7d004f;
+  --magenta-90: #440027;
+
+  --purple-30: #c069ff;
+  --purple-40: #ad3bff;
+  --purple-50: #9400ff;
+  --purple-60: #8000d7;
+  --purple-70: #6200a4;
+  --purple-80: #440071;
+  --purple-90: #25003e;
+
+  --blue-40: #45a1ff;
+  --blue-50: #0a84ff;
+  --blue-50-a30: rgba(10, 132, 255, 0.3);
+  --blue-60: #0060df;
+  --blue-70: #003eaa;
+  --blue-80: #002275;
+  --blue-90: #000f40;
+
+  --teal-50: #00feff;
+  --teal-60: #00c8d7;
+  --teal-70: #008ea4;
+  --teal-80: #005a71;
+  --teal-90: #002d3e;
+
+  --green-50: #30e60b;
+  --green-60: #12bc00;
+  --green-70: #058b00;
+  --green-80: #006504;
+  --green-90: #003706;
+
+  --yellow-50: #ffe900;
+  --yellow-60: #d7b600;
+  --yellow-70: #a47f00;
+  --yellow-80: #715100;
+  --yellow-90: #3e2800;
+
+  --red-50: #ff0039;
+  --red-60: #d70022;
+  --red-70: #a4000f;
+  --red-80: #5a0002;
+  --red-90: #3e0200;
+
+  --orange-50: #ff9400;
+  --orange-60: #d76e00;
+  --orange-70: #a44900;
+  --orange-80: #712b00;
+  --orange-90: #3e1300;
+
+  --grey-10: #f9f9fa;
+  --grey-10-a10: rgba(249, 249, 250, 0.1);
+  --grey-10-a20: rgba(249, 249, 250, 0.2);
+  --grey-10-a40: rgba(249, 249, 250, 0.4);
+  --grey-10-a60: rgba(249, 249, 250, 0.6);
+  --grey-10-a80: rgba(249, 249, 250, 0.8);
+  --grey-20: #ededf0;
+  --grey-30: #d7d7db;
+  --grey-40: #b1b1b3;
+  --grey-50: #737373;
+  --grey-60: #4a4a4f;
+  --grey-70: #38383d;
+  --grey-80: #2a2a2e;
+  --grey-90: #0c0c0d;
+  --grey-90-a05: rgba(12, 12, 13, 0.05);
+  --grey-90-a10: rgba(12, 12, 13, 0.1);
+  --grey-90-a20: rgba(12, 12, 13, 0.2);
+  --grey-90-a30: rgba(12, 12, 13, 0.3);
+  --grey-90-a40: rgba(12, 12, 13, 0.4);
+  --grey-90-a50: rgba(12, 12, 13, 0.5);
+  --grey-90-a60: rgba(12, 12, 13, 0.6);
+  --grey-90-a70: rgba(12, 12, 13, 0.7);
+  --grey-90-a80: rgba(12, 12, 13, 0.8);
+  --grey-90-a90: rgba(12, 12, 13, 0.9);
+
+  --ink-70: #363959;
+  --ink-80: #202340;
+  --ink-90: #0f1126;
+
+  --white-100: #ffffff;
+}
\ No newline at end of file
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index 62ae1f5595635..e2824bffdf070 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -725,6 +725,20 @@ let JSWINDOWACTORS = {
     allFrames: true,
   },
 
+  TorConnect: {
+    parent: {
+      moduleURI: "resource:///modules/TorConnectParent.jsm",
+    },
+    child: {
+      moduleURI: "resource:///modules/TorConnectChild.jsm",
+      events: {
+        DOMWindowCreated: {},
+      },
+    },
+
+    matches: ["about:torconnect","about:torconnect?*"],
+  },
+
   Translation: {
     parent: {
       moduleURI: "resource:///modules/translation/TranslationParent.jsm",
@@ -2522,7 +2536,28 @@ BrowserGlue.prototype = {
 
       {
         task: () => {
-          OnionAliasStore.init();
+          const { TorConnect, TorConnectTopics } = ChromeUtils.import(
+            "resource:///modules/TorConnect.jsm"
+          );
+          if (!TorConnect.shouldShowTorConnect) {
+            // we will take this path when the user is using the legacy tor launcher or
+            // when Tor Browser didn't launch its own tor.
+            OnionAliasStore.init();
+          } else {
+            // this path is taken when using about:torconnect, we wait to init
+            // after we are bootstrapped and connected to tor
+            const topic = TorConnectTopics.BootstrapComplete;
+            let bootstrapObserver = {
+              observe(aSubject, aTopic, aData) {
+                if (aTopic === topic) {
+                  OnionAliasStore.init();
+                  // we only need to init once, so remove ourselves as an obvserver
+                  Services.obs.removeObserver(this, topic);
+                }
+              }
+            };
+            Services.obs.addObserver(bootstrapObserver, topic);
+          }
         },
       },
 
diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp
index 323c1b6fb6531..fd828a630c92a 100644
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -128,6 +128,10 @@ static const RedirEntry kRedirMap[] = {
          nsIAboutModule::URI_MUST_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT |
          nsIAboutModule::HIDE_FROM_ABOUTABOUT},
 #endif
+    {"torconnect", "chrome://browser/content/torconnect/aboutTorConnect.xhtml",
+     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+         nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT |
+         nsIAboutModule::HIDE_FROM_ABOUTABOUT},
 };
 
 static nsAutoCString GetAboutModuleName(nsIURI* aURI) {
diff --git a/browser/components/about/components.conf b/browser/components/about/components.conf
index 67f178ee23fff..0916bb75e1d57 100644
--- a/browser/components/about/components.conf
+++ b/browser/components/about/components.conf
@@ -26,6 +26,7 @@ pages = [
     'robots',
     'sessionrestore',
     'tabcrashed',
+    'torconnect',
     'welcome',
     'welcomeback',
 ]
diff --git a/browser/components/moz.build b/browser/components/moz.build
index 0ea2969e60b06..c304973749122 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -55,6 +55,7 @@ DIRS += [
     "syncedtabs",
     "uitour",
     "urlbar",
+    "torconnect",
     "torpreferences",
     "translation",
 ]
diff --git a/browser/components/onionservices/HttpsEverywhereControl.jsm b/browser/components/onionservices/HttpsEverywhereControl.jsm
index 525ed5233be7c..d673de4cd6e57 100644
--- a/browser/components/onionservices/HttpsEverywhereControl.jsm
+++ b/browser/components/onionservices/HttpsEverywhereControl.jsm
@@ -41,6 +41,7 @@ const SECUREDROP_TOR_ONION_CHANNEL = {
 class HttpsEverywhereControl {
   constructor() {
     this._extensionMessaging = null;
+    this._init();
   }
 
   async _sendMessage(type, object) {
@@ -61,7 +62,6 @@ class HttpsEverywhereControl {
    * Installs the .tor.onion update channel in https-everywhere
    */
   async installTorOnionUpdateChannel(retries = 5) {
-    this._init();
 
     // TODO: https-everywhere store is initialized asynchronously, so sending a message
     // immediately results in a `store.get is undefined` error.
@@ -143,5 +143,20 @@ class HttpsEverywhereControl {
     if (!this._extensionMessaging) {
       this._extensionMessaging = new ExtensionMessaging();
     }
+
+    // update all of the existing https-everywhere channels
+    setTimeout(async () => {
+      let pinnedChannels = await this._sendMessage("get_pinned_update_channels");
+      for(let channel of pinnedChannels.update_channels) {
+        this._sendMessage("update_update_channel", channel);
+      }
+
+      let storedChannels = await this._sendMessage("get_stored_update_channels");
+      for(let channel of storedChannels.update_channels) {
+        this._sendMessage("update_update_channel", channel);
+      }
+    }, 0);
+
+
   }
 }
diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm
index 2150c424d8b83..ddeb923784329 100644
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -186,6 +186,10 @@ ChromeUtils.defineModuleGetter(
   "resource://gre/modules/sessionstore/SessionHistory.jsm"
 );
 
+const { TorProtocolService } = ChromeUtils.import(
+    "resource:///modules/TorProtocolService.jsm"
+);
+
 XPCOMUtils.defineLazyServiceGetters(this, {
   gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"],
 });
diff --git a/browser/components/torconnect/TorConnectChild.jsm b/browser/components/torconnect/TorConnectChild.jsm
new file mode 100644
index 0000000000000..bd6dd549f156d
--- /dev/null
+++ b/browser/components/torconnect/TorConnectChild.jsm
@@ -0,0 +1,9 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+var EXPORTED_SYMBOLS = ["TorConnectChild"];
+
+const { RemotePageChild } = ChromeUtils.import(
+  "resource://gre/actors/RemotePageChild.jsm"
+);
+
+class TorConnectChild extends RemotePageChild {}
diff --git a/browser/components/torconnect/TorConnectParent.jsm b/browser/components/torconnect/TorConnectParent.jsm
new file mode 100644
index 0000000000000..526c588a423ef
--- /dev/null
+++ b/browser/components/torconnect/TorConnectParent.jsm
@@ -0,0 +1,144 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+var EXPORTED_SYMBOLS = ["TorConnectParent"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+
+const TorLauncherPrefs = Object.freeze({
+  quickstart: "extensions.torlauncher.quickstart",
+});
+
+/*
+This object is basically a marshalling interface between the TorConnect module
+and a particular about:torconnect page
+*/
+
+class TorConnectParent extends JSWindowActorParent {
+  constructor(...args) {
+    super(...args);
+
+    const self = this;
+
+    this.state = {
+      State: TorConnect.state,
+      StateChanged: false,
+      ErrorMessage: TorConnect.errorMessage,
+      ErrorDetails: TorConnect.errorDetails,
+      BootstrapProgress: TorConnect.bootstrapProgress,
+      BootstrapStatus: TorConnect.bootstrapStatus,
+      ShowCopyLog: TorConnect.logHasWarningOrError,
+      QuickStartEnabled: Services.prefs.getBoolPref(TorLauncherPrefs.quickstart, false),
+    };
+
+    // JSWindowActiveParent derived objects cannot observe directly, so create a member
+    // object to do our observing for us
+    //
+    // This object converts the various lifecycle events from the TorConnect module, and
+    // maintains a state object which we pass down to our about:torconnect page, which uses
+    // the state object to update its UI
+    this.torConnectObserver = {
+      observe(aSubject, aTopic, aData) {
+        let obj = aSubject?.wrappedJSObject;
+
+        // update our state struct based on received torconnect topics and forward on
+        // to aboutTorConnect.js
+        self.state.StateChanged = false;
+        switch(aTopic) {
+          case TorConnectTopics.StateChange: {
+            self.state.State = obj.state;
+            self.state.StateChanged = true;
+            // clear any previous error information if we are bootstrapping
+            if (self.state.State === TorConnectState.Bootstrapping) {
+              self.state.ErrorMessage = null;
+              self.state.ErrorDetails = null;
+            }
+            break;
+          }
+          case TorConnectTopics.BootstrapProgress: {
+            self.state.BootstrapProgress = obj.progress;
+            self.state.BootstrapStatus = obj.status;
+            self.state.ShowCopyLog = obj.hasWarnings;
+            break;
+          }
+          case TorConnectTopics.BootstrapComplete: {
+            // noop
+            break;
+          }
+          case TorConnectTopics.BootstrapError: {
+            self.state.ErrorMessage = obj.message;
+            self.state.ErrorDetails = obj.details;
+            self.state.ShowCopyLog = true;
+            break;
+          }
+          case TorConnectTopics.FatalError: {
+            // TODO: handle
+            break;
+          }
+          case "nsPref:changed": {
+            if (aData === TorLauncherPrefs.quickstart) {
+              self.state.QuickStartEnabled = Services.prefs.getBoolPref(TorLauncherPrefs.quickstart);
+            }
+            break;
+          }
+          default: {
+            console.log(`TorConnect: unhandled observe topic '${aTopic}'`);
+          }
+        }
+
+        self.sendAsyncMessage("torconnect:state-change", self.state);
+      },
+    };
+
+    // observe all of the torconnect:.* topics
+    for (const key in TorConnectTopics) {
+      const topic = TorConnectTopics[key];
+      Services.obs.addObserver(this.torConnectObserver, topic);
+    }
+    Services.prefs.addObserver(TorLauncherPrefs.quickstart, this.torConnectObserver);
+  }
+
+  willDestroy() {
+    // stop observing all of our torconnect:.* topics
+    for (const key in TorConnectTopics) {
+      const topic = TorConnectTopics[key];
+      Services.obs.removeObserver(this.torConnectObserver, topic);
+    }
+    Services.prefs.removeObserver(TorLauncherPrefs.quickstart, this.torConnectObserver);
+  }
+
+  receiveMessage(message) {
+    switch (message.name) {
+      case "torconnect:set-quickstart":
+        Services.prefs.setBoolPref(TorLauncherPrefs.quickstart, message.data);
+        break;
+      case "torconnect:open-tor-preferences":
+        TorConnect.openTorPreferences();
+        break;
+      case "torconnect:copy-tor-logs":
+        return TorConnect.copyTorLogs();
+      case "torconnect:cancel-bootstrap":
+        TorConnect.cancelBootstrap();
+        break;
+      case "torconnect:begin-bootstrap":
+        TorConnect.beginBootstrap();
+        break;
+      case "torconnect:get-init-args":
+        // called on AboutTorConnect.init(), pass down all state data it needs to init
+
+        // pretend this is a state transition on init
+        // so we always get fresh UI
+        this.state.StateChanged = true;
+        return {
+            TorStrings: TorStrings,
+            TorConnectState: TorConnectState,
+            Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr",
+            State: this.state,
+        };
+    }
+    return undefined;
+  }
+}
diff --git a/browser/components/torconnect/content/aboutTorConnect.css b/browser/components/torconnect/content/aboutTorConnect.css
new file mode 100644
index 0000000000000..14a3df2a59be3
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.css
@@ -0,0 +1,180 @@
+
+/* Copyright (c) 2021, The Tor Project, Inc. */
+
+ at import url("chrome://browser/skin/error-pages.css");
+ at import url("chrome://branding/content/tor-styles.css");
+
+:root {
+  --onion-opacity: 1;
+  --onion-color: var(--card-outline-color);
+  --onion-radius: 75px;
+}
+
+/* override firefox's default blue focus coloring */
+:focus {
+  outline:  none!important;
+  box-shadow: 0 0 0 3px var(--purple-30) !important;
+  border:  1px var(--purple-80) solid !important;
+}
+
+ at media (prefers-color-scheme: dark)
+{
+  :focus {
+    box-shadow: 0 0 0 3px var(--purple-50)!important;
+  }
+}
+
+#connectButton {
+  background-color: var(--purple-60)!important;
+  color: white;
+  fill: white;
+}
+
+#connectButton:hover {
+  background-color: var(--purple-70)!important;
+  color: white;
+  fill: white;
+}
+
+#connectButton:active {
+  background-color: var(--purple-80)!important;
+  color: white;
+  fill: white;
+}
+
+/* checkbox css */
+input[type="checkbox"]:not(:disabled) {
+  background-color: var(--grey-20)!important;
+}
+
+input[type="checkbox"]:not(:disabled):checked {
+  background-color: var(--purple-60)!important;
+  color: white;
+  fill: white;
+}
+
+input[type="checkbox"]:not(:disabled):hover {
+  /* override firefox's default blue border on hover */
+  border-color: var(--purple-70);
+  background-color: var(--grey-30)!important;
+}
+
+input[type="checkbox"]:not(:disabled):hover:checked {
+  background-color: var(--purple-70)!important;
+}
+
+input[type="checkbox"]:not(:disabled):active {
+  background-color: var(--grey-40)!important;
+}
+
+input[type="checkbox"]:not(:disabled):active:checked {
+  background-color: var(--purple-80)!important;
+}
+
+#progressBackground {
+  position:fixed;
+  padding:0;
+  margin:0;
+  top:0;
+  left:0;
+  width: 0%;
+  height: 7px;
+  background-image: linear-gradient(90deg, rgb(20, 218, 221) 0%, rgb(128, 109, 236) 100%);
+  border-radius: 0;
+}
+
+#connectPageContainer {
+  margin-top: 10vh;
+  width: 50%;
+}
+
+#quickstartCheckbox, #quickstartCheckboxLabel {
+  vertical-align: middle;
+}
+
+#copyLogButton {
+  position: relative;
+}
+
+/* mirrors p element spacing */
+#copyLogContainer {
+  margin:  1em 0;
+  height:  1.2em;
+  min-height:  1.2em;
+}
+
+#copyLogLink {
+  position:  relative;
+  display:  inline-block;
+  color:  var(--in-content-link-color);
+}
+
+/* hidden apparently only works if no display is set; who knew? */
+#copyLogLink[hidden="true"] {
+  display:  none;
+}
+
+#copyLogLink:hover {
+  cursor:pointer;
+}
+
+/* This div:
+   - is centered over its parent
+   - centers its child
+   - has z-index above parent
+   - ignores mouse events from parent
+*/
+#copyLogTooltip {
+  pointer-events: none;
+  visibility: hidden;
+  display:  flex;
+  justify-content: center;
+  white-space: nowrap;
+  width: 0;
+  position:  absolute;
+
+  z-index:  1;
+  left: 50%;
+  bottom:  calc(100% + 0.25em);
+}
+
+/* tooltip content (any content could go here) */
+#copyLogTooltipText {
+  background-color: var(--green-50);
+  color: var(--green-90);
+  border-radius: 2px;
+  padding: 4px;
+  line-height: 13px;
+  font: 11px sans-serif;
+  font-weight: 400;
+}
+
+/* our speech bubble tail */
+#copyLogTooltipText::after {
+  content: "";
+  position: absolute;
+  top: 100%;
+  left: 50%;
+  margin-left: -4px;
+  border-width: 4px;
+  border-style: solid;
+  border-color: var(--green-50) transparent transparent transparent;
+}
+
+body {
+  padding: 0px !important;
+  justify-content: space-between;
+  background-color: var(--in-content-page-background);
+}
+
+.title {
+  background-image: url("chrome://browser/content/torconnect/onion.svg");
+  -moz-context-properties: fill, fill-opacity;
+  fill-opacity: var(--onion-opacity);
+  fill: var(--onion-color);
+}
+
+.title.error {
+  background-image: url("chrome://browser/content/torconnect/onion-slash.svg");
+}
+
diff --git a/browser/components/torconnect/content/aboutTorConnect.js b/browser/components/torconnect/content/aboutTorConnect.js
new file mode 100644
index 0000000000000..b53f8b13cb800
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.js
@@ -0,0 +1,302 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+/* eslint-env mozilla/frame-script */
+
+// populated in AboutTorConnect.init()
+let TorStrings = {};
+let TorConnectState = {};
+
+class AboutTorConnect {
+  selectors = Object.freeze({
+    textContainer: {
+      title: "div.title",
+      titleText: "h1.title-text",
+    },
+    progress: {
+      description: "p#connectShortDescText",
+      meter: "div#progressBackground",
+    },
+    copyLog: {
+      link: "span#copyLogLink",
+      tooltip: "div#copyLogTooltip",
+      tooltipText: "span#copyLogTooltipText",
+    },
+    quickstart: {
+      checkbox: "input#quickstartCheckbox",
+      label: "label#quickstartCheckboxLabel",
+    },
+    buttons: {
+      connect: "button#connectButton",
+      cancel: "button#cancelButton",
+      advanced: "button#advancedButton",
+    },
+  })
+
+  elements = Object.freeze({
+    title: document.querySelector(this.selectors.textContainer.title),
+    titleText: document.querySelector(this.selectors.textContainer.titleText),
+    progressDescription: document.querySelector(this.selectors.progress.description),
+    progressMeter: document.querySelector(this.selectors.progress.meter),
+    copyLogLink: document.querySelector(this.selectors.copyLog.link),
+    copyLogTooltip: document.querySelector(this.selectors.copyLog.tooltip),
+    copyLogTooltipText: document.querySelector(this.selectors.copyLog.tooltipText),
+    quickstartCheckbox: document.querySelector(this.selectors.quickstart.checkbox),
+    quickstartLabel: document.querySelector(this.selectors.quickstart.label),
+    connectButton: document.querySelector(this.selectors.buttons.connect),
+    cancelButton: document.querySelector(this.selectors.buttons.cancel),
+    advancedButton: document.querySelector(this.selectors.buttons.advanced),
+  })
+
+  // a redirect url can be passed as a query parameter for the page to
+  // forward us to once bootstrap completes (otherwise the window will just close)
+  redirect = null
+
+  beginBootstrap() {
+    this.hide(this.elements.connectButton);
+    this.show(this.elements.cancelButton);
+    this.elements.cancelButton.focus();
+    RPMSendAsyncMessage("torconnect:begin-bootstrap");
+  }
+
+  cancelBootstrap() {
+    RPMSendAsyncMessage("torconnect:cancel-bootstrap");
+  }
+
+  /*
+  Element helper methods
+  */
+
+  show(element) {
+    element.removeAttribute("hidden");
+  }
+
+  hide(element) {
+    element.setAttribute("hidden", "true");
+  }
+
+  setTitle(title, error) {
+    this.elements.titleText.textContent = title;
+    document.title = title;
+
+    if (error) {
+      this.elements.title.classList.add("error");
+    } else {
+      this.elements.title.classList.remove("error");
+    }
+  }
+
+  setProgress(description, visible, percent) {
+    this.elements.progressDescription.textContent = description;
+    if (visible) {
+      this.show(this.elements.progressMeter);
+      this.elements.progressMeter.style.width = `${percent}%`;
+    } else {
+      this.hide(this.elements.progressMeter);
+    }
+  }
+
+  /*
+  These methods update the UI based on the current TorConnect state
+  */
+
+  updateUI(state) {
+    console.log(state);
+
+    // calls update_$state()
+    this[`update_${state.State}`](state);
+    this.elements.quickstartCheckbox.checked = state.QuickStartEnabled;
+  }
+
+  /* Per-state updates */
+
+  update_Initial(state) {
+    const hasError = false;
+    const showProgressbar = false;
+
+    this.setTitle(TorStrings.torConnect.torConnect, hasError);
+    this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar);
+    this.hide(this.elements.copyLogLink);
+    this.hide(this.elements.connectButton);
+    this.hide(this.elements.advancedButton);
+    this.hide(this.elements.cancelButton);
+  }
+
+  update_Configuring(state) {
+    const hasError = state.ErrorMessage != null;
+    const showProgressbar = false;
+
+    if (hasError) {
+      this.setTitle(state.ErrorMessage, hasError);
+      this.setProgress(state.ErrorDetails, showProgressbar);
+      this.show(this.elements.copyLogLink);
+      this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
+    } else {
+      this.setTitle(TorStrings.torConnect.torConnect, hasError);
+      this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar);
+      this.hide(this.elements.copyLogLink);
+      this.elements.connectButton.textContent = TorStrings.torConnect.torConnectButton;
+    }
+    this.show(this.elements.connectButton);
+    if (state.StateChanged) {
+      this.elements.connectButton.focus();
+    }
+    this.show(this.elements.advancedButton);
+    this.hide(this.elements.cancelButton);
+  }
+
+  update_AutoConfiguring(state) {
+    // TODO: noop until this state is used
+  }
+
+  update_Bootstrapping(state) {
+    const hasError = false;
+    const showProgressbar = true;
+
+    this.setTitle(state.BootstrapStatus ? state.BootstrapStatus : TorStrings.torConnect.torConnecting, hasError);
+    this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar, state.BootstrapProgress);
+    if (state.ShowCopyLog) {
+      this.show(this.elements.copyLogLink);
+    } else {
+      this.hide(this.elements.copyLogLink);
+    }
+    this.hide(this.elements.connectButton);
+    this.hide(this.elements.advancedButton);
+    this.show(this.elements.cancelButton);
+    if (state.StateChanged) {
+      this.elements.cancelButton.focus();
+    }
+  }
+
+  update_Error(state) {
+    const hasError = true;
+    const showProgressbar = false;
+
+    this.setTitle(state.ErrorMessage, hasError);
+    this.setProgress(state.ErrorDetails, showProgressbar);
+    this.show(this.elements.copyLogLink);
+    this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
+    this.show(this.elements.connectButton);
+    this.show(this.elements.advancedButton);
+    this.hide(this.elements.cancelButton);
+  }
+
+  update_FatalError(state) {
+    // TODO: noop until this state is used
+  }
+
+  update_Bootstrapped(state) {
+    const hasError = false;
+    const showProgressbar = true;
+
+    this.setTitle(TorStrings.torConnect.torConnected, hasError);
+    this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar, 100);
+    this.hide(this.elements.connectButton);
+    this.hide(this.elements.advancedButton);
+    this.hide(this.elements.cancelButton);
+
+    // redirects page to the requested redirect url, removes about:torconnect
+    // from the page stack, so users cannot accidentally go 'back' to the
+    // now unresponsive page
+    window.location.replace(this.redirect);
+  }
+
+  update_Disabled(state) {
+    // TODO: we should probably have some UX here if a user goes to about:torconnect when
+    // it isn't in use (eg using tor-launcher or system tor)
+  }
+
+  async initElements(direction) {
+
+    document.documentElement.setAttribute("dir", direction);
+
+    // sets the text content while keeping the child elements intact
+    this.elements.copyLogLink.childNodes[0].nodeValue =
+      TorStrings.torConnect.copyLog;
+    this.elements.copyLogLink.addEventListener("click", async (event) => {
+      const copiedMessage = await RPMSendQuery("torconnect:copy-tor-logs");
+      this.elements.copyLogTooltipText.textContent = copiedMessage;
+      this.elements.copyLogTooltipText.style.visibility = "visible";
+
+      // clear previous timeout if one already exists
+      if (this.copyLogTimeoutId) {
+        clearTimeout(this.copyLogTimeoutId);
+      }
+
+      // hide tooltip after X ms
+      const TOOLTIP_TIMEOUT = 2000;
+      this.copyLogTimeoutId = setTimeout(() => {
+        this.elements.copyLogTooltipText.style.visibility = "hidden";
+        this.copyLogTimeoutId = 0;
+      }, TOOLTIP_TIMEOUT);
+    });
+
+    this.elements.quickstartCheckbox.addEventListener("change", () => {
+      const quickstart = this.elements.quickstartCheckbox.checked;
+      RPMSendAsyncMessage("torconnect:set-quickstart", quickstart);
+    });
+    this.elements.quickstartLabel.textContent = TorStrings.settings.quickstartCheckbox;
+
+    this.elements.connectButton.textContent =
+      TorStrings.torConnect.torConnectButton;
+    this.elements.connectButton.addEventListener("click", () => {
+      this.beginBootstrap();
+    });
+
+    this.elements.advancedButton.textContent = TorStrings.torConnect.torConfigure;
+    this.elements.advancedButton.addEventListener("click", () => {
+      RPMSendAsyncMessage("torconnect:open-tor-preferences");
+    });
+
+    this.elements.cancelButton.textContent = TorStrings.torConnect.cancel;
+    this.elements.cancelButton.addEventListener("click", () => {
+      this.cancelBootstrap();
+    });
+  }
+
+  initObservers() {
+    // TorConnectParent feeds us state blobs to we use to update our UI
+    RPMAddMessageListener("torconnect:state-change", ({ data }) => {
+      this.updateUI(data);
+    });
+  }
+
+  initKeyboardShortcuts() {
+    document.onkeydown = (evt) => {
+      // unfortunately it looks like we still haven't standardized keycodes to
+      // integers, so we must resort to a string compare here :(
+      // see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for relevant documentation
+      if (evt.code === "Escape") {
+        this.cancelBootstrap();
+      }
+    };
+  }
+
+  async init() {
+    // see if a user has a final destination after bootstrapping
+    let params = new URLSearchParams(new URL(document.location.href).search);
+    if (params.has("redirect")) {
+      const encodedRedirect = params.get("redirect");
+      this.redirect = decodeURIComponent(encodedRedirect);
+    } else {
+      // if the user gets here manually or via the button in the urlbar
+      // then we will redirect to about:tor
+      this.redirect = "about:tor";
+    }
+
+    let args = await RPMSendQuery("torconnect:get-init-args");
+
+    // various constants
+    TorStrings = Object.freeze(args.TorStrings);
+    TorConnectState = Object.freeze(args.TorConnectState);
+
+    this.initElements(args.Direction);
+    this.initObservers();
+    this.initKeyboardShortcuts();
+
+    // populate UI based on current state
+    this.updateUI(args.State);
+  }
+}
+
+const aboutTorConnect = new AboutTorConnect();
+aboutTorConnect.init();
diff --git a/browser/components/torconnect/content/aboutTorConnect.xhtml b/browser/components/torconnect/content/aboutTorConnect.xhtml
new file mode 100644
index 0000000000000..595bbdf9a70a1
--- /dev/null
+++ b/browser/components/torconnect/content/aboutTorConnect.xhtml
@@ -0,0 +1,45 @@
+<!-- Copyright (c) 2021, The Tor Project, Inc. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
+    <link rel="stylesheet" href="chrome://browser/skin/onionPattern.css" type="text/css" media="all" />
+    <link rel="stylesheet" href="chrome://browser/content/torconnect/aboutTorConnect.css" type="text/css" media="all" />
+  </head>
+  <body>
+    <div id="progressBackground"></div>
+    <div id="connectPageContainer" class="container">
+      <div id="text-container">
+        <div class="title">
+          <h1 class="title-text"/>
+        </div>
+        <div id="connectLongContent">
+          <div id="connectShortDesc">
+            <p id="connectShortDescText" />
+          </div>
+        </div>
+
+        <div id="copyLogContainer">
+          <span id="copyLogLink" hidden="true">
+            <div id="copyLogTooltip">
+              <span id="copyLogTooltipText"/>
+            </div>
+          </span>
+        </div>
+
+        <div id="quickstartContainer">
+          <input id="quickstartCheckbox" type="checkbox" />
+          <label id="quickstartCheckboxLabel" for="quickstartCheckbox"/>
+        </div>
+
+        <div id="connectButtonContainer" class="button-container">
+          <button id="advancedButton" hidden="true"></button>
+          <button id="cancelButton" hidden="true"></button>
+          <button id="connectButton" class="primary try-again" hidden="true"></button>
+        </div>
+      </div>
+    </div>
+#include ../../../themes/shared/onionPattern.inc.xhtml
+  </body>
+  <script src="chrome://browser/content/torconnect/aboutTorConnect.js"/>
+</html>
diff --git a/browser/components/torconnect/content/onion-slash.svg b/browser/components/torconnect/content/onion-slash.svg
new file mode 100644
index 0000000000000..93eb24b039055
--- /dev/null
+++ b/browser/components/torconnect/content/onion-slash.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
+  <path d="m14.1161 15.6245c-.0821.0001-.1634-.016-.2393-.0474-.0758-.0314-.1447-.0775-.2027-.1356l-12.749984-12.749c-.109266-.11882-.168406-.27526-.165071-.43666.003335-.16139.068886-.31525.182967-.42946.114078-.11421.267868-.17994.429258-.18345.16139-.00352.3179.05544.43685.16457l12.74998 12.75c.1168.1176.1824.2767.1824.4425s-.0656.3249-.1824.4425c-.058.058-.1269.1039-.2028.1352-.0759.0312-.1571.0471-.2392.0468z" fill-opacity="context-fill-opacity" fill="#ff0039" />
+  <path d="m 8,0.5000002 c -1.61963,0 -3.1197431,0.5137987 -4.3457031,1.3867188 l 0.84375,0.8417968 0.7792969,0.78125 0.8613281,0.8613282 0.8164062,0.8164062 0.9863281,0.984375 h 0.058594 c 1.00965,0 1.828125,0.818485 1.828125,1.828125 0,0.01968 6.2e-4,0.039074 0,0.058594 L 10.8125,9.0449221 C 10.9334,8.7195921 11,8.3674002 11,8.0000002 c 0,-1.65685 -1.34314,-3 -3,-3 v -1.078125 c 2.25231,0 4.078125,1.825845 4.078125,4.078125 0,0.67051 -0.162519,1.3033281 -0.449219,1.8613281 l 0.861328,0 [...]
+</svg>
diff --git a/browser/components/torconnect/content/onion.svg b/browser/components/torconnect/content/onion.svg
new file mode 100644
index 0000000000000..7655a800d9eec
--- /dev/null
+++ b/browser/components/torconnect/content/onion.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
+  <path d="M 8 0.5 C 3.85786 0.5 0.5 3.85786 0.5 8 C 0.5 12.1421 3.85786 15.5 8 15.5 C 12.1421 15.5 15.5 12.1421 15.5 8 C 15.5 3.85786 12.1421 0.5 8 0.5 z M 8 1.671875 C 11.4949 1.671875 14.328125 4.50507 14.328125 8 C 14.328125 11.4949 11.4949 14.328125 8 14.328125 L 8 13.25 C 10.89951 13.25 13.25 10.89951 13.25 8 C 13.25 5.10051 10.89951 2.75 8 2.75 L 8 1.671875 z M 8 3.921875 C 10.25231 3.921875 12.078125 5.74772 12.078125 8 C 12.078125 10.25231 10.25231 12.078125 8 12.078125 L 8 11 C [...]
+</svg>
diff --git a/browser/components/torconnect/content/torBootstrapUrlbar.js b/browser/components/torconnect/content/torBootstrapUrlbar.js
new file mode 100644
index 0000000000000..e6a88490f33d4
--- /dev/null
+++ b/browser/components/torconnect/content/torBootstrapUrlbar.js
@@ -0,0 +1,93 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+"use strict";
+
+const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+const { TorStrings } = ChromeUtils.import(
+  "resource:///modules/TorStrings.jsm"
+);
+
+var TorBootstrapUrlbar = {
+  selectors: Object.freeze({
+    torConnect: {
+      box: "hbox#torconnect-box",
+      label: "label#torconnect-label",
+    },
+  }),
+
+  elements: null,
+
+  updateTorConnectBox: function(state) {
+    switch(state)
+    {
+      case TorConnectState.Initial:
+      case TorConnectState.Configuring:
+      case TorConnectState.AutoConfiguring:
+      case TorConnectState.Error:
+      case TorConnectState.FatalError: {
+        this.elements.torConnectBox.removeAttribute("hidden");
+        this.elements.torConnectLabel.textContent =
+          TorStrings.torConnect.torNotConnectedConcise;
+        this.elements.inputContainer.setAttribute("torconnect", "offline");
+        break;
+      }
+      case TorConnectState.Bootstrapping: {
+        this.elements.torConnectBox.removeAttribute("hidden");
+        this.elements.torConnectLabel.textContent =
+          TorStrings.torConnect.torConnectingConcise;
+        this.elements.inputContainer.setAttribute("torconnect", "connecting");
+        break;
+      }
+      case TorConnectState.Bootstrapped: {
+        this.elements.torConnectBox.removeAttribute("hidden");
+        this.elements.torConnectLabel.textContent =
+          TorStrings.torConnect.torConnectedConcise;
+        this.elements.inputContainer.setAttribute("torconnect", "connected");
+        // hide torconnect box after 5 seconds
+        setTimeout(() => {
+          this.elements.torConnectBox.setAttribute("hidden", "true");
+        }, 5000);
+        break;
+      }
+      case TorConnectState.Disabled: {
+        this.elements.torConnectBox.setAttribute("hidden", "true");
+        break;
+      }
+      default:
+        break;
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic === TorConnectTopics.StateChange) {
+      const obj = aSubject?.wrappedJSObject;
+      this.updateTorConnectBox(obj?.state);
+    }
+  },
+
+  init: function() {
+    if (TorConnect.shouldShowTorConnect) {
+      // browser isn't populated until init
+      this.elements = Object.freeze({
+        torConnectBox: browser.ownerGlobal.document.querySelector(this.selectors.torConnect.box),
+        torConnectLabel: browser.ownerGlobal.document.querySelector(this.selectors.torConnect.label),
+        inputContainer: gURLBar._inputContainer,
+      })
+      this.elements.torConnectBox.addEventListener("click", () => {
+        TorConnect.openTorConnect();
+      });
+      Services.obs.addObserver(this, TorConnectTopics.StateChange);
+      this.observing = true;
+      this.updateTorConnectBox(TorConnect.state);
+    }
+  },
+
+  uninit: function() {
+    if (this.observing) {
+      Services.obs.removeObserver(this, TorConnectTopics.StateChange);
+    }
+  },
+};
+
diff --git a/browser/components/torconnect/content/torconnect-urlbar.css b/browser/components/torconnect/content/torconnect-urlbar.css
new file mode 100644
index 0000000000000..5aabcffedbd02
--- /dev/null
+++ b/browser/components/torconnect/content/torconnect-urlbar.css
@@ -0,0 +1,57 @@
+/*
+    ensure our torconnect button is always visible (same rule as for the bookmark button)
+*/
+hbox.urlbar-page-action#torconnect-box {
+    display: -moz-inline-box!important;
+    height: 28px;
+}
+
+label#torconnect-label {
+    line-height: 28px;
+    margin: 0;
+    opacity: 0.6;
+    padding: 0 0.5em;
+}
+
+/* set appropriate sizes for the non-standard ui densities */
+:root[uidensity=compact] hbox.urlbar-page-action#torconnect-box {
+    height: 24px;
+}
+:root[uidensity=compact] label#torconnect-label {
+    line-height: 24px;
+}
+
+
+:root[uidensity=touch] hbox.urlbar-page-action#torconnect-box {
+    height: 30px;
+}
+:root[uidensity=touch] label#torconnect-label {
+    line-height: 30px;
+}
+
+
+/* hide when hidden attribute is set */
+hbox.urlbar-page-action#torconnect-box[hidden="true"],
+/* hide when user is typing in URL bar */
+#urlbar[usertyping] > #urlbar-input-container > #page-action-buttons > #torconnect-box {
+    display: none!important;
+}
+
+/* hide urlbar's placeholder text when not connectd to tor */
+hbox#urlbar-input-container[torconnect="offline"] input#urlbar-input::placeholder,
+hbox#urlbar-input-container[torconnect="connecting"] input#urlbar-input::placeholder {
+    opacity: 0;
+}
+
+/* hide search suggestions when not connected to tor */
+hbox#urlbar-input-container[torconnect="offline"] + vbox.urlbarView,
+hbox#urlbar-input-container[torconnect="connecting"] + vbox.urlbarView {
+    display: none!important;
+}
+
+/* hide search icon when we are not connected to tor */
+hbox#urlbar-input-container[torconnect="offline"] > #identity-box[pageproxystate="invalid"] > #identity-icon,
+hbox#urlbar-input-container[torconnect="connecting"] > #identity-box[pageproxystate="invalid"] > #identity-icon
+{
+    display: none!important;
+}
diff --git a/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml b/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml
new file mode 100644
index 0000000000000..60e985a726910
--- /dev/null
+++ b/browser/components/torconnect/content/torconnect-urlbar.inc.xhtml
@@ -0,0 +1,10 @@
+# Copyright (c) 2021, The Tor Project, Inc.
+
+<hbox id="torconnect-box"
+      class="urlbar-icon-wrapper urlbar-page-action"
+      role="status"
+      hidden="true">
+    <hbox id="torconnect-container">
+        <label id="torconnect-label"/>
+    </hbox>
+</hbox>
\ No newline at end of file
diff --git a/browser/components/torconnect/jar.mn b/browser/components/torconnect/jar.mn
new file mode 100644
index 0000000000000..ed8a4de299b2b
--- /dev/null
+++ b/browser/components/torconnect/jar.mn
@@ -0,0 +1,7 @@
+browser.jar:
+    content/browser/torconnect/torBootstrapUrlbar.js               (content/torBootstrapUrlbar.js)
+    content/browser/torconnect/aboutTorConnect.css                 (content/aboutTorConnect.css)
+*   content/browser/torconnect/aboutTorConnect.xhtml               (content/aboutTorConnect.xhtml)
+    content/browser/torconnect/aboutTorConnect.js                  (content/aboutTorConnect.js)
+    content/browser/torconnect/onion.svg                           (content/onion.svg)
+    content/browser/torconnect/onion-slash.svg                     (content/onion-slash.svg)
diff --git a/browser/components/torconnect/moz.build b/browser/components/torconnect/moz.build
new file mode 100644
index 0000000000000..eb29c31a42439
--- /dev/null
+++ b/browser/components/torconnect/moz.build
@@ -0,0 +1,6 @@
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+    'TorConnectChild.jsm',
+    'TorConnectParent.jsm',
+]
diff --git a/browser/components/torpreferences/content/torPane.js b/browser/components/torpreferences/content/torPane.js
index 49054b5dac6a9..b81a238efc6fc 100644
--- a/browser/components/torpreferences/content/torPane.js
+++ b/browser/components/torpreferences/content/torPane.js
@@ -1,9 +1,15 @@
 "use strict";
 
+/* global Services */
+
 const { TorProtocolService } = ChromeUtils.import(
   "resource:///modules/TorProtocolService.jsm"
 );
 
+const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+
 const {
   TorBridgeSource,
   TorBridgeSettings,
@@ -51,6 +57,10 @@ const { parsePort, parseBridgeStrings, parsePortList } = ChromeUtils.import(
   "chrome://browser/content/torpreferences/parseFunctions.jsm"
 );
 
+const TorLauncherPrefs = {
+  quickstart: "extensions.torlauncher.quickstart",
+}
+
 /*
   Tor Pane
 
@@ -62,11 +72,21 @@ const gTorPane = (function() {
     category: {
       title: "label#torPreferences-labelCategory",
     },
+    messageBox: {
+      box: "div#torPreferences-connectMessageBox",
+      message: "td#torPreferences-connectMessageBox-message",
+      button: "button#torPreferences-connectMessageBox-button",
+    },
     torPreferences: {
       header: "h1#torPreferences-header",
       description: "span#torPreferences-description",
       learnMore: "label#torPreferences-learnMore",
     },
+    quickstart: {
+      header: "h2#torPreferences-quickstart-header",
+      description: "span#torPreferences-quickstart-description",
+      enableQuickstartCheckbox: "checkbox#torPreferences-quickstart-toggle",
+    },
     bridges: {
       header: "h2#torPreferences-bridges-header",
       description: "span#torPreferences-bridges-description",
@@ -112,6 +132,10 @@ const gTorPane = (function() {
 
   let retval = {
     // cached frequently accessed DOM elements
+    _messageBox: null,
+    _messageBoxMessage: null,
+    _messageBoxButton: null,
+    _enableQuickstartCheckbox: null,
     _useBridgeCheckbox: null,
     _bridgeSelectionRadiogroup: null,
     _builtinBridgeOption: null,
@@ -161,6 +185,51 @@ const gTorPane = (function() {
 
       let prefpane = document.getElementById("mainPrefPane");
 
+      // 'Connect to Tor' Message Bar
+
+      this._messageBox = prefpane.querySelector(selectors.messageBox.box);
+      this._messageBoxMessage = prefpane.querySelector(selectors.messageBox.message);
+      this._messageBoxButton = prefpane.querySelector(selectors.messageBox.button);
+      // wire up connect button
+      this._messageBoxButton.addEventListener("click", () => {
+        TorConnect.beginBootstrap();
+        TorConnect.openTorConnect();
+      });
+
+      this._populateMessagebox = () => {
+        if (TorConnect.shouldShowTorConnect &&
+            TorConnect.state === TorConnectState.Configuring) {
+          // set messagebox style and text
+          if (TorProtocolService.torBootstrapErrorOccurred()) {
+            this._messageBox.parentNode.style.display = null;
+            this._messageBox.className = "error";
+            this._messageBoxMessage.innerText = TorStrings.torConnect.tryAgainMessage;
+            this._messageBoxButton.innerText = TorStrings.torConnect.tryAgain;
+          } else {
+            this._messageBox.parentNode.style.display = null;
+            this._messageBox.className = "warning";
+            this._messageBoxMessage.innerText = TorStrings.torConnect.connectMessage;
+            this._messageBoxButton.innerText = TorStrings.torConnect.torConnectButton;
+          }
+        } else {
+          // we need to explicitly hide the groupbox, as switching between
+          // the tor pane and other panes will 'unhide' (via the 'hidden'
+          // attribute) the groupbox, offsetting all of the content down
+          // by the groupbox's margin (even if content is 0 height)
+          this._messageBox.parentNode.style.display = "none";
+          this._messageBox.className = "hidden";
+          this._messageBoxMessage.innerText = "";
+          this._messageBoxButton.innerText = "";
+        }
+      }
+      this._populateMessagebox();
+      Services.obs.addObserver(this, TorConnectTopics.StateChange);
+
+      // update the messagebox whenever we come back to the page
+      window.addEventListener("focus", val => {
+        this._populateMessagebox();
+      });
+
       // Heading
       prefpane.querySelector(selectors.torPreferences.header).innerText =
         TorStrings.settings.torPreferencesHeading;
@@ -177,6 +246,26 @@ const gTorPane = (function() {
         );
       }
 
+      // Quickstart
+      prefpane.querySelector(selectors.quickstart.header).innerText =
+        TorStrings.settings.quickstartHeading;
+      prefpane.querySelector(selectors.quickstart.description).textContent =
+        TorStrings.settings.quickstartDescription;
+
+      this._enableQuickstartCheckbox = prefpane.querySelector(
+        selectors.quickstart.enableQuickstartCheckbox
+      );
+      this._enableQuickstartCheckbox.setAttribute(
+        "label",
+        TorStrings.settings.quickstartCheckbox
+      );
+      this._enableQuickstartCheckbox.addEventListener("command", e => {
+        const checked = this._enableQuickstartCheckbox.checked;
+        Services.prefs.setBoolPref(TorLauncherPrefs.quickstart, checked);
+      });
+      this._enableQuickstartCheckbox.checked = Services.prefs.getBoolPref(TorLauncherPrefs.quickstart);
+      Services.prefs.addObserver(TorLauncherPrefs.quickstart, this);
+
       // Bridge setup
       prefpane.querySelector(selectors.bridges.header).innerText =
         TorStrings.settings.bridgesHeading;
@@ -526,6 +615,18 @@ const gTorPane = (function() {
 
     init() {
       this._populateXUL();
+
+      let onUnload = () => {
+        window.removeEventListener("unload", onUnload);
+        gTorPane.uninit();
+      };
+      window.addEventListener("unload", onUnload);
+    },
+
+    uninit() {
+      // unregister our observer topics
+      Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged);
+      Services.obs.removeObserver(this, TorConnectTopics.StateChange);
     },
 
     // whether the page should be present in about:preferences
@@ -537,6 +638,28 @@ const gTorPane = (function() {
     // Callbacks
     //
 
+    observe(subject, topic, data) {
+      switch (topic) {
+       // triggered when a TorSettings param has changed
+        case TorSettingsTopics.SettingChanged: {
+          let obj = subject?.wrappedJSObject;
+          switch(data) {
+            case TorSettingsData.QuickStartEnabled: {
+              this._enableQuickstartCheckbox.checked = obj.value;
+              break;
+            }
+          }
+          break;
+        }
+        // triggered when tor connect state changes and we may
+        // need to update the messagebox
+        case TorConnectTopics.StateChange: {
+          this._populateMessagebox();
+          break;
+        }
+      }
+    },
+
     // callback when using bridges toggled
     onToggleBridge(enabled) {
       this._useBridgeCheckbox.checked = enabled;
diff --git a/browser/components/torpreferences/content/torPane.xhtml b/browser/components/torpreferences/content/torPane.xhtml
index 3c966b2b3726d..7c8071f2cf106 100644
--- a/browser/components/torpreferences/content/torPane.xhtml
+++ b/browser/components/torpreferences/content/torPane.xhtml
@@ -3,6 +3,29 @@
 <script type="application/javascript"
         src="chrome://browser/content/torpreferences/torPane.js"/>
 <html:template id="template-paneTor">
+
+<!-- Tor Connect Message Box -->
+<groupbox data-category="paneTor" hidden="true">
+  <html:div id="torPreferences-connectMessageBox"
+            class="subcategory"
+            data-category="paneTor"
+            hidden="true">
+    <html:table >
+      <html:tr>
+        <html:td>
+          <html:div id="torPreferences-connectMessageBox-icon"/>
+        </html:td>
+        <html:td id="torPreferences-connectMessageBox-message">
+        </html:td>
+        <html:td>
+          <html:button id="torPreferences-connectMessageBox-button">
+          </html:button>
+        </html:td>
+      </html:tr>
+    </html:table>
+  </html:div>
+</groupbox>
+
 <hbox id="torPreferencesCategory"
       class="subcategory"
       data-category="paneTor"
@@ -18,6 +41,17 @@
   </description>
 </groupbox>
 
+<!-- Quickstart -->
+<groupbox id="torPreferences-quickstart-group"
+          data-category="paneTor"
+          hidden="true">
+  <html:h2 id="torPreferences-quickstart-header"/>
+  <description flex="1">
+    <html:span id="torPreferences-quickstart-description"/>
+  </description>
+  <checkbox id="torPreferences-quickstart-toggle"/>
+</groupbox>
+
 <!-- Bridges -->
 <groupbox id="torPreferences-bridges-group"
           data-category="paneTor"
diff --git a/browser/components/torpreferences/content/torPreferences.css b/browser/components/torpreferences/content/torPreferences.css
index 4dac2c4578237..b6eb0a740e5e4 100644
--- a/browser/components/torpreferences/content/torPreferences.css
+++ b/browser/components/torpreferences/content/torPreferences.css
@@ -1,7 +1,119 @@
+ at import url("chrome://branding/content/tor-styles.css");
+
 #category-tor > .category-icon {
   list-style-image: url("chrome://browser/content/torpreferences/torPreferencesIcon.svg");
 }
 
+/* Connect Message Box */
+
+#torPreferences-connectMessageBox {
+  display: block;
+  position: relative;
+
+  width: auto;
+  min-height: 32px;
+  border-radius: 4px;
+  padding: 8px;
+}
+
+#torPreferences-connectMessageBox.hidden {
+  display: none;
+}
+
+#torPreferences-connectMessageBox.error {
+  background-color: var(--red-60);
+  color: white;
+}
+
+#torPreferences-connectMessageBox.warning {
+  background-color: var(--purple-50);
+  color: white;
+}
+
+#torPreferences-connectMessageBox table {
+  border-collapse: collapse;
+}
+
+#torPreferences-connectMessageBox td {
+  vertical-align: middle;
+}
+
+#torPreferences-connectMessageBox td:first-child {
+  width: 16px;
+}
+
+#torPreferences-connectMessageBox-icon {
+  width: 16px;
+  height: 16px;
+
+  mask-repeat: no-repeat !important;
+  mask-size: 16px !important;
+}
+
+#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-icon
+{
+  mask: url("chrome://browser/skin/onion-slash.svg");
+  background-color: white;
+}
+
+#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-icon
+{
+  mask: url("chrome://browser/skin/onion.svg");
+  background-color: white;
+}
+
+#torPreferences-connectMessageBox-message {
+  line-height: 16px;
+  font-size: 1.11em;
+  padding-left: 8px!important;
+}
+
+#torPreferences-connectMessageBox-button {
+  display: block;
+  width: auto;
+
+  border-radius: 4px;
+  border: 0;
+
+  padding-inline: 18px;
+  padding-block: 8px;
+  margin-block: 0px;
+  margin-inline-start: 8px;
+  margin-inline-end: 0px;
+
+  font-size: 1.0em;
+  font-weight: 600;
+  white-space: nowrap;
+
+  color: white;
+}
+
+#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button {
+  background-color: var(--red-70);
+}
+
+#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button:hover {
+  background-color: var(--red-80);
+}
+
+#torPreferences-connectMessageBox.error #torPreferences-connectMessageBox-button:active {
+  background-color: var(--red-90);
+}
+
+#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button {
+  background-color: var(--purple-70);
+}
+
+#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button:hover {
+  background-color: var(--purple-80);
+}
+
+#torPreferences-connectMessageBox.warning #torPreferences-connectMessageBox-button:active {
+  background-color: var(--purple-90);
+}
+
+/* Advanced Settings */
+
 #torPreferences-advanced-grid {
   display: grid;
   grid-template-columns: auto 1fr;
diff --git a/browser/components/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm
index 45c784351c8f0..29ee12914719b 100644
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -10,6 +10,34 @@ const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
+const { TorConnect } = ChromeUtils.import(
+  "resource:///modules/TorConnect.jsm"
+);
+
+// in certain scenarios we want user input uris to open in a new tab if they do so from the
+// about:torconnect tab 
+function maybeUpdateOpenLocationForTorConnect(openUILinkWhere, currentURI, destinationURI) {
+  try {
+    // only open in new tab if:
+    if (// user is navigating away from about:torconnect
+        currentURI === "about:torconnect" &&
+        // we are trying to open in same tab
+        openUILinkWhere === "current" &&
+        // only if user still has not bootstrapped
+        TorConnect.shouldShowTorConnect &&
+        // and user is not just navigating to about:torconnect
+        destinationURI !== "about:torconnect") {
+      return "tab";
+    }
+  } catch (e) {
+    // swallow exception and fall through returning original so we don't accidentally break
+    // anything if an exception is thrown
+    console.log(e?.message ? e.message : e);
+  }
+
+  return openUILinkWhere;
+};
+
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm",
@@ -2427,6 +2455,10 @@ class UrlbarInput {
       this.selectionStart = this.selectionEnd = 0;
     }
 
+    openUILinkWhere = maybeUpdateOpenLocationForTorConnect(
+                        openUILinkWhere,
+                        this.window.gBrowser.currentURI.asciiSpec,
+                        url);
     if (openUILinkWhere != "current") {
       this.handleRevert();
     }
diff --git a/browser/modules/TorConnect.jsm b/browser/modules/TorConnect.jsm
new file mode 100644
index 0000000000000..4be220108d730
--- /dev/null
+++ b/browser/modules/TorConnect.jsm
@@ -0,0 +1,506 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorConnect", "TorConnectTopics", "TorConnectState"];
+
+const { Services } = ChromeUtils.import(
+    "resource://gre/modules/Services.jsm"
+);
+
+const { BrowserWindowTracker } = ChromeUtils.import(
+    "resource:///modules/BrowserWindowTracker.jsm"
+);
+
+const { TorProtocolService, TorProcessStatus } = ChromeUtils.import(
+    "resource:///modules/TorProtocolService.jsm"
+);
+
+const { TorLauncherUtil } = ChromeUtils.import(
+    "resource://torlauncher/modules/tl-util.jsm"
+);
+
+/* Browser observer topis */
+const BrowserTopics = Object.freeze({
+    ProfileAfterChange: "profile-after-change",
+});
+
+/* tor-launcher observer topics */
+const TorTopics = Object.freeze({
+    ProcessIsReady: "TorProcessIsReady",
+    BootstrapStatus: "TorBootstrapStatus",
+    BootstrapError: "TorBootstrapError",
+    ProcessExited: "TorProcessExited",
+    LogHasWarnOrErr: "TorLogHasWarnOrErr",
+});
+
+/* Relevant prefs used by tor-launcher */
+const TorLauncherPrefs = Object.freeze({
+  quickstart: "extensions.torlauncher.quickstart",
+  prompt_at_startup: "extensions.torlauncher.prompt_at_startup",
+});
+
+const TorConnectState = Object.freeze({
+    /* Our initial state */
+    Initial: "Initial",
+    /* In-between initial boot and bootstrapping, users can change tor network settings during this state */
+    Configuring: "Configuring",
+    /* Geo-location and setting bridges/etc */
+    AutoConfiguring: "AutoConfiguring",
+    /* Tor is bootstrapping */
+    Bootstrapping: "Bootstrapping",
+    /* Passthrough state back to Configuring or Fatal */
+    Error: "Error",
+    /* An unrecoverable error */
+    FatalError: "FatalError",
+    /* Final state, after successful bootstrap */
+    Bootstrapped: "Bootstrapped",
+    /* If we are using System tor or the legacy Tor-Launcher */
+    Disabled: "Disabled",
+});
+
+/*
+
+                                               TorConnect State Transitions
+
+                                              ┌──────────────────────┐
+                                              │       Disabled       │
+                                              └──────────────────────┘
+                                                ▲
+                                                │ legacyOrSystemTor()
+                                                │
+                                              ┌──────────────────────┐
+                      ┌────────────────────── │       Initial        │ ───────────────────────────┐
+                      │                       └──────────────────────┘                            │
+                      │                         │                                                 │
+                      │                         │ beginBootstrap()                                │
+                      │                         ▼                                                 │
+┌────────────────┐    │  bootstrapComplete()  ┌────────────────────────────────────────────────┐  │  beginBootstrap()
+│  Bootstrapped  │ ◀──┼────────────────────── │                 Bootstrapping                  │ ◀┼─────────────────┐
+└────────────────┘    │                       └────────────────────────────────────────────────┘  │                 │
+                      │                         │                       ▲                    │    │                 │
+                      │                         │ cancelBootstrap()     │ beginBootstrap()   └────┼─────────────┐   │
+                      │                         ▼                       │                         │             │   │
+                      │   beginConfigure()    ┌────────────────────────────────────────────────┐  │             │   │
+                      └─────────────────────▶ │                                                │  │             │   │
+                                              │                                                │  │             │   │
+                       beginConfigure()       │                                                │  │             │   │
+                 ┌──────────────────────────▶ │                  Configuring                   │  │             │   │
+                 │                            │                                                │  │             │   │
+                 │                            │                                                │  │             │   │
+                 │    ┌─────────────────────▶ │                                                │  │             │   │
+                 │    │                       └────────────────────────────────────────────────┘  │             │   │
+                 │    │                         │                       │                         │             │   │
+                 │    │ cancelAutoconfigure()   │ autoConfigure()       │                    ┌────┼─────────────┼───┘
+                 │    │                         ▼                       │                    │    │             │
+                 │    │                       ┌──────────────────────┐  │                    │    │             │
+                 │    └────────────────────── │   AutoConfiguring    │ ─┼────────────────────┘    │             │
+                 │                            └──────────────────────┘  │                         │             │
+                 │                              │                       │                         │ onError()   │
+                 │                              │ onError()             │ onError()               │             │
+                 │                              ▼                       ▼                         │             │
+                 │                            ┌────────────────────────────────────────────────┐  │             │
+                 └─────────────────────────── │                     Error                      │ ◀┘             │
+                                              └────────────────────────────────────────────────┘                │
+                                                │                                            ▲   onError()      │
+                                                │ onFatalError()                             └──────────────────┘
+                                                ▼
+                                              ┌──────────────────────┐
+                                              │      FatalError      │
+                                              └──────────────────────┘
+
+*/
+
+
+/* Maps allowed state transitions
+   TorConnectStateTransitions[state] maps to an array of allowed states to transition to
+*/
+const TorConnectStateTransitions =
+    Object.freeze(new Map([
+        [TorConnectState.Initial,
+            [TorConnectState.Disabled,
+             TorConnectState.Bootstrapping,
+             TorConnectState.Configuring,
+             TorConnectState.Error]],
+        [TorConnectState.Configuring,
+            [TorConnectState.AutoConfiguring,
+             TorConnectState.Bootstrapping,
+             TorConnectState.Error]],
+        [TorConnectState.AutoConfiguring,
+            [TorConnectState.Configuring,
+             TorConnectState.Bootstrapping,
+             TorConnectState.Error]],
+        [TorConnectState.Bootstrapping,
+            [TorConnectState.Configuring,
+             TorConnectState.Bootstrapped,
+             TorConnectState.Error]],
+        [TorConnectState.Error,
+            [TorConnectState.Configuring,
+             TorConnectState.FatalError]],
+        // terminal states
+        [TorConnectState.FatalError, []],
+        [TorConnectState.Bootstrapped, []],
+        [TorConnectState.Disabled, []],
+    ]));
+
+/* Topics Notified by the TorConnect module */
+const TorConnectTopics = Object.freeze({
+    StateChange: "torconnect:state-change",
+    BootstrapProgress: "torconnect:bootstrap-progress",
+    BootstrapComplete: "torconnect:bootstrap-complete",
+    BootstrapError: "torconnect:bootstrap-error",
+    FatalError: "torconnect:fatal-error",
+});
+
+const TorConnect = (() => {
+    let retval = {
+
+        _state: TorConnectState.Initial,
+        _bootstrapProgress: 0,
+        _bootstrapStatus: null,
+        _errorMessage: null,
+        _errorDetails: null,
+        _logHasWarningOrError: false,
+
+        /* These functions are called after transitioning to a new state */
+        _transitionCallbacks: Object.freeze(new Map([
+            /* Initial is never transitioned to */
+            [TorConnectState.Initial, null],
+            /* Configuring */
+            [TorConnectState.Configuring, async (self, prevState) => {
+                // TODO move this to the transition function
+                if (prevState === TorConnectState.Bootstrapping) {
+                    await TorProtocolService.torStopBootstrap();
+                }
+            }],
+            /* AutoConfiguring */
+            [TorConnectState.AutoConfiguring, async (self, prevState) => {
+
+            }],
+            /* Bootstrapping */
+            [TorConnectState.Bootstrapping, async (self, prevState) => {
+                let error = await TorProtocolService.connect();
+                if (error) {
+                    self.onError(error.message, error.details);
+                } else {
+                    self._errorMessage = self._errorDetails = null;
+                }
+            }],
+            /* Bootstrapped */
+            [TorConnectState.Bootstrapped, async (self,prevState) => {
+                // notify observers of bootstrap completion
+                Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete);
+            }],
+            /* Error */
+            [TorConnectState.Error, async (self, prevState, errorMessage, errorDetails, fatal) => {
+                self._errorMessage = errorMessage;
+                self._errorDetails = errorDetails;
+
+                Services.obs.notifyObservers({message: errorMessage, details: errorDetails}, TorConnectTopics.BootstrapError);
+                if (fatal) {
+                    self.onFatalError();
+                } else {
+                    self.beginConfigure();
+                }
+            }],
+            /* FatalError */
+            [TorConnectState.FatalError, async (self, prevState) => {
+                Services.obs.notifyObservers(null, TorConnectTopics.FatalError);
+            }],
+            /* Disabled */
+            [TorConnectState.Disabled, (self, prevState) => {
+
+            }],
+        ])),
+
+        _changeState: async function(newState, ...args) {
+            const prevState = this._state;
+
+            // ensure this is a valid state transition
+            if (!TorConnectStateTransitions.get(prevState)?.includes(newState)) {
+                throw Error(`TorConnect: Attempted invalid state transition from ${prevState} to ${newState}`);
+            }
+
+            console.log(`TorConnect: transitioning state from ${prevState} to ${newState}`);
+
+            // set our new state first so that state transitions can themselves trigger
+            // a state transition
+            this._state = newState;
+
+            // call our transition function and forward any args
+            await this._transitionCallbacks.get(newState)(this, prevState, ...args);
+
+            Services.obs.notifyObservers({state: newState}, TorConnectTopics.StateChange);
+        },
+
+        // init should be called on app-startup in MainProcessingSingleton.jsm
+        init : function() {
+            console.log("TorConnect: Init");
+
+            // delay remaining init until after profile-after-change
+            Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange);
+        },
+
+        observe: async function(subject, topic, data) {
+            console.log(`TorConnect: observed ${topic}`);
+
+            switch(topic) {
+
+            /* Determine which state to move to from Initial */
+            case BrowserTopics.ProfileAfterChange: {
+                if (TorLauncherUtil.useLegacyLauncher || !TorProtocolService.ownsTorDaemon) {
+                    // Disabled
+                    this.legacyOrSystemTor();
+                } else {
+                    // register the Tor topics we always care about
+                    for (const topicKey in TorTopics) {
+                        const topic = TorTopics[topicKey];
+                        Services.obs.addObserver(this, topic);
+                        console.log(`TorConnect: observing topic '${topic}'`);
+                    }
+
+                    if (TorProtocolService.torProcessStatus == TorProcessStatus.Running) {
+                        if (this.shouldQuickStart) {
+                            // Quickstart
+                            this.beginBootstrap();
+                        } else {
+                            // Configuring
+                            this.beginConfigure();
+                        }
+                    }
+                }
+
+                Services.obs.removeObserver(this, topic);
+                break;
+            }
+            /* Transition out of Initial if Tor daemon wasn't running yet in BrowserTopics.ProfileAfterChange */
+            case TorTopics.ProcessIsReady: {
+                if (this.state === TorConnectState.Initial)
+                {
+                    if (this.shouldQuickStart) {
+                        // Quickstart
+                        this.beginBootstrap();
+                    } else {
+                        // Configuring
+                        this.beginConfigure();
+                    }
+                }
+                break;
+            }
+            /* Updates our bootstrap status */
+            case TorTopics.BootstrapStatus: {
+                if (this._state != TorConnectState.Bootstrapping) {
+                    console.log(`TorConnect: observed ${TorTopics.BootstrapStatus} topic while in state TorConnectState.${this._state}`);
+                    break;
+                }
+
+                const obj = subject?.wrappedJSObject;
+                if (obj) {
+                    this._bootstrapProgress= obj.PROGRESS;
+                    this._bootstrapStatus = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG");
+
+                    console.log(`TorConnect: Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`);
+                    Services.obs.notifyObservers({
+                        progress: this._bootstrapProgress,
+                        status: this._bootstrapStatus,
+                        hasWarnings: this._logHasWarningOrError
+                    }, TorConnectTopics.BootstrapProgress);
+
+                    if (this._bootstrapProgress === 100) {
+                        this.bootstrapComplete();
+                    }
+                }
+                break;
+            }
+            /* Handle bootstrap error*/
+            case TorTopics.BootstrapError: {
+                const obj = subject?.wrappedJSObject;
+                await TorProtocolService.torStopBootstrap();
+                this.onError(obj.message, obj.details);
+                break;
+            }
+            case TorTopics.LogHasWarnOrErr: {
+                this._logHasWarningOrError = true;
+                break;
+            }
+            default:
+                // ignore
+                break;
+            }
+        },
+
+        /*
+        Various getters
+        */
+
+        get shouldShowTorConnect() {
+                   // TorBrowser must control the daemon
+            return (TorProtocolService.ownsTorDaemon &&
+                   // and we're not using the legacy launcher
+                   !TorLauncherUtil.useLegacyLauncher &&
+                   // if we have succesfully bootstraped, then no need to show TorConnect
+                   this.state != TorConnectState.Bootstrapped);
+        },
+
+        get shouldQuickStart() {
+                   // quickstart must be enabled
+            return Services.prefs.getBoolPref(TorLauncherPrefs.quickstart, false) &&
+                   // and the previous bootstrap attempt must have succeeded
+                   !Services.prefs.getBoolPref(TorLauncherPrefs.prompt_at_startup, true);
+        },
+
+        get state() {
+            return this._state;
+        },
+
+        get bootstrapProgress() {
+            return this._bootstrapProgress;
+        },
+
+        get bootstrapStatus() {
+            return this._bootstrapStatus;
+        },
+
+        get errorMessage() {
+            return this._errorMessage;
+        },
+
+        get errorDetails() {
+            return this._errorDetails;
+        },
+
+        get logHasWarningOrError() {
+            return this._logHasWarningOrError;
+        },
+
+        /*
+        These functions tell TorConnect to transition states
+        */
+
+        legacyOrSystemTor: function() {
+            console.log("TorConnect: legacyOrSystemTor()");
+            this._changeState(TorConnectState.Disabled);
+        },
+
+        beginBootstrap: function() {
+            console.log("TorConnect: beginBootstrap()");
+            this._changeState(TorConnectState.Bootstrapping);
+        },
+
+        beginConfigure: function() {
+            console.log("TorConnect: beginConfigure()");
+            this._changeState(TorConnectState.Configuring);
+        },
+
+        autoConfigure: function() {
+            console.log("TorConnect: autoConfigure()");
+            // TODO: implement
+            throw Error("TorConnect: not implemented");
+        },
+
+        cancelAutoConfigure: function() {
+            console.log("TorConnect: cancelAutoConfigure()");
+            // TODO: implement
+            throw Error("TorConnect: not implemented");
+        },
+
+        cancelBootstrap: function() {
+            console.log("TorConnect: cancelBootstrap()");
+            this._changeState(TorConnectState.Configuring);
+        },
+
+        bootstrapComplete: function() {
+            console.log("TorConnect: bootstrapComplete()");
+            this._changeState(TorConnectState.Bootstrapped);
+        },
+
+        onError: function(message, details) {
+            console.log("TorConnect: onError()");
+            this._changeState(TorConnectState.Error, message, details, false);
+        },
+
+        onFatalError: function() {
+            console.log("TorConnect: onFatalError()");
+            // TODO: implement
+            throw Error("TorConnect: not implemented");
+        },
+
+        /*
+        Further external commands and helper methods
+        */
+        openTorPreferences: function() {
+            const win = BrowserWindowTracker.getTopWindow();
+            win.switchToTabHavingURI("about:preferences#tor", true);
+        },
+
+        openTorConnect: function() {
+            const win = BrowserWindowTracker.getTopWindow();
+            win.switchToTabHavingURI("about:torconnect", true, {ignoreQueryString: true});
+        },
+
+        copyTorLogs: function() {
+            // Copy tor log messages to the system clipboard.
+            const chSvc = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+              Ci.nsIClipboardHelper
+            );
+            const countObj = { value: 0 };
+            chSvc.copyString(TorProtocolService.getLog(countObj));
+            const count = countObj.value;
+            return TorLauncherUtil.getFormattedLocalizedString(
+              "copiedNLogMessagesShort",
+              [count],
+              1
+            );
+        },
+
+        // called from browser.js on browser startup, passed in either the user's homepage(s)
+        // or uris passed via command-line; we want to replace them with about:torconnect uris
+        // which redirect after bootstrapping
+        getURIsToLoad: function(uriVariant) {
+            // convert the object we get from browser.js
+            let uriStrings = ((v) => {
+                // an interop array
+                if (v instanceof Ci.nsIArray) {
+                    // Transform the nsIArray of nsISupportsString's into a JS Array of
+                    // JS strings.
+                    return Array.from(
+                      v.enumerate(Ci.nsISupportsString),
+                      supportStr => supportStr.data
+                    );
+                // an interop string
+                } else if (v instanceof Ci.nsISupportsString) {
+                    return [v.data];
+                // a js string
+                } else if (typeof v === "string") {
+                    return v.split("|");
+                // a js array of js strings
+                } else if (Array.isArray(v) &&
+                           v.reduce((allStrings, entry) => {return allStrings && (typeof entry === "string");}, true)) {
+                    return v;
+                }
+                // about:tor as safe fallback
+                console.log(`TorConnect: getURIsToLoad() received unknown variant '${JSON.stringify(v)}'`);
+                return ["about:tor"];
+            })(uriVariant);
+
+            // will attempt to convert user-supplied string to a uri, fallback to about:tor if cannot convert
+            // to valid uri object
+            let uriStringToUri = (uriString) => {
+                const fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE;
+                let uri = Services.uriFixup.getFixupURIInfo(uriString, fixupFlags)
+                  .preferredURI;
+                return uri ? uri : Services.io.newURI("about:tor");
+            };
+            let uris = uriStrings.map(uriStringToUri);
+
+            // assume we have a valid uri and generate an about:torconnect redirect uri
+            let uriToRedirectUri = (uri) => {
+                return`about:torconnect?redirect=${encodeURIComponent(uri.spec)}`;
+            };
+            let redirectUris = uris.map(uriToRedirectUri);
+
+            console.log(`TorConnect: will load after bootstrap => [${uris.map((uri) => {return uri.spec;}).join(", ")}]`);
+            return redirectUris;
+        },
+    };
+    retval.init();
+    return retval;
+})(); /* TorConnect */
diff --git a/browser/modules/TorProcessService.jsm b/browser/modules/TorProcessService.jsm
new file mode 100644
index 0000000000000..201e331b28066
--- /dev/null
+++ b/browser/modules/TorProcessService.jsm
@@ -0,0 +1,12 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProcessService"];
+
+var TorProcessService = {
+  get isBootstrapDone() {
+    const svc = Cc["@torproject.org/torlauncher-process-service;1"].getService(
+      Ci.nsISupports
+    ).wrappedJSObject;
+    return svc.mIsBootstrapDone;
+  },
+};
diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm
index 409d6be60b830..b8678fbca9aa0 100644
--- a/browser/modules/TorProtocolService.jsm
+++ b/browser/modules/TorProtocolService.jsm
@@ -1,21 +1,60 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
 "use strict";
 
-var EXPORTED_SYMBOLS = ["TorProtocolService"];
+var EXPORTED_SYMBOLS = ["TorProtocolService", "TorProcessStatus"];
 
-const { TorLauncherUtil } = ChromeUtils.import(
-  "resource://torlauncher/modules/tl-util.jsm"
+const { Services } = ChromeUtils.import(
+    "resource://gre/modules/Services.jsm"
 );
 
+// see tl-process.js
+const TorProcessStatus = Object.freeze({
+  Unknown: 0,
+  Starting: 1,
+  Running: 2,
+  Exited: 3,
+});
+
+/* Browser observer topis */
+const BrowserTopics = Object.freeze({
+    ProfileAfterChange: "profile-after-change",
+});
+
 var TorProtocolService = {
-  _tlps: Cc["@torproject.org/torlauncher-protocol-service;1"].getService(
-    Ci.nsISupports
-  ).wrappedJSObject,
+  _TorLauncherUtil: function() {
+      let { TorLauncherUtil } = ChromeUtils.import(
+        "resource://torlauncher/modules/tl-util.jsm"
+      );
+      return TorLauncherUtil;
+    }(),
+  _TorLauncherProtocolService: null,
+  _TorProcessService: null,
 
   // maintain a map of tor settings set by Tor Browser so that we don't
   // repeatedly set the same key/values over and over
   // this map contains string keys to primitive or array values
   _settingsCache: new Map(),
 
+  init() {
+    Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange);
+  },
+
+  observe(subject, topic, data) {
+    if (topic === BrowserTopics.ProfileAfterChange) {
+      // we have to delay init'ing this or else the crypto service inits too early without a profile
+      // which breaks the password manager
+      this._TorLauncherProtocolService = Cc["@torproject.org/torlauncher-protocol-service;1"].getService(
+        Ci.nsISupports
+      ).wrappedJSObject;
+      this._TorProcessService = Cc["@torproject.org/torlauncher-process-service;1"].getService(
+        Ci.nsISupports
+      ).wrappedJSObject,
+
+      Services.obs.removeObserver(this, topic);
+    }
+  },
+
   _typeof(aValue) {
     switch (typeof aValue) {
       case "boolean":
@@ -199,9 +238,9 @@ var TorProtocolService = {
     await this.sendCommand("SAVECONF");
   },
 
-  getLog() {
-    let countObj = { value: 0 };
-    let torLog = this._tlps.TorGetLog(countObj);
+  getLog(countObj) {
+    countObj = countObj || { value: 0 };
+    let torLog = this._TorLauncherProtocolService.TorGetLog(countObj);
     return torLog;
   },
 
@@ -321,3 +360,4 @@ var TorProtocolService = {
     return TorProcessStatus.Unknown;
   },
 };
+TorProtocolService.init();
\ No newline at end of file
diff --git a/browser/modules/TorStrings.jsm b/browser/modules/TorStrings.jsm
index cc4f6b340c5f4..73671c08693d9 100644
--- a/browser/modules/TorStrings.jsm
+++ b/browser/modules/TorStrings.jsm
@@ -261,6 +261,9 @@ var TorStrings = {
         "Tor Browser routes your traffic over the Tor Network, run by thousands of volunteers around the world."
       ),
       learnMore: getString("torPreferences.learnMore", "Learn More"),
+      quickstartHeading: getString("torPreferences.quickstart", "Quickstart"),
+      quickstartDescription: getString("torPreferences.quickstartDescription", "Quickstart allows Tor Browser to connect automatically."),
+      quickstartCheckbox : getString("torPreferences.quickstartCheckbox", "Always connect automatically"),
       bridgesHeading: getString("torPreferences.bridges", "Bridges"),
       bridgesDescription: getString(
         "torPreferences.bridgesDescription",
@@ -368,6 +371,83 @@ var TorStrings = {
     return retval;
   })() /* Tor Network Settings Strings */,
 
+  torConnect: (() => {
+    const tsbNetwork = new TorDTDStringBundle(
+      ["chrome://torlauncher/locale/network-settings.dtd"],
+      ""
+    );
+    const tsbLauncher = new TorPropertyStringBundle(
+      "chrome://torlauncher/locale/torlauncher.properties",
+      "torlauncher."
+    );
+    const tsbCommon = new TorPropertyStringBundle(
+      "chrome://global/locale/commonDialogs.properties",
+      ""
+    );
+
+    const getStringNet = tsbNetwork.getString.bind(tsbNetwork);
+    const getStringLauncher = tsbLauncher.getString.bind(tsbLauncher);
+    const getStringCommon = tsbCommon.getString.bind(tsbCommon);
+
+    return {
+      torConnect: getStringNet(
+        "torsettings.wizard.title.default",
+        "Connect to Tor"
+      ),
+
+      torConnecting: getStringNet(
+        "torsettings.wizard.title.connecting",
+        "Establishing a Connection"
+      ),
+
+      torNotConnectedConcise: getStringNet(
+        "torConnect.notConnectedConcise",
+        "Not Connected"
+      ),
+
+      torConnectingConcise: getStringNet(
+        "torConnect.connectingConcise",
+        "Connecting…"
+      ),
+
+      torBootstrapFailed: getStringLauncher(
+        "tor_bootstrap_failed",
+        "Tor failed to establish a Tor network connection."
+      ),
+
+      torConfigure: getStringNet(
+        "torsettings.wizard.title.configure",
+        "Tor Network Settings"
+      ),
+
+      copyLog: getStringNet(
+        "torConnect.copyLog",
+        "Copy Tor Logs"
+      ),
+
+      torConnectButton: getStringNet("torSettings.connect", "Connect"),
+
+      cancel: getStringCommon("Cancel", "Cancel"),
+
+      torConnected: getStringLauncher(
+        "torlauncher.bootstrapStatus.done",
+        "Connected to the Tor network"
+      ),
+
+      torConnectedConcise: getStringLauncher(
+        "torConnect.connectedConcise",
+        "Connected"
+      ),
+
+      tryAgain: getStringNet("torConnect.tryAgain", "Try connecting again"),
+      offline: getStringNet("torConnect.offline", "Offline"),
+
+      // tor connect strings for message box in about:preferences#tor
+      connectMessage: getStringNet("torConnect.connectMessage", "Changes to Tor Settings will not take effect until you connect to the Tor Network"),
+      tryAgainMessage: getStringNet("torConnect.tryAgainMessage", "Tor Browser has failed to establish a connection to the Tor Network"),
+    };
+  })(),
+
   /*
     Tor Onion Services Strings, e.g., for the authentication prompt.
   */
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index 1f7c6bc4c67e3..1ea57aba1a93d 100644
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -153,6 +153,8 @@ EXTRA_JS_MODULES += [
     "TabsList.jsm",
     "TabUnloader.jsm",
     "ThemeVariableMap.jsm",
+    'TorConnect.jsm',
+    'TorProcessService.jsm',
     "TorProtocolService.jsm",
     "TorStrings.jsm",
     "TransientPrefs.jsm",
diff --git a/browser/themes/shared/identity-block/identity-block.inc.css b/browser/themes/shared/identity-block/identity-block.inc.css
index a863d1d7d20e3..145ea27328460 100644
--- a/browser/themes/shared/identity-block/identity-block.inc.css
+++ b/browser/themes/shared/identity-block/identity-block.inc.css
@@ -53,16 +53,15 @@
   border-radius: var(--urlbar-icon-border-radius);
 }
 
-%ifdef MOZ_OFFICIAL_BRANDING
 #identity-box[pageproxystate="valid"].notSecureText #identity-icon-label,
 #identity-box[pageproxystate="valid"].chromeUI #identity-icon-label {
-  color: #420C5D;
+  color: var(--tor-branding-color);
+  opacity: 1;
 }
 
 toolbar[brighttext] #identity-box[pageproxystate="valid"].chromeUI #identity-icon-label {
   color: #CC80FF;
 }
-%endif
 
 #identity-box[pageproxystate="valid"].chromeUI #identity-icon-label,
 #identity-box[pageproxystate="valid"].extensionPage #identity-icon-label,
@@ -161,6 +160,8 @@ toolbar[brighttext] #identity-box[pageproxystate="valid"].chromeUI #identity-ico
 
 #identity-box[pageproxystate="valid"].chromeUI #identity-icon {
   list-style-image: url(chrome://branding/content/identity-icons-brand.svg);
+  fill: var(--tor-branding-color);
+  fill-opacity: 1;
 }
 
 #identity-box[pageproxystate="valid"].localResource #identity-icon {
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
index 3b11a9864cf86..b2e469b90aa80 100644
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -9,6 +9,8 @@
 
   skin/classic/browser/aboutNetError.css                       (../shared/aboutNetError.css)
   skin/classic/browser/offlineSupportPages.css                 (../shared/offlineSupportPages.css)
+  skin/classic/browser/onionPattern.css                        (../shared/onionPattern.css)
+  skin/classic/browser/onionPattern.svg                        (../shared/onionPattern.svg)
   skin/classic/browser/blockedSite.css                         (../shared/blockedSite.css)
   skin/classic/browser/error-pages.css                         (../shared/error-pages.css)
   skin/classic/browser/aboutRestartRequired.css                (../shared/aboutRestartRequired.css)
diff --git a/browser/themes/shared/onionPattern.css b/browser/themes/shared/onionPattern.css
new file mode 100644
index 0000000000000..1852350d57f73
--- /dev/null
+++ b/browser/themes/shared/onionPattern.css
@@ -0,0 +1,31 @@
+/* Onion pattern */
+
+.onion-pattern-container {
+
+  flex: auto;           /* grow to consume remaining space on the page */
+  display: flex;
+  margin: 0 auto;
+  width: 100%;
+  /* two onions tall, 4x the radius */
+  height: calc(4 * var(--onion-radius));
+  max-height: calc(4 * var(--onion-radius));
+  min-height: calc(4 * var(--onion-radius));
+  direction: ltr;
+}
+
+.onion-pattern-crop {
+  height: 100%;
+  width: 100%;
+
+  -moz-context-properties: fill;
+  fill: var(--onion-color, currentColor);
+  /* opacity of the entire div, not context-opacity */
+  opacity: var(--onion-opacity, 1);
+
+  background-image: url("chrome://browser/skin/onionPattern.svg");
+  background-repeat: repeat;
+  background-attachment: local;
+  background-position: center;
+  /* svg source is 6 onions wide and 2 onions tall */
+  background-size: calc(6 * 2 * var(--onion-radius)) calc(2 * 2 * var(--onion-radius));;
+}
\ No newline at end of file
diff --git a/browser/themes/shared/onionPattern.inc.xhtml b/browser/themes/shared/onionPattern.inc.xhtml
new file mode 100644
index 0000000000000..de57b6ee301a3
--- /dev/null
+++ b/browser/themes/shared/onionPattern.inc.xhtml
@@ -0,0 +1,12 @@
+<!--
+  Container div that holds onionPattern.svg
+  It is expected the includer of this xhtml file also includes onionPattern.css
+  and define the following vars:
+    onion-radius : radius of an onion
+    onion-color : the base color of the onion pattern
+    onion-opacity : the opacity of the entire repeating pattern
+-->
+
+<div class="onion-pattern-container">
+  <div class="onion-pattern-crop"/>
+</div>
\ No newline at end of file
diff --git a/browser/themes/shared/onionPattern.svg b/browser/themes/shared/onionPattern.svg
new file mode 100644
index 0000000000000..e2937b1753414
--- /dev/null
+++ b/browser/themes/shared/onionPattern.svg
@@ -0,0 +1,22 @@
+<svg fill="context-fill"  viewBox="0 0 900 300" width="900" height="300" xmlns="http://www.w3.org/2000/svg">
+    <g>
+        <path d="m825 0c41.421 0 75 33.5786 75 75 0 41.421-33.579 75-75 75z" fill-opacity=".3"/>
+        <path d="m750 0c41.421 0 75 33.5786 75 75 0 41.421-33.579 75-75 75z" fill-opacity=".15"/>
+        <path d="m525 225c0-41.421-33.579-75-75-75s-75 33.579-75 75z" fill-opacity=".3"/>
+        <path d="m525 300c0-41.421-33.579-75-75-75s-75 33.579-75 75z" fill-opacity=".15"/>
+        <path d="m300 0c0 41.4214-33.579 75-75 75s-75-33.5786-75-75z" fill-opacity=".3"/>
+        <path d="m300 75c0 41.421-33.579 75-75 75s-75-33.579-75-75z" fill-opacity=".15"/>
+        <g clip-rule="evenodd" fill-opacity=".3" fill-rule="evenodd">
+            <path d="m525 .25c-.176 0-.351.000606-.527.001817-.966.006671-1.744.795563-1.737 1.762033.006.96648.795 1.74455 1.762 1.73788.167-.00115.334-.00173.502-.00173s.335.00058.502.00173c.967.00667 1.756-.7714 1.762-1.73788.007-.96647-.771-1.755363-1.737-1.762033-.176-.001211-.351-.001817-.527-.001817zm7.849.407251c-.962-.100329-1.822.597609-1.923 1.558879-.1.96128.598 1.82188 1.559 1.92221.333.03473.665.07174.996.11103.96.11381 1.83-.57199 1.944-1.53176s-.572-1.830084-1.532-1.94389 [...]
+            <path d="m3.75 75c0-39.3503 31.8997-71.25 71.25-71.25 39.35 0 71.25 31.8997 71.25 71.25 0 39.35-31.9 71.25-71.25 71.25-39.3503 0-71.25-31.9-71.25-71.25zm71.25-74.75c-41.2833 0-74.75 33.4667-74.75 74.75 0 41.283 33.4667 74.75 74.75 74.75 41.283 0 74.75-33.467 74.75-74.75 0-41.2833-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.5137 24.7363-55.25 55.25-55.25 30.514 0 55.25 24.7363 55.25 55.25 0 30.514-24.736 55.25-55.25 55.25-30.5137 0-55.25-24.736-55.25-55.25zm55.25-58.75c-32.446 [...]
+            <path d="m228.75 225c0-39.35 31.9-71.25 71.25-71.25s71.25 31.9 71.25 71.25-31.9 71.25-71.25 71.25-71.25-31.9-71.25-71.25zm71.25-74.75c-41.283 0-74.75 33.467-74.75 74.75s33.467 74.75 74.75 74.75 74.75-33.467 74.75-74.75-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.514 24.736-55.25 55.25-55.25s55.25 24.736 55.25 55.25-24.736 55.25-55.25 55.25-55.25-24.736-55.25-55.25zm55.25-58.75c-32.447 0-58.75 26.303-58.75 58.75s26.303 58.75 58.75 58.75 58.75-26.303 58.75-58.75-26.303-58.75-58 [...]
+            <path d="m828.75 225c0-39.35 31.9-71.25 71.25-71.25v-3.5c-41.283 0-74.75 33.467-74.75 74.75s33.467 74.75 74.75 74.75v-3.5c-39.35 0-71.25-31.9-71.25-71.25zm16 0c0-30.514 24.736-55.25 55.25-55.25v-3.5c-32.447 0-58.75 26.303-58.75 58.75s26.303 58.75 58.75 58.75v-3.5c-30.514 0-55.25-24.736-55.25-55.25zm55.25-39.25c-21.677 0-39.25 17.573-39.25 39.25s17.573 39.25 39.25 39.25v3.5c-23.61 0-42.75-19.14-42.75-42.75s19.14-42.75 42.75-42.75zm-22.25 39.25c0-12.288 9.962-22.25 22.25-22.25v [...]
+            <path d="m71.25 225c0-39.35-31.8997-71.25-71.25-71.25v-3.5c41.2833 0 74.75 33.467 74.75 74.75s-33.4667 74.75-74.75 74.75v-3.5c39.3503 0 71.25-31.9 71.25-71.25zm-16 0c0-30.514-24.7363-55.25-55.25-55.25v-3.5c32.4467 0 58.75 26.303 58.75 58.75s-26.3033 58.75-58.75 58.75v-3.5c30.5137 0 55.25-24.736 55.25-55.25zm-55.25-39.25c21.6772 0 39.25 17.573 39.25 39.25s-17.5728 39.25-39.25 39.25v3.5c23.6102 0 42.75-19.14 42.75-42.75s-19.1398-42.75-42.75-42.75zm22.25 39.25c0-12.288-9.9617-22 [...]
+            <path d="m303.75 75c0-39.3503 31.9-71.25 71.25-71.25s71.25 31.8997 71.25 71.25c0 39.35-31.9 71.25-71.25 71.25s-71.25-31.9-71.25-71.25zm71.25-74.75c-41.283 0-74.75 33.4667-74.75 74.75 0 41.283 33.467 74.75 74.75 74.75s74.75-33.467 74.75-74.75c0-41.2833-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.5137 24.736-55.25 55.25-55.25s55.25 24.7363 55.25 55.25c0 30.514-24.736 55.25-55.25 55.25s-55.25-24.736-55.25-55.25zm55.25-58.75c-32.447 0-58.75 26.3033-58.75 58.75 0 32.447 26.303 58. [...]
+            <path d="m603.75 75c0-39.3503 31.9-71.25 71.25-71.25s71.25 31.8997 71.25 71.25c0 39.35-31.9 71.25-71.25 71.25s-71.25-31.9-71.25-71.25zm71.25-74.75c-41.283 0-74.75 33.4667-74.75 74.75 0 41.283 33.467 74.75 74.75 74.75s74.75-33.467 74.75-74.75c0-41.2833-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.5137 24.736-55.25 55.25-55.25s55.25 24.7363 55.25 55.25c0 30.514-24.736 55.25-55.25 55.25s-55.25-24.736-55.25-55.25zm55.25-58.75c-32.447 0-58.75 26.3033-58.75 58.75 0 32.447 26.303 58. [...]
+            <path d="m150 150.25c-.878 0-1.753.015-2.624.045-.966.034-1.722.844-1.689 1.81.033.965.843 1.721 1.809 1.688.831-.029 1.666-.043 2.504-.043s1.673.014 2.504.043c.966.033 1.776-.723 1.809-1.688.033-.966-.723-1.776-1.689-1.81-.871-.03-1.746-.045-2.624-.045zm-11.449 4.415c.954-.154 1.603-1.053 1.449-2.007s-1.053-1.603-2.007-1.449c-1.735.281-3.45.621-5.143 1.018-.941.221-1.525 1.163-1.304 2.104s1.163 1.524 2.104 1.303c1.613-.378 3.248-.702 4.901-.969zm23.456-3.456c-.954-.154-1.853 [...]
+            <path d="m750 150.25c-.878 0-1.753.015-2.624.045-.966.034-1.722.844-1.689 1.81.033.965.843 1.721 1.809 1.688.831-.029 1.666-.043 2.504-.043s1.673.014 2.504.043c.966.033 1.776-.723 1.809-1.688.033-.966-.723-1.776-1.689-1.81-.871-.03-1.746-.045-2.624-.045zm-11.449 4.415c.954-.154 1.603-1.053 1.449-2.007s-1.053-1.603-2.007-1.449c-1.735.281-3.45.621-5.143 1.018-.941.221-1.525 1.163-1.304 2.104s1.163 1.524 2.104 1.303c1.613-.378 3.248-.702 4.901-.969zm23.456-3.456c-.954-.154-1.853 [...]
+            <path d="m528.75 225c0-39.35 31.9-71.25 71.25-71.25s71.25 31.9 71.25 71.25-31.9 71.25-71.25 71.25-71.25-31.9-71.25-71.25zm71.25-74.75c-41.283 0-74.75 33.467-74.75 74.75s33.467 74.75 74.75 74.75 74.75-33.467 74.75-74.75-33.467-74.75-74.75-74.75zm-55.25 74.75c0-30.514 24.736-55.25 55.25-55.25s55.25 24.736 55.25 55.25-24.736 55.25-55.25 55.25-55.25-24.736-55.25-55.25zm55.25-58.75c-32.447 0-58.75 26.303-58.75 58.75s26.303 58.75 58.75 58.75 58.75-26.303 58.75-58.75-26.303-58.75-58 [...]
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/browser/themes/shared/urlbar-searchbar.inc.css b/browser/themes/shared/urlbar-searchbar.inc.css
index 0158597991ecb..d7dc7df17f19d 100644
--- a/browser/themes/shared/urlbar-searchbar.inc.css
+++ b/browser/themes/shared/urlbar-searchbar.inc.css
@@ -747,3 +747,5 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after {
 }
 
 %include ../../components/onionservices/content/onionlocation-urlbar.css
+%include ../../components/torconnect/content/torconnect-urlbar.css
+
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index 97a985fd557ac..1ce9472ebae2c 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -17168,9 +17168,56 @@ void Document::RemoveToplevelLoadingDocument(Document* aDoc) {
 
 StylePrefersColorScheme Document::PrefersColorScheme(
     IgnoreRFP aIgnoreRFP) const {
+
+  // tor-browser#27476
+  // should this document ignore resist finger-printing settings with regards to
+  // setting the color scheme
+  // currently only enabled for about:torconnect but we could expand to other non-
+  // SystemPrincipal pages if we wish
+  const auto documentUsesPreferredColorScheme = [](auto const* constDocument) -> bool {
+    if (auto* document = const_cast<Document*>(constDocument); document != nullptr) {
+      auto uri = document->GetDocBaseURI();
+
+      // try and extract out our prepath and filepath portions of the uri to C-strings
+      nsAutoCString prePathStr, filePathStr;
+      if(NS_FAILED(uri->GetPrePath(prePathStr)) ||
+         NS_FAILED(uri->GetFilePath(filePathStr))) {
+        return false;
+      }
+
+      // stick them in string view for easy comparisons
+      std::string_view prePath(prePathStr.get(), prePathStr.Length()),
+                       filePath(filePathStr.get(), filePathStr.Length());
+
+      // these about URIs will have the user's preferred color scheme exposed to them
+      // we can place other URIs here in the future if we wish
+      // see nsIURI.idl for URI part definitions
+      constexpr struct {
+        std::string_view prePath;
+        std::string_view filePath;
+      } allowedURIs[] = {
+        { "about:", "torconnect" },
+      };
+
+      // check each uri in the allow list against this document's uri
+      // verify the prepath and the file path match
+      for(auto const& uri : allowedURIs) {
+        if (prePath == uri.prePath &&
+            filePath == uri.filePath) {
+          // positive match means we can apply dark-mode to the page
+          return true;
+        }
+      }
+    }
+
+    // do not allow if no match or other error
+    return false;
+  };
+
   if (aIgnoreRFP == IgnoreRFP::No &&
-      nsContentUtils::ShouldResistFingerprinting(this)) {
-    return StylePrefersColorScheme::Light;
+      nsContentUtils::ShouldResistFingerprinting(this) &&
+      !documentUsesPreferredColorScheme(this)) {
+      return StylePrefersColorScheme::Light;
   }
 
   if (auto* bc = GetBrowsingContext()) {
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
index 4da5365f214d8..e981573e9822b 100644
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -6213,6 +6213,8 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) {
     NS_ENSURE_SUCCESS_VOID(rv);
 
     if (!StringBeginsWith(url, u"about:neterror"_ns) &&
+        // we want about:torconnect pages to be able to close themselves after bootstrap
+        !StringBeginsWith(url, u"about:torconnect"_ns) &&
         !mBrowsingContext->HadOriginalOpener() && !aTrustedCaller &&
         !IsOnlyTopLevelDocumentInSHistory()) {
       bool allowClose =
diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm
index ecdbf2a01d99a..62afa98e1ffc8 100644
--- a/toolkit/components/processsingleton/MainProcessSingleton.jsm
+++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm
@@ -24,6 +24,11 @@ MainProcessSingleton.prototype = {
           null
         );
 
+        ChromeUtils.import(
+          "resource:///modules/TorConnect.jsm",
+          null
+        );
+
         Services.ppmm.loadProcessScript(
           "chrome://global/content/process-content.js",
           true
diff --git a/toolkit/modules/AsyncPrefs.jsm b/toolkit/modules/AsyncPrefs.jsm
index 2834c484c9197..f7d867e47dc0d 100644
--- a/toolkit/modules/AsyncPrefs.jsm
+++ b/toolkit/modules/AsyncPrefs.jsm
@@ -20,6 +20,7 @@ const kAllowedPrefs = new Set([
 
   "browser.contentblocking.report.hide_vpn_banner",
   "browser.contentblocking.report.show_mobile_app",
+  "extensions.torlauncher.quickstart",
 
   "narrate.rate",
   "narrate.voice",
diff --git a/toolkit/modules/RemotePageAccessManager.jsm b/toolkit/modules/RemotePageAccessManager.jsm
index c12e71ac4d425..5125203866b80 100644
--- a/toolkit/modules/RemotePageAccessManager.jsm
+++ b/toolkit/modules/RemotePageAccessManager.jsm
@@ -103,6 +103,7 @@ let RemotePageAccessManager = {
       RPMGetInnerMostURI: ["*"],
       RPMGetHttpResponseHeader: ["*"],
       RPMGetTorStrings: ["*"],
+      RPMSendQuery: ["ShouldShowTorConnect"],
     },
     "about:plugins": {
       RPMSendQuery: ["RequestPlugins"],
@@ -219,6 +220,21 @@ let RemotePageAccessManager = {
         "FetchUpdateData",
       ],
     },
+    "about:torconnect": {
+      RPMAddMessageListener: [
+        "torconnect:state-change",
+      ],
+      RPMSendAsyncMessage: [
+        "torconnect:open-tor-preferences",
+        "torconnect:begin-bootstrap",
+        "torconnect:cancel-bootstrap",
+        "torconnect:set-quickstart",
+      ],
+      RPMSendQuery: [
+        "torconnect:get-init-args",
+        "torconnect:copy-tor-logs",
+      ],
+    },
   },
 
   /**
diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm
index f4f9259920275..f0a48d0216386 100644
--- a/toolkit/mozapps/update/UpdateService.jsm
+++ b/toolkit/mozapps/update/UpdateService.jsm
@@ -12,6 +12,17 @@ const { AppConstants } = ChromeUtils.import(
 const { AUSTLMY } = ChromeUtils.import(
   "resource://gre/modules/UpdateTelemetry.jsm"
 );
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource:///modules/TorProtocolService.jsm"
+);
+
+function _shouldRegisterBootstrapObserver(errorCode) {
+  return errorCode == PROXY_SERVER_CONNECTION_REFUSED &&
+         !TorProtocolService.isBootstrapDone() &&
+         TorProtocolService.ownsTorDaemon;
+};
+
 const {
   Bits,
   BitsRequest,
@@ -232,6 +243,7 @@ const SERVICE_ERRORS = [
 // Custom update error codes
 const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
 const NETWORK_ERROR_OFFLINE = 111;
+const PROXY_SERVER_CONNECTION_REFUSED = 2152398920;
 
 // Error codes should be < 1000. Errors above 1000 represent http status codes
 const HTTP_ERROR_OFFSET = 1000;
@@ -2676,6 +2688,9 @@ UpdateService.prototype = {
       case "network:offline-status-changed":
         this._offlineStatusChanged(data);
         break;
+      case "torconnect:bootstrap-complete":
+        this._bootstrapComplete();
+        break;
       case "nsPref:changed":
         if (data == PREF_APP_UPDATE_LOG || data == PREF_APP_UPDATE_LOG_FILE) {
           gLogEnabled; // Assigning this before it is lazy-loaded is an error.
@@ -3169,6 +3184,35 @@ UpdateService.prototype = {
     this._attemptResume();
   },
 
+  _registerBootstrapObserver: function AUS__registerBootstrapObserver() {
+    if (this._registeredBootstrapObserver) {
+      LOG(
+        "UpdateService:_registerBootstrapObserver - observer already registered"
+      );
+      return;
+    }
+
+    LOG(
+      "UpdateService:_registerBootstrapObserver - waiting for tor bootstrap to " +
+        "be complete, then forcing another check"
+    );
+
+    Services.obs.addObserver(this, "torconnect:bootstrap-complete");
+    this._registeredBootstrapObserver = true;
+  },
+
+  _bootstrapComplete: function AUS__bootstrapComplete() {
+    Services.obs.removeObserver(this, "torconnect:bootstrap-complete");
+    this._registeredBootstrapObserver = false;
+
+    LOG(
+      "UpdateService:_bootstrapComplete - bootstrapping complete, forcing " +
+        "another background check"
+    );
+
+    this._attemptResume();
+  },
+
   onCheckComplete: function AUS_onCheckComplete(request, updates) {
     this._selectAndInstallUpdate(updates);
   },
@@ -3188,6 +3232,11 @@ UpdateService.prototype = {
         AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE);
       }
       return;
+    } else if (_shouldRegisterBootstrapObserver(update.errorCode)) {
+      // Register boostrap observer to try again, but only when we own the
+      // tor process.
+      this._registerBootstrapObserver();
+      return;
     }
 
     // Send the error code to telemetry
@@ -6011,6 +6060,7 @@ Downloader.prototype = {
     var state = this._patch.state;
     var shouldShowPrompt = false;
     var shouldRegisterOnlineObserver = false;
+    var shouldRegisterBootstrapObserver = false;
     var shouldRetrySoon = false;
     var deleteActiveUpdate = false;
     let migratedToReadyUpdate = false;
@@ -6129,7 +6179,18 @@ Downloader.prototype = {
       );
       shouldRegisterOnlineObserver = true;
       deleteActiveUpdate = false;
-
+    } else if(_shouldRegisterBootstrapObserver(status)) {
+      // Register a bootstrap observer to try again.
+      // The bootstrap observer will continue the incremental download by
+      // calling downloadUpdate on the active update which continues
+      // downloading the file from where it was.
+      LOG("Downloader:onStopRequest - not bootstrapped, register bootstrap observer: true");
+      AUSTLMY.pingDownloadCode(
+        this.isCompleteUpdate,
+        AUSTLMY.DWNLD_RETRY_OFFLINE
+      );
+      shouldRegisterBootstrapObserver = true;
+      deleteActiveUpdate = false;
       // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED,
       // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned
       // when disconnecting the internet while a download of a MAR is in
@@ -6251,7 +6312,7 @@ Downloader.prototype = {
 
     // Only notify listeners about the stopped state if we
     // aren't handling an internal retry.
-    if (!shouldRetrySoon && !shouldRegisterOnlineObserver) {
+    if (!shouldRetrySoon && !shouldRegisterOnlineObserver && !shouldRegisterBootstrapObserver) {
       this.updateService.forEachDownloadListener(listener => {
         listener.onStopRequest(request, status);
       });
@@ -6437,6 +6498,9 @@ Downloader.prototype = {
     if (shouldRegisterOnlineObserver) {
       LOG("Downloader:onStopRequest - Registering online observer");
       this.updateService._registerOnlineObserver();
+    } else if (shouldRegisterBootstrapObserver) {
+      LOG("Downloader:onStopRequest - Registering bootstrap observer");
+      this.updateService._registerBootstrapObserver();
     } else if (shouldRetrySoon) {
       LOG("Downloader:onStopRequest - Retrying soon");
       this.updateService._consecutiveSocketErrors++;
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
index 2ff107b553b2a..f8fa83574df70 100644
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
@@ -70,6 +70,10 @@ function getGlobalScriptIncludes(scriptPath) {
     let match = line.match(globalScriptsRegExp);
     if (match) {
       let sourceFile = match[1]
+        .replace(
+          "chrome://browser/content/torconnect/",
+          "browser/components/torconnect/content/"
+        )
         .replace(
           "chrome://browser/content/search/",
           "browser/components/search/content/"

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the tor-commits mailing list