[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