[tor-commits] [arm/master] Handling when tor needs root to start

atagar at torproject.org atagar at torproject.org
Tue Jul 12 21:36:33 UTC 2011


commit c801be076450e0ee1c80d02c3523d91f63ef6bfa
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Jul 11 12:44:37 2011 -0700

    Handling when tor needs root to start
    
    When connecting to privileged ports the tor process needs root permissions to
    start. If the torrc needs root then I shouldn't offer to start tor with arm,
    and the wizard makes a startup shell script rather than making a doomed
    attempt to start tor.
    
    Currently the startup script is just a stub - I'll need to wait until I have a
    connection to fill in all the shell scripting voodoo.
---
 src/cli/controller.py  |   59 +++++++++++++++++++++++++++++++++--------------
 src/cli/headerPanel.py |   11 +++++++-
 src/cli/wizard.py      |   37 +++++++++++++++++++----------
 src/resources/startTor |   14 +++++++++++
 src/util/torConfig.py  |   36 +++++++++++++++++++++++++++++
 5 files changed, 124 insertions(+), 33 deletions(-)

diff --git a/src/cli/controller.py b/src/cli/controller.py
index c6534b3..d4fcf2f 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -379,10 +379,10 @@ class Controller:
     with a slash and is created if it doesn't already exist.
     """
     
-    dataDir = CONFIG["startup.dataDirectory"]
+    dataDir = os.path.expanduser(CONFIG["startup.dataDirectory"])
     if not dataDir.endswith("/"): dataDir += "/"
     if not os.path.exists(dataDir): os.makedirs(dataDir)
-    return os.path.expanduser(dataDir)
+    return dataDir
   
   def getTorManager(self):
     """
@@ -458,10 +458,26 @@ class TorManager:
   
   def isTorrcAvailable(self):
     """
-    True if a wizard generated torrc exists, false otherwise.
+    True if a wizard generated torrc exists and the user has permissions to
+    run it, false otherwise.
     """
     
-    return os.path.exists(self.getTorrcPath())
+    torrcLoc = self.getTorrcPath()
+    if os.path.exists(torrcLoc):
+      # If we aren't running as root and would be trying to bind to low ports
+      # then the startup will fail due to permissons. Attempts to check for
+      # this in the torrc. If unable to read the torrc then we probably
+      # wouldn't be able to use it anyway with our permissions.
+      
+      if os.getuid() != 0:
+        try:
+          return not torConfig.isRootNeeded(torrcLoc)
+        except IOError, exc:
+          log.log(log.INFO, "Failed to read torrc at '%s': %s" % (torrcLoc, exc))
+          return False
+      else: return True
+    
+    return False
   
   def isManaged(self, conn):
     """
@@ -485,30 +501,37 @@ class TorManager:
     
     # attempts to connect for five seconds (tor might or might not be
     # immediately available)
-    torctlConn, authType, authValue = None, None, None
-    while not torctlConn and time.time() - startTime < 5:
+    raisedExc = None
+    
+    while time.time() - startTime < 5:
       try:
-        torctlConn, authType, authValue = TorCtl.preauth_connect(controlPort = int(CONFIG["wizard.default"]["Control"]))
-      except IOError: time.sleep(0.5)
+        self.connectManagedInstance()
+        return True
+      except IOError, exc:
+        raisedExc = exc
+        time.sleep(0.5)
+    
+    if raisedExc: log.log(log.WARN, str(raisedExc))
+    return False
+  
+  def connectManagedInstance(self):
+    """
+    Attempts to connect to a managed tor instance, raising an IOError if
+    unsuccessful.
+    """
+    
+    torctlConn, authType, authValue = TorCtl.preauth_connect(controlPort = int(CONFIG["wizard.default"]["Control"]))
     
     if not torctlConn:
       msg = "Unable to start tor, try running \"tor -f %s\" to see the error output" % torrcLoc
-      log.log(log.WARN, msg)
-      return False
+      raise IOError(msg)
     
     if authType == TorCtl.AUTH_TYPE.COOKIE:
       try:
         torctlConn.authenticate(authValue)
         torTools.getConn().init(torctlConn)
-        return True
       except Exception, exc:
-        msg = "Unable to connect to Tor: %s" % exc
-        log.log(log.WARN, msg)
-        return False
-    else:
-      msg = "Unable to connect to Tor, unexpected authentication type '%s'" % authType
-      log.log(log.WARN, msg)
-      return False
+        raise IOError("Unable to connect to Tor: %s" % exc)
 
 def shutdownDaemons():
   """
diff --git a/src/cli/headerPanel.py b/src/cli/headerPanel.py
index 5a2c1bc..f80eff8 100644
--- a/src/cli/headerPanel.py
+++ b/src/cli/headerPanel.py
@@ -22,6 +22,7 @@ import threading
 import TorCtl.TorCtl
 
 import cli.popups
+import cli.controller
 
 from util import log, panel, sysTools, torTools, uiTools
 
@@ -144,8 +145,14 @@ class HeaderPanel(panel.Panel, threading.Thread):
         log.log(log.NOTICE, "Reconnected to Tor's control port")
         cli.popups.showMsg("Tor reconnected", 1)
       except Exception, exc:
-        # displays notice for failed connection attempt
-        if exc.args: cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3)
+        # attempts to use the wizard port too
+        try:
+          cli.controller.getController().getTorManager().connectManagedInstance()
+          log.log(log.NOTICE, "Reconnected to Tor's control port")
+          cli.popups.showMsg("Tor reconnected", 1)
+        except:
+          # displays notice for the first failed connection attempt
+          if exc.args: cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3)
     else: isKeystrokeConsumed = False
     
     return isKeystrokeConsumed
diff --git a/src/cli/wizard.py b/src/cli/wizard.py
index efb6d98..be0deb4 100644
--- a/src/cli/wizard.py
+++ b/src/cli/wizard.py
@@ -341,27 +341,38 @@ def showWizard():
           torrcFile.write(generatedTorrc)
           torrcFile.close()
           
+          dataDir = cli.controller.getController().getDataDirectory()
+          
+          pathPrefix = os.path.dirname(sys.argv[0])
+          if pathPrefix and not pathPrefix.endswith("/"):
+            pathPrefix = pathPrefix + "/"
+          
           # copies exit notice into data directory if it's being used
           if Options.NOTICE in RelayOptions[relayType] and config[Options.NOTICE].getValue() and config[Options.LOWPORTS].getValue():
-            dataDir = cli.controller.getController().getDataDirectory()
-            
-            pathPrefix = os.path.dirname(sys.argv[0])
-            if pathPrefix and not pathPrefix.endswith("/"):
-              pathPrefix = pathPrefix + "/"
-            
             src = "%sresources/exitNotice" % pathPrefix
             dst = "%sexitNotice" % dataDir
             
             if not os.path.exists(dst):
               shutil.copytree(src, dst)
           
-          # If we're connected to a managed instance then just need to
-          # issue a sighup to pick up the new settings. Otherwise starts
-          # a new tor instance.
-          
-          conn = torTools.getConn()
-          if manager.isManaged(conn): conn.reload()
-          else: manager.startManagedInstance()
+          if manager.isTorrcAvailable():
+            # If we're connected to a managed instance then just need to
+            # issue a sighup to pick up the new settings. Otherwise starts
+            # a new tor instance.
+            
+            conn = torTools.getConn()
+            if manager.isManaged(conn): conn.reload()
+            else: manager.startManagedInstance()
+          else:
+            # If we don't have permissions to run the torrc we just made then
+            # makes a shell script they can run as root to start tor.
+            
+            src = "%sresources/startTor" % pathPrefix
+            dst = "%sstartTor" % dataDir
+            if not os.path.exists(dst): shutil.copy(src, dst)
+            
+            msg = "Tor needs root permissions to start with this configuration (it will drop itself to a 'tor-arm' user afterward). To continue...\n- open another terminal\n- run \"sudo %s\"\n- press 'r' here to tell arm to reconnect" % dst
+            log.log(log.NOTICE, msg)
           
           break
         elif confirmationSelection == CANCEL: break
diff --git a/src/resources/startTor b/src/resources/startTor
new file mode 100755
index 0000000..c575c23
--- /dev/null
+++ b/src/resources/startTor
@@ -0,0 +1,14 @@
+#!/bin/sh
+#
+# When binding to privilaged ports the tor process needs to start with root
+# permissions, then lower the user it's running as afterward. This script
+# simply makes a "tor-arm" user if it doesn't already exist then starts the
+# tor process.
+
+# TODO: check if the user's running as root
+# TODO: check if the tor-arm user exists and if not, make it
+# TODO: run arm
+# TODO: bonus points: double check that the torrc in this directory has a
+#       "User tor-arm" entry - this would be a problem if they run the wizard
+#       without low ports, then use this script
+
diff --git a/src/util/torConfig.py b/src/util/torConfig.py
index 8510c7a..2eca571 100644
--- a/src/util/torConfig.py
+++ b/src/util/torConfig.py
@@ -52,6 +52,9 @@ MAN_EX_INDENT = 15 # indentation used for man page examples
 PERSIST_ENTRY_DIVIDER = "-" * 80 + "\n" # splits config entries when saving to a file
 MULTILINE_PARAM = None # cached multiline parameters (lazily loaded)
 
+# torrc options that bind to ports
+PORT_OPT = ("SocksPort", "ORPort", "DirPort", "ControlPort", "TransPort")
+
 def loadConfig(config):
   config.update(CONFIG)
   
@@ -845,6 +848,39 @@ def _testConfigDescriptions():
     print "\"%s\"" % description
     if i != len(sortedOptions) - 1: print "-" * 80
 
+def isRootNeeded(torrcPath):
+  """
+  Returns True if the given torrc needs root permissions to be ran, False
+  otherwise. This raises an IOError if the torrc can't be read.
+  
+  Arguments:
+    torrcPath - torrc to be checked
+  """
+  
+  try:
+    torrcFile = open(torrcPath, "r")
+    torrcLines = torrcFile.readlines()
+    torrcFile.close()
+    
+    for line in torrcLines:
+      line = line.strip()
+      
+      isPortOpt = False
+      for opt in PORT_OPT:
+        if line.startswith(opt):
+          isPortOpt = True
+          break
+      
+      if isPortOpt and " " in line:
+        arg = line.split(" ")[1]
+        
+        if arg.isdigit() and int(arg) <= 1024 and int(arg) != 0:
+          return True
+    
+    return False
+  except Exception, exc:
+    raise IOError(exc)
+
 def renderTorrc(template, options, commentIndent = 30):
   """
   Uses the given template to generate a nicely formatted torrc with the given





More information about the tor-commits mailing list