commit cca2aae001bab0e6bb09e81a4e9df11bec2bc2bb Author: Damian Johnson atagar@torproject.org Date: Wed Aug 10 09:55:13 2011 -0700
Adding tor control socket support
This adds support for connecting via a tor control socket. Connection behavior is as follows...
- if an argument or armrc option for the control port or socket path are provided then just use that connection method - if neither or both args are provided then the default behavior is... - try connecting to the control socket (defaulting to /var/lib/tor/control) - try connecting to the control ports (defaulting to 9051 and 9052) - fall back to starting arm without a tor instance and show setup wizard
Control socket support is a feature request by Dererk on... https://trac.torproject.org/projects/tor/ticket/3638 --- armrc.sample | 5 ++- src/cli/headerPanel.py | 55 +++++++++++++++++--------- src/starter.py | 100 +++++++++++++++++++++++++++++++++++------------- src/util/torTools.py | 22 ++++++++++ 4 files changed, 135 insertions(+), 47 deletions(-)
diff --git a/armrc.sample b/armrc.sample index d547b6c..22fb903 100644 --- a/armrc.sample +++ b/armrc.sample @@ -2,6 +2,7 @@ startup.controlPassword startup.interface.ipAddress 127.0.0.1 startup.interface.port 9051 +startup.interface.socket /var/lib/tor/control startup.blindModeEnabled false startup.events N3 startup.dataDirectory ~/.arm @@ -66,7 +67,9 @@ features.refreshRate 5 features.confirmQuit true
# Allows arm to start when there's no running tor instance if true, otherwise -# we terminate right away. +# we terminate right away. This is ignored if the user provides an option +# specifying how to connect to tor (ie, a 'startup.interface.*' option or +# startup argument). features.allowDetachedStartup true
# Paremters for the log panel diff --git a/src/cli/headerPanel.py b/src/cli/headerPanel.py index 4e78f6c..d5ce332 100644 --- a/src/cli/headerPanel.py +++ b/src/cli/headerPanel.py @@ -21,6 +21,7 @@ import threading
import TorCtl.TorCtl
+import starter import cli.popups import cli.controller
@@ -40,6 +41,7 @@ VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "re
DEFAULT_CONFIG = {"startup.interface.ipAddress": "127.0.0.1", "startup.interface.port": 9051, + "startup.interface.socket": "/var/lib/tor/control", "features.showFdUsage": False, "log.fdUsageSixtyPercent": log.NOTICE, "log.fdUsageNinetyPercent": log.WARN} @@ -132,27 +134,42 @@ class HeaderPanel(panel.Panel, threading.Thread): if key in (ord('n'), ord('N')) and torTools.getConn().isNewnymAvailable(): self.sendNewnym() elif key in (ord('r'), ord('R')) and not self._isTorConnected: - try: - ctlAddr, ctlPort = self._config["startup.interface.ipAddress"], self._config["startup.interface.port"] - tmpConn, authType, authValue = TorCtl.TorCtl.preauth_connect(ctlAddr, ctlPort) - - if authType == TorCtl.TorCtl.AUTH_TYPE.PASSWORD: - authValue = cli.popups.inputPrompt("Controller Password: ") - if not authValue: raise IOError() # cancel reconnection - - tmpConn.authenticate(authValue) - torTools.getConn().init(tmpConn) + torctlConn = None + allowPortConnection, allowSocketConnection, _ = starter.allowConnectionTypes() + + if os.path.exists(self._config["startup.interface.socket"]) and allowSocketConnection: + try: torctlConn = torTools.connect_socket(self._config["startup.interface.socket"]) + except IOError, exc: + if not allowPortConnection: + cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3) + elif not allowPortConnection: + cli.popups.showMsg("Unable to reconnect (socket '%s' doesn't exist)" % self._config["startup.interface.socket"], 3) + + if not torctlConn and allowPortConnection: + try: + ctlAddr, ctlPort = self._config["startup.interface.ipAddress"], self._config["startup.interface.port"] + tmpConn, authType, authValue = TorCtl.TorCtl.preauth_connect(ctlAddr, ctlPort) + + if authType == TorCtl.TorCtl.AUTH_TYPE.PASSWORD: + authValue = cli.popups.inputPrompt("Controller Password: ") + if not authValue: raise IOError() # cancel reconnection + + tmpConn.authenticate(authValue) + torctlConn = tmpConn + except Exception, exc: + # 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) + + if torctlConn: + torTools.getConn().init(torctlConn) log.log(log.NOTICE, "Reconnected to Tor's control port") cli.popups.showMsg("Tor reconnected", 1) - except Exception, exc: - # 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/starter.py b/src/starter.py index 01acf24..a148947 100644 --- a/src/starter.py +++ b/src/starter.py @@ -35,6 +35,7 @@ DEFAULT_CONFIG = os.path.expanduser("~/.arm/armrc") CONFIG = {"startup.controlPassword": None, "startup.interface.ipAddress": "127.0.0.1", "startup.interface.port": 9051, + "startup.interface.socket": "/var/lib/tor/control", "startup.blindModeEnabled": False, "startup.events": "N3", "startup.dataDirectory": "~/.arm", @@ -52,14 +53,16 @@ CONFIG = {"startup.controlPassword": None, "log.configDescriptions.persistance.saveFailed": util.log.NOTICE, "log.savingDebugLog": util.log.NOTICE}
-OPT = "gi:c:dbe:vh" -OPT_EXPANDED = ["gui", "interface=", "config=", "debug", "blind", "event=", "version", "help"] +OPT = "gi:s:c:dbe:vh" +OPT_EXPANDED = ["gui", "interface=", "socket=", "config=", "debug", "blind", "event=", "version", "help"]
HELP_MSG = """Usage arm [OPTION] Terminal status monitor for Tor relays.
-g, --gui launch the Gtk+ interface -i, --interface [ADDRESS:]PORT change control interface from %s:%i + -s, --socket SOCKET_PATH attach using unix domain socket if present, + SOCKET_PATH defaults to: %s -c, --config CONFIG_PATH loaded configuration options, CONFIG_PATH defaults to: %s -d, --debug writes all arm logs to %s @@ -72,7 +75,7 @@ Terminal status monitor for Tor relays. Example: arm -b -i 1643 hide connection data, attaching to control port 1643 arm -e we -c /tmp/cfg use this configuration file with 'WARN'/'ERR' events -""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], DEFAULT_CONFIG, LOG_DUMP_PATH, CONFIG["startup.events"], cli.logPanel.EVENT_LISTING) +""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], CONFIG["startup.interface.socket"], DEFAULT_CONFIG, LOG_DUMP_PATH, CONFIG["startup.events"], cli.logPanel.EVENT_LISTING)
# filename used for cached tor config descriptions CONFIG_DESC_FILENAME = "torConfigDesc.txt" @@ -94,6 +97,29 @@ STANDARD_CFG_NOT_FOUND_MSG = "No configuration found at '%s', using defaults" # torrc entries that are scrubbed when dumping PRIVATE_TORRC_ENTRIES = ["HashedControlPassword", "Bridge", "HiddenServiceDir"]
+def allowConnectionTypes(): + """ + This provides a tuple with booleans indicating if we should or shouldn't + attempt to connect by various methods... + (allowPortConnection, allowSocketConnection, allowDetachedStart) + """ + + confKeys = util.conf.getConfig("arm").getKeys() + + isPortArgPresent = "startup.interface.ipAddress" in confKeys or "startup.interface.port" in confKeys + isSocketArgPresent = "startup.interface.socket" in confKeys + + skipPortConnection = isSocketArgPresent and not isPortArgPresent + skipSocketConnection = isPortArgPresent and not isSocketArgPresent + + # Flag to indicate if we'll start arm reguardless of being unable to connect + # to Tor. This is the default behavior if the user hasn't provided a port or + # socket to connect to, so we can show the relay setup wizard. + + allowDetachedStart = CONFIG["features.allowDetachedStartup"] and not isPortArgPresent and not isSocketArgPresent + + return (not skipPortConnection, not skipSocketConnection, allowDetachedStart) + def _loadConfigurationDescriptions(pathPrefix): """ Attempts to load descriptions for tor's configuration options, fetching them @@ -168,7 +194,7 @@ def _loadConfigurationDescriptions(pathPrefix): msg = DESC_INTERNAL_LOAD_FAILED_MSG % util.sysTools.getFileErrorMsg(exc) util.log.log(CONFIG["log.configDescriptions.internalLoadFailed"], msg)
-def _torCtlConnect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None, incorrectPasswordMsg=""): +def _torCtlConnect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None, incorrectPasswordMsg="", printError=True): """ Custom handler for establishing a TorCtl connection. """ @@ -222,7 +248,7 @@ def _torCtlConnect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None, i # again prompting for the user to enter it print incorrectPasswordMsg return _torCtlConnect(controlAddr, controlPort) - elif not CONFIG["features.allowDetachedStartup"]: + elif printError: print exc return None
@@ -300,6 +326,8 @@ if __name__ == '__main__':
param["startup.interface.ipAddress"] = controlAddr param["startup.interface.port"] = controlPort + elif opt in ("-s", "--socket"): + param["startup.interface.socket"] = arg elif opt in ("-g", "--gui"): launchGui = True elif opt in ("-c", "--config"): configPath = arg # sets path of user's config elif opt in ("-d", "--debug"): isDebugMode = True # dumps all logs @@ -361,7 +389,7 @@ if __name__ == '__main__': for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.procTools, util.sysTools, util.torConfig, util.torTools, util.uiTools): utilModule.loadConfig(config)
- # snycs config and parameters, saving changed config options and overwriting + # syncs config and parameters, saving changed config options and overwriting # undefined parameters with defaults for key in param.keys(): if param[key] == None: param[key] = CONFIG[key] @@ -389,28 +417,46 @@ if __name__ == '__main__': # temporarily disables TorCtl logging to prevent issues from going to stdout while starting TorCtl.TorUtil.loglevel = "NONE"
- # sets up TorCtl connection, prompting for the passphrase if necessary and - # sending problems to stdout if they arise - authPassword = config.get("startup.controlPassword", CONFIG["startup.controlPassword"]) - incorrectPasswordMsg = "Password found in '%s' was incorrect" % configPath - conn = _torCtlConnect(controlAddr, controlPort, authPassword, incorrectPasswordMsg) - if conn == None and not CONFIG["features.allowDetachedStartup"]: sys.exit(1) - - # removing references to the controller password so the memory can be freed - # (unfortunately python does allow for direct access to the memory so this - # is the best we can do) - del authPassword - if "startup.controlPassword" in config.contents: - del config.contents["startup.controlPassword"] - - pwLineNum = None - for i in range(len(config.rawContents)): - if config.rawContents[i].strip().startswith("startup.controlPassword"): - pwLineNum = i - break + # By default attempts to connect using the control socket if it exists. This + # skips attempting to connect by socket or port if the user has given + # arguments for connecting to the other. + + conn = None + allowPortConnection, allowSocketConnection, allowDetachedStart = allowConnectionTypes() + + socketPath = param["startup.interface.socket"] + if os.path.exists(socketPath) and allowSocketConnection: + try: conn = util.torTools.connect_socket(socketPath) + except IOError, exc: + if not allowPortConnection: + print "Unable to use socket '%s': %s" % (socketPath, exc) + elif not allowPortConnection: + print "Socket '%s' doesn't exist" % socketPath + + if not conn and allowPortConnection: + # sets up TorCtl connection, prompting for the passphrase if necessary and + # sending problems to stdout if they arise + authPassword = config.get("startup.controlPassword", CONFIG["startup.controlPassword"]) + incorrectPasswordMsg = "Password found in '%s' was incorrect" % configPath + conn = _torCtlConnect(controlAddr, controlPort, authPassword, incorrectPasswordMsg, not allowDetachedStart)
- if pwLineNum != None: - del config.rawContents[i] + # removing references to the controller password so the memory can be freed + # (unfortunately python does allow for direct access to the memory so this + # is the best we can do) + del authPassword + if "startup.controlPassword" in config.contents: + del config.contents["startup.controlPassword"] + + pwLineNum = None + for i in range(len(config.rawContents)): + if config.rawContents[i].strip().startswith("startup.controlPassword"): + pwLineNum = i + break + + if pwLineNum != None: + del config.rawContents[i] + + if conn == None and not allowDetachedStart: sys.exit(1)
# initializing the connection may require user input (for the password) # skewing the startup time results so this isn't counted diff --git a/src/util/torTools.py b/src/util/torTools.py index f81e10a..366deb0 100644 --- a/src/util/torTools.py +++ b/src/util/torTools.py @@ -111,6 +111,28 @@ IS_STARTUP_SIGNAL = True def loadConfig(config): config.update(CONFIG)
+# TODO: temporary code until this is added to torctl as part of... +# https://trac.torproject.org/projects/tor/ticket/3638 +def connect_socket(socketPath="/var/lib/tor/control", ConnClass=TorCtl.Connection): + """ + Connects to a unix domain socket available to controllers (set via tor's + ControlSocket option). This raises an IOError if unable to do so. + + Arguments: + socketPath - path of the socket to attach to + ConnClass - connection type to instantiate + """ + + import socket + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(socketPath) + conn = ConnClass(s) + conn.authenticate("") + return conn + except Exception, exc: + raise IOError(exc) + def getPid(controlPort=9051, pidFilePath=None): """ Attempts to determine the process id for a running tor process, using the
tor-commits@lists.torproject.org