Browse Source

Merge remote-tracking branch 'tor-github/pr/32'

teor 4 years ago
parent
commit
ebaac29796

+ 20 - 0
lib/chutney/Host.py

@@ -0,0 +1,20 @@
+
+import socket
+import chutney.Util
+
+@chutney.Util.memoized
+def is_ipv6_supported():
+    """Return true iff ipv6 is supported on this host."""
+    try:
+        s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+        s.bind(("::1", 0))
+        s.listen(128)
+        a = s.getsockname()
+        s2 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+        s2.settimeout(1)
+        s2.connect(a)
+        return True
+    except socket.error:
+        return False
+
+

+ 152 - 50
lib/chutney/TorNet.py

@@ -23,8 +23,10 @@ import importlib
 
 
 from chutney.Debug import debug_flag, debug
 from chutney.Debug import debug_flag, debug
 
 
+import chutney.Host
 import chutney.Templating
 import chutney.Templating
 import chutney.Traffic
 import chutney.Traffic
+import chutney.Util
 
 
 _BASE_ENVIRON = None
 _BASE_ENVIRON = None
 _TOR_VERSIONS = None
 _TOR_VERSIONS = None
@@ -37,6 +39,9 @@ torrc_option_warn_count =  0
 # Get verbose tracebacks, so we can diagnose better.
 # Get verbose tracebacks, so we can diagnose better.
 cgitb.enable(format="plain")
 cgitb.enable(format="plain")
 
 
+class MissingBinaryException(Exception):
+    pass
+
 def getenv_int(envvar, default):
 def getenv_int(envvar, default):
     """
     """
        Return the value of the environment variable 'envar' as an integer,
        Return the value of the environment variable 'envar' as an integer,
@@ -131,11 +136,14 @@ def _warnMissingTor(tor_path, cmdline, tor_name="tor"):
            "containing {}.")
            "containing {}.")
           .format(tor_name, tor_path, " ".join(cmdline), tor_name))
           .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
     """Run the tor command line cmdline, which must start with the path or
        name of a tor binary.
        name of a tor binary.
 
 
        Returns the combined stdout and stderr of the process.
        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:
     if not debug_flag:
         cmdline.append("--quiet")
         cmdline.append("--quiet")
@@ -148,20 +156,26 @@ def run_tor(cmdline):
     except OSError as e:
     except OSError as e:
         # only catch file not found error
         # only catch file not found error
         if e.errno == errno.ENOENT:
         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:
         else:
             raise
             raise
     except subprocess.CalledProcessError as e:
     except subprocess.CalledProcessError as e:
         # only catch file not found error
         # only catch file not found error
         if e.returncode == 127:
         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:
         else:
             raise
             raise
     return stdouterr
     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
     """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.
        name of a binary. Use tor_name as the canonical name of the binary.
        Pass stdin to the Popen constructor.
        Pass stdin to the Popen constructor.
@@ -182,8 +196,11 @@ def launch_process(cmdline, tor_name="tor", stdin=None):
     except OSError as e:
     except OSError as e:
         # only catch file not found error
         # only catch file not found error
         if e.errno == errno.ENOENT:
         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:
         else:
             raise
             raise
     return p
     return p
@@ -204,49 +221,94 @@ def run_tor_gencert(cmdline, passphrase):
     assert empty_stderr is None
     assert empty_stderr is None
     return stdouterr
     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):
 def get_tor_version(tor):
     """Return the version of the tor binary.
     """Return the version of the tor binary.
        Versions are cached for each unique tor path.
        Versions are cached for each unique tor path.
     """
     """
-    # find the version of the current tor binary, and cache it
-    if tor not in _TOR_VERSIONS:
-        cmdline = [
-            tor,
-            "--version",
-        ]
-        tor_version = run_tor(cmdline)
-        # clean it up a bit
-        tor_version = tor_version.strip()
-        tor_version = tor_version.replace("version ", "")
-        tor_version = tor_version.replace(").", ")")
-        # check we received a tor version, and nothing else
-        assert re.match(r'^[-+.() A-Za-z0-9]+$', tor_version)
-        # cache the version for this tor binary's path
-        _TOR_VERSIONS[tor] = tor_version
-    else:
-        tor_version = _TOR_VERSIONS[tor]
+    cmdline = [
+        tor,
+        "--version",
+    ]
+    tor_version = run_tor(cmdline)
+    # clean it up a bit
+    tor_version = tor_version.strip()
+    tor_version = tor_version.replace("version ", "")
+    tor_version = tor_version.replace(").", ")")
+    # check we received a tor version, and nothing else
+    assert re.match(r'^[-+.() A-Za-z0-9]+$', tor_version)
+
     return tor_version
     return tor_version
 
 
+@chutney.Util.memoized
 def get_torrc_options(tor):
 def get_torrc_options(tor):
     """Return the torrc options supported by the tor binary.
     """Return the torrc options supported by the tor binary.
        Options are cached for each unique tor path.
        Options are cached for each unique tor path.
     """
     """
-    # find the options the current tor binary supports, and cache them
-    if tor not in _TORRC_OPTIONS:
-        cmdline = [
-            tor,
-            "--list-torrc-options",
-        ]
-        opts = run_tor(cmdline)
-        # check we received a list of options, and nothing else
-        assert re.match(r'(^\w+$)+', opts, flags=re.MULTILINE)
-        torrc_opts = opts.split()
-        # cache the options for this tor binary's path
-        _TORRC_OPTIONS[tor] = torrc_opts
-    else:
-        torrc_opts = _TORRC_OPTIONS[tor]
+    cmdline = [
+        tor,
+        "--list-torrc-options",
+    ]
+    opts = run_tor(cmdline)
+    # check we received a list of options, and nothing else
+    assert re.match(r'(^\w+$)+', opts, flags=re.MULTILINE)
+    torrc_opts = opts.split()
+
     return torrc_opts
     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):
 class Node(object):
 
 
@@ -366,6 +428,11 @@ class NodeBuilder(_NodeCommon):
         """Called on each nodes after all nodes configure."""
         """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):
 class NodeController(_NodeCommon):
 
 
     """Abstract base class.  A NodeController is responsible for running a
     """Abstract base class.  A NodeController is responsible for running a
@@ -509,6 +576,21 @@ class LocalNodeBuilder(NodeBuilder):
         # self.net.addNode(self)
         # self.net.addNode(self)
         pass
         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):
     def _makeDataDir(self):
         """Create the data directory (with keys subdirectory) for this node.
         """Create the data directory (with keys subdirectory) for this node.
         """
         """
@@ -1052,14 +1134,17 @@ class TorEnviron(chutney.Templating.Environ):
             dns_conf = TorEnviron.OFFLINE_DNS_RESOLV_CONF
             dns_conf = TorEnviron.OFFLINE_DNS_RESOLV_CONF
         return "ServerDNSResolvConfFile %s" % (dns_conf)
         return "ServerDNSResolvConfFile %s" % (dns_conf)
 
 
+KNOWN_REQUIREMENTS = {
+    "IPV6": chutney.Host.is_ipv6_supported
+}
 
 
 class Network(object):
 class Network(object):
-
     """A network of Tor nodes, plus functions to manipulate them
     """A network of Tor nodes, plus functions to manipulate them
     """
     """
 
 
     def __init__(self, defaultEnviron):
     def __init__(self, defaultEnviron):
         self._nodes = []
         self._nodes = []
+        self._requirements = []
         self._dfltEnv = defaultEnviron
         self._dfltEnv = defaultEnviron
         self._nextnodenum = 0
         self._nextnodenum = 0
 
 
@@ -1068,6 +1153,12 @@ class Network(object):
         self._nextnodenum += 1
         self._nextnodenum += 1
         self._nodes.append(n)
         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):
     def move_aside_nodes_dir(self):
         """Move aside the nodes directory, if it exists and is not a link.
         """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
         Used for backwards-compatibility only: nodes is created as a link to
@@ -1128,6 +1219,22 @@ class Network(object):
         for n in self._nodes:
         for n in self._nodes:
             n.getBuilder().checkConfig(self)
             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):
     def configure(self):
         self.create_new_nodes_dir()
         self.create_new_nodes_dir()
         network = self
         network = self
@@ -1244,6 +1351,10 @@ class Network(object):
                 sys.stdout.flush()
                 sys.stdout.flush()
 
 
 
 
+def Require(feature):
+    network = _THE_NETWORK
+    network._addRequirement(feature)
+
 def ConfigureNodes(nodelist):
 def ConfigureNodes(nodelist):
     network = _THE_NETWORK
     network = _THE_NETWORK
 
 
@@ -1252,7 +1363,6 @@ def ConfigureNodes(nodelist):
         if n._env['bridgeauthority']:
         if n._env['bridgeauthority']:
             network._dfltEnv['hasbridgeauth'] = True
             network._dfltEnv['hasbridgeauth'] = True
 
 
-
 def getTests():
 def getTests():
     tests = []
     tests = []
     chutney_path = get_absolute_chutney_path()
     chutney_path = get_absolute_chutney_path()
@@ -1283,6 +1393,7 @@ def exit_on_error(err_msg):
 def runConfigFile(verb, data):
 def runConfigFile(verb, data):
     _GLOBALS = dict(_BASE_ENVIRON=_BASE_ENVIRON,
     _GLOBALS = dict(_BASE_ENVIRON=_BASE_ENVIRON,
                     Node=Node,
                     Node=Node,
+                    Require=Require,
                     ConfigureNodes=ConfigureNodes,
                     ConfigureNodes=ConfigureNodes,
                     _THE_NETWORK=_THE_NETWORK,
                     _THE_NETWORK=_THE_NETWORK,
                     torrc_option_warn_count=0,
                     torrc_option_warn_count=0,
@@ -1319,17 +1430,8 @@ def parseArgs():
 
 
 def main():
 def main():
     global _BASE_ENVIRON
     global _BASE_ENVIRON
-    global _TOR_VERSIONS
-    global _TORRC_OPTIONS
     global _THE_NETWORK
     global _THE_NETWORK
     _BASE_ENVIRON = TorEnviron(chutney.Templating.Environ(**DEFAULTS))
     _BASE_ENVIRON = TorEnviron(chutney.Templating.Environ(**DEFAULTS))
-    # _TOR_VERSIONS gets initialised on demand as a map of
-    # "/path/to/tor" => "Tor version ..."
-    _TOR_VERSIONS = dict()
-    # _TORRC_OPTIONS gets initialised on demand as a map of
-    # "/path/to/tor" => ["SupportedOption1", "SupportedOption2", ...]
-    # Or it can be pre-populated as a static whitelist of options
-    _TORRC_OPTIONS = dict()
     _THE_NETWORK = Network(_BASE_ENVIRON)
     _THE_NETWORK = Network(_BASE_ENVIRON)
 
 
     args = parseArgs()
     args = parseArgs()

+ 13 - 0
lib/chutney/Util.py

@@ -0,0 +1,13 @@
+
+
+def memoized(fn):
+    """Decorator: memoize a function."""
+    memory = {}
+    def memoized_fn(*args, **kwargs):
+        key = (args, tuple(sorted(kwargs.items())))
+        try:
+            result = memory[key]
+        except KeyError:
+            result = memory[key] = fn(*args, **kwargs)
+        return result
+    return memoized_fn

+ 1 - 0
networks/bridges+ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")

+ 1 - 0
networks/bridges+ipv6+hs

@@ -1,3 +1,4 @@
+Require("IPV6")
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")

+ 1 - 0
networks/bridges+ipv6+hs-v2

@@ -1,3 +1,4 @@
+Require("IPV6")
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")

+ 1 - 0
networks/bridges+ipv6+hs-v23

@@ -1,3 +1,4 @@
+Require("IPV6")
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")

+ 1 - 0
networks/bridges+ipv6-min

@@ -1,3 +1,4 @@
+Require("IPV6")
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")
 ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay.tmpl")

+ 1 - 0
networks/client-ipv6-only

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/client-ipv6-only-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/hs-client-ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/hs-client-ipv6-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/hs-ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/hs-ipv6-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/hs-v23-ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/hs-v23-ipv6-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/hs-v3-ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/hs-v3-ipv6-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/ipv6-exit-min

@@ -1,3 +1,4 @@
+Require("IPV6")
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 Authority = Node(tag="a", authority=1, relay=1, torrc="authority.tmpl")
 IPv6ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay-exit-v6-only.tmpl")
 IPv6ExitRelay = Node(tag="r", relay=1, exit=1, torrc="relay-exit-v6-only.tmpl")

+ 1 - 0
networks/single-onion-client-ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/single-onion-client-ipv6-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/single-onion-ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/single-onion-ipv6-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/single-onion-v23-ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/single-onion-v23-ipv6-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/single-onion-v3-ipv6

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 1 - 0
networks/single-onion-v3-ipv6-md

@@ -1,3 +1,4 @@
+Require("IPV6")
 import os
 import os
 # By default, Authorities are not configured as exits
 # By default, Authorities are not configured as exits
 Authority6 = Node(tag="a", authority=1, relay=1,
 Authority6 = Node(tag="a", authority=1, relay=1,

+ 5 - 0
tools/bootstrap-network.sh

@@ -53,6 +53,11 @@ export CHUTNEY_NETWORK="$CHUTNEY_PATH/networks/$NETWORK_FLAVOUR"
 
 
 "$CHUTNEY" stop "$CHUTNEY_NETWORK"
 "$CHUTNEY" stop "$CHUTNEY_NETWORK"
 
 
+if ! "$CHUTNEY" supported "$CHUTNEY_NETWORK"; then
+    echo "%myname: network not supported."
+    exit 77
+fi
+
 echo "$myname: bootstrapping network: $NETWORK_FLAVOUR"
 echo "$myname: bootstrapping network: $NETWORK_FLAVOUR"
 "$CHUTNEY" configure "$CHUTNEY_NETWORK"
 "$CHUTNEY" configure "$CHUTNEY_NETWORK"
 
 

+ 4 - 0
tools/test-network-impl.sh

@@ -1,6 +1,10 @@
 #!/bin/sh
 #!/bin/sh
 
 
 if ! "$CHUTNEY_PATH/tools/bootstrap-network.sh" "$NETWORK_FLAVOUR"; then
 if ! "$CHUTNEY_PATH/tools/bootstrap-network.sh" "$NETWORK_FLAVOUR"; then
+    if test "$?" = 77; then
+	$ECHO "SKIP: $NETWORK_FLAVOR not supported."
+	exit 77
+    fi
     CHUTNEY_WARNINGS_IGNORE_EXPECTED=false CHUTNEY_WARNINGS_SUMMARY=false \
     CHUTNEY_WARNINGS_IGNORE_EXPECTED=false CHUTNEY_WARNINGS_SUMMARY=false \
         "$WARNING_COMMAND"
         "$WARNING_COMMAND"
     "$WARNINGS"
     "$WARNINGS"

+ 3 - 0
tools/test-network.sh

@@ -351,6 +351,9 @@ while [ "$n_attempts" -lt "$max_attempts" ]; do
 	$ECHO "==== Chutney succeeded after $n_attempts attempt(s)."
 	$ECHO "==== Chutney succeeded after $n_attempts attempt(s)."
 	exit 0
 	exit 0
     fi
     fi
+    if test "$?" = 77; then
+	exit 77
+    fi
 done
 done
 
 
 $ECHO "Chutney failed $n_attempts times; we may have a problem here."
 $ECHO "Chutney failed $n_attempts times; we may have a problem here."