commit 43d034bb1b00171f3d0607c7fcd7fd7c120f76e9 Author: Nick Mathewson nickm@torproject.org Date: Thu May 9 13:06:25 2019 -0400
Add a "supported" command that checks whether a network can work.
Right now a network is unsupported if it requires IPV6 and we don't have it, if the directory authorities don't actually have dirauth support, or if one of the binaries is missing. --- lib/chutney/TorNet.py | 139 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 10 deletions(-)
diff --git a/lib/chutney/TorNet.py b/lib/chutney/TorNet.py index a8be5bc..2f1b37b 100644 --- a/lib/chutney/TorNet.py +++ b/lib/chutney/TorNet.py @@ -23,6 +23,7 @@ import importlib
from chutney.Debug import debug_flag, debug
+import chutney.Host import chutney.Templating import chutney.Traffic import chutney.Util @@ -38,6 +39,9 @@ torrc_option_warn_count = 0 # Get verbose tracebacks, so we can diagnose better. cgitb.enable(format="plain")
+class MissingBinaryException(Exception): + pass + def getenv_int(envvar, default): """ Return the value of the environment variable 'envar' as an integer, @@ -132,11 +136,14 @@ def _warnMissingTor(tor_path, cmdline, tor_name="tor"): "containing {}.") .format(tor_name, tor_path, " ".join(cmdline), tor_name))
-def run_tor(cmdline): +def run_tor(cmdline, exit_on_missing=True): """Run the tor command line cmdline, which must start with the path or name of a tor binary.
Returns the combined stdout and stderr of the process. + + If exit_on_missing is true, warn and exit if the tor binary is missing. + Otherwise, raise a MissingBinaryException. """ if not debug_flag: cmdline.append("--quiet") @@ -149,20 +156,26 @@ def run_tor(cmdline): except OSError as e: # only catch file not found error if e.errno == errno.ENOENT: - _warnMissingTor(cmdline[0], cmdline) - sys.exit(1) + if exit_on_missing: + _warnMissingTor(cmdline[0], cmdline) + sys.exit(1) + else: + raise MissingBinaryException() else: raise except subprocess.CalledProcessError as e: # only catch file not found error if e.returncode == 127: - _warnMissingTor(cmdline[0], cmdline) - sys.exit(1) + if exit_on_missing: + _warnMissingTor(cmdline[0], cmdline) + sys.exit(1) + else: + raise MissingBinaryException() else: raise return stdouterr
-def launch_process(cmdline, tor_name="tor", stdin=None): +def launch_process(cmdline, tor_name="tor", stdin=None, exit_on_missing=True): """Launch the command line cmdline, which must start with the path or name of a binary. Use tor_name as the canonical name of the binary. Pass stdin to the Popen constructor. @@ -183,8 +196,11 @@ def launch_process(cmdline, tor_name="tor", stdin=None): except OSError as e: # only catch file not found error if e.errno == errno.ENOENT: - _warnMissingTor(cmdline[0], cmdline, tor_name=tor_name) - sys.exit(1) + if exit_on_missing: + _warnMissingTor(cmdline[0], cmdline, tor_name=tor_name) + sys.exit(1) + else: + raise MissingBinaryException() else: raise return p @@ -206,6 +222,25 @@ def run_tor_gencert(cmdline, passphrase): return stdouterr
@chutney.Util.memoized +def tor_exists(tor): + """Return true iff this tor binary exists.""" + try: + run_tor([tor, "--quiet", "--version"], exit_on_missing=False) + return True + except MissingBinaryException: + return False + +@chutney.Util.memoized +def tor_gencert_exists(gencert): + """Return true iff this tor-gencert binary exists.""" + try: + p = launch_process([gencert, "--help"], exit_on_missing=False) + p.wait() + return True + except MissingBinaryException: + return False + +@chutney.Util.memoized def get_tor_version(tor): """Return the version of the tor binary. Versions are cached for each unique tor path. @@ -240,6 +275,41 @@ def get_torrc_options(tor):
return torrc_opts
+@chutney.Util.memoized +def get_tor_modules(tor): + """Check the list of compile-time modules advertised by the given + 'tor' binary, and return a map from module name to a boolean + describing whether it is supported. + + Unlisted modules are ones that Tor did not treat as compile-time + optional modules. + """ + cmdline = [ + tor, + "--list-modules", + "--quiet" + ] + try: + mods = run_tor(cmdline) + except subprocess.CalledProcessError as e: + # Tor doesn't support --list-modules; act as if it said nothing. + mods = "" + + supported = {} + for line in mods.split("\n"): + m = re.match(r'^(\S+): (yes|no)', line) + if not m: + continue + supported[m.group(1)] = (m.group(2) == "yes") + + return supported + +def tor_has_module(tor, modname, default=True): + """Return true iff the given tor binary supports a given compile-time + module. If the module is not listed, return 'default'. + """ + return get_tor_modules(tor).get(modname, default) + class Node(object):
"""A Node represents a Tor node or a set of Tor nodes. It's created @@ -358,6 +428,11 @@ class NodeBuilder(_NodeCommon): """Called on each nodes after all nodes configure."""
+ def isSupported(self, net): + """Return true if this node appears to have everything it needs; + false otherwise.""" + + class NodeController(_NodeCommon):
"""Abstract base class. A NodeController is responsible for running a @@ -501,6 +576,21 @@ class LocalNodeBuilder(NodeBuilder): # self.net.addNode(self) pass
+ def isSupported(self, net): + """Return true if this node appears to have everything it needs; + false otherwise.""" + + if not tor_exists(self._env['tor']): + print("No binary found for %r"%self._env['tor']) + return False + + if self._env['authority']: + if not tor_has_module(self._env['tor'], "dirauth"): + print("No dirauth support in %r"%self._env['tor']) + return False + if not tor_gencert_exists(self._env['tor-gencert']): + print("No binary found for tor-gencert %r"%self._env['tor-gencrrt']) + def _makeDataDir(self): """Create the data directory (with keys subdirectory) for this node. """ @@ -1044,14 +1134,17 @@ class TorEnviron(chutney.Templating.Environ): dns_conf = TorEnviron.OFFLINE_DNS_RESOLV_CONF return "ServerDNSResolvConfFile %s" % (dns_conf)
+KNOWN_REQUIREMENTS = { + "IPV6": chutney.Host.is_ipv6_supported +}
class Network(object): - """A network of Tor nodes, plus functions to manipulate them """
def __init__(self, defaultEnviron): self._nodes = [] + self._requirements = [] self._dfltEnv = defaultEnviron self._nextnodenum = 0
@@ -1060,6 +1153,12 @@ class Network(object): self._nextnodenum += 1 self._nodes.append(n)
+ def _addRequirement(self, requirement): + requirement = requirement.upper() + if requirement not in KNOWN_REQUIREMENTS: + raise RuntimemeError(("Unrecognized requirement %r"%requirement)) + self._requirements.append(requirement) + def move_aside_nodes_dir(self): """Move aside the nodes directory, if it exists and is not a link. Used for backwards-compatibility only: nodes is created as a link to @@ -1120,6 +1219,22 @@ class Network(object): for n in self._nodes: n.getBuilder().checkConfig(self)
+ def supported(self): + """Check whether this network is supported by the set of binaries + and host information we have. + """ + missing_any = False + for r in self._requirements: + if not KNOWN_REQUIREMENTS[r](): + print(("Can't run this network: %s is missing.")) + missing_any = True + for n in self._nodes: + if not n.getBuilder().isSupported(self): + missing_any = False + + if missing_any: + sys.exit(1) + def configure(self): self.create_new_nodes_dir() network = self @@ -1236,6 +1351,10 @@ class Network(object): sys.stdout.flush()
+def Require(feature): + network = _THE_NETWORK + network._addRequirement(feature) + def ConfigureNodes(nodelist): network = _THE_NETWORK
@@ -1244,7 +1363,6 @@ def ConfigureNodes(nodelist): if n._env['bridgeauthority']: network._dfltEnv['hasbridgeauth'] = True
- def getTests(): tests = [] chutney_path = get_absolute_chutney_path() @@ -1275,6 +1393,7 @@ def exit_on_error(err_msg): def runConfigFile(verb, data): _GLOBALS = dict(_BASE_ENVIRON=_BASE_ENVIRON, Node=Node, + Require=Require, ConfigureNodes=ConfigureNodes, _THE_NETWORK=_THE_NETWORK, torrc_option_warn_count=0,