commit c801be076450e0ee1c80d02c3523d91f63ef6bfa Author: Damian Johnson atagar@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