Ticket #9087: 0001-Move-network-tests-to-separate-Python-files.patch

File 0001-Move-network-tests-to-separate-Python-files.patch, 22.3 KB (added by cypherpunks, 3 years ago)
  • README

    From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
    From: cypherpunks <cypherpunks@torproject.org>
    Date: Fri, 1 Jul 2016 13:32:49 +0000
    Subject: [PATCH] Move network tests to separate Python files
    
    Based on a patch from chobe.
    
    Closes ticket 9087.
    ---
     README                            |   5 +
     chutney                           |   2 +-
     lib/chutney/TorNet.py             | 219 +++++---------------------------------
     scripts/chutney_tests/__init__.py |   0
     scripts/chutney_tests/verify.py   | 203 +++++++++++++++++++++++++++++++++++
     5 files changed, 233 insertions(+), 196 deletions(-)
     create mode 100644 scripts/chutney_tests/__init__.py
     create mode 100644 scripts/chutney_tests/verify.py
    
    diff --git a/README b/README
    index e0eb457..d11da52 100644
    a b The working files: 
    6969
    7070  You can override the directory "./net" with the CHUTNEY_DATA_DIR
    7171  environment variable.
     72
     73Test scripts:
     74  The test scripts are stored in the "scripts/chutney_tests" directory. These
     75  Python files must define a "run_test(network)" function. Files starting with
     76  an underscore ("_") are ignored.
  • chutney

    diff --git a/chutney b/chutney
    index 2076185..da8d97d 100755
    a b  
    33set -o errexit
    44set -o nounset
    55
    6 export PYTHONPATH="$(dirname "${0}")/lib:${PYTHONPATH-}"
     6export PYTHONPATH="$(dirname "${0}")/lib:$(dirname "${0}")/scripts:${PYTHONPATH-}"
    77
    88binaries="python2 python"
    99
  • lib/chutney/TorNet.py

    diff --git a/lib/chutney/TorNet.py b/lib/chutney/TorNet.py
    index a952944..87c5dab 100644
    a b import re 
    1919import errno
    2020import time
    2121import shutil
     22import importlib
    2223
    2324import chutney.Templating
    2425import chutney.Traffic
    class Network(object): 
    903904            for c in controllers:
    904905                c.check(listNonRunning=False)
    905906
    906     def verify(self):
    907         print("Verifying data transmission:")
    908         status = self._verify_traffic()
    909         print("Transmission: %s" % ("Success" if status else "Failure"))
    910         if not status:
    911             # TODO: allow the debug flag to be passed as an argument to
    912             # src/test/test-network.sh and chutney
    913             print("Set 'debug_flag = True' in Traffic.py to diagnose.")
    914         return status
    915 
    916     def _verify_traffic(self):
    917         """Verify (parts of) the network by sending traffic through it
    918         and verify what is received."""
    919         LISTEN_PORT = 4747  # FIXME: Do better! Note the default exit policy.
    920         # HSs must have a HiddenServiceDir with
    921         # "HiddenServicePort <HS_PORT> <CHUTNEY_LISTEN_ADDRESS>:<LISTEN_PORT>"
    922         HS_PORT = 5858
    923         # The amount of data to send between each source-sink pair,
    924         # each time the source connects.
    925         # We create a source-sink pair for each (bridge) client to an exit,
    926         # and a source-sink pair for a (bridge) client to each hidden service
    927         DATALEN = self._dfltEnv['data_bytes']
    928         # Print a dot each time a sink verifies this much data
    929         DOTDATALEN = 5 * 1024 * 1024  # Octets.
    930         TIMEOUT = 3                   # Seconds.
    931         # Calculate the amount of random data we should use
    932         randomlen = self._calculate_randomlen(DATALEN)
    933         reps = self._calculate_reps(DATALEN, randomlen)
    934         # sanity check
    935         if reps == 0:
    936             DATALEN = 0
    937         # Get the random data
    938         if randomlen > 0:
    939             # print a dot after every DOTDATALEN data is verified, rounding up
    940             dot_reps = self._calculate_reps(DOTDATALEN, randomlen)
    941             # make sure we get at least one dot per transmission
    942             dot_reps = min(reps, dot_reps)
    943             with open('/dev/urandom', 'r') as randfp:
    944                 tmpdata = randfp.read(randomlen)
    945         else:
    946             dot_reps = 0
    947             tmpdata = {}
    948         # now make the connections
    949         bind_to = (DEFAULTS['ip'], LISTEN_PORT)
    950         tt = chutney.Traffic.TrafficTester(bind_to, tmpdata, TIMEOUT, reps,
    951                                            dot_reps)
    952         client_list = filter(lambda n:
    953                              n._env['tag'] == 'c' or n._env['tag'] == 'bc',
    954                              self._nodes)
    955         exit_list = filter(lambda n:
    956                            ('exit' in n._env.keys()) and n._env['exit'] == 1,
    957                            self._nodes)
    958         hs_list = filter(lambda n:
    959                          n._env['tag'] == 'h',
    960                          self._nodes)
    961         if len(client_list) == 0:
    962             print("  Unable to verify network: no client nodes available")
    963             return False
    964         if len(exit_list) == 0 and len(hs_list) == 0:
    965             print("  Unable to verify network: no exit/hs nodes available")
    966             print("  Exit nodes must be declared 'relay=1, exit=1'")
    967             print("  HS nodes must be declared 'tag=\"hs\"'")
    968             return False
    969         print("Connecting:")
    970         # the number of tor nodes in paths which will send DATALEN data
    971         # if a node is used in two paths, we count it twice
    972         # this is a lower bound, as cannabilised circuits are one node longer
    973         total_path_node_count = 0
    974         total_path_node_count += self._configure_exits(tt, bind_to, tmpdata,
    975                                                        reps, client_list,
    976                                                        exit_list, LISTEN_PORT)
    977         total_path_node_count += self._configure_hs(tt, tmpdata, reps,
    978                                                     client_list, hs_list,
    979                                                     HS_PORT, LISTEN_PORT)
    980         print("Transmitting Data:")
    981         start_time = time.clock()
    982         status = tt.run()
    983         end_time = time.clock()
    984         # if we fail, don't report the bandwidth
    985         if not status:
    986             return status
    987         # otherwise, report bandwidth used, if sufficient data was transmitted
    988         self._report_bandwidth(DATALEN, total_path_node_count,
    989                                start_time, end_time)
    990         return status
    991 
    992     # In order to performance test a tor network, we need to transmit
    993     # several hundred megabytes of data or more. Passing around this
    994     # much data in Python has its own performance impacts, so we provide
    995     # a smaller amount of random data instead, and repeat it to DATALEN
    996     def _calculate_randomlen(self, datalen):
    997         MAX_RANDOMLEN = 128 * 1024   # Octets.
    998         if datalen > MAX_RANDOMLEN:
    999             return MAX_RANDOMLEN
    1000         else:
    1001             return datalen
    1002 
    1003     def _calculate_reps(self, datalen, replen):
    1004         # sanity checks
    1005         if datalen == 0 or replen == 0:
    1006             return 0
    1007         # effectively rounds datalen up to the nearest replen
    1008         if replen < datalen:
    1009             return (datalen + replen - 1) / replen
    1010         else:
    1011             return 1
    1012 
    1013     # if there are any exits, each client / bridge client transmits
    1014     # via 4 nodes (including the client) to an arbitrary exit
    1015     # Each client binds directly to <CHUTNEY_LISTEN_ADDRESS>:LISTEN_PORT
    1016     # via an Exit relay
    1017     def _configure_exits(self, tt, bind_to, tmpdata, reps, client_list,
    1018                          exit_list, LISTEN_PORT):
    1019         CLIENT_EXIT_PATH_NODES = 4
    1020         connection_count = self._dfltEnv['connection_count']
    1021         exit_path_node_count = 0
    1022         if len(exit_list) > 0:
    1023             exit_path_node_count += (len(client_list) *
    1024                                      CLIENT_EXIT_PATH_NODES *
    1025                                      connection_count)
    1026             for op in client_list:
    1027                 print("  Exit to %s:%d via client %s:%s"
    1028                       % (DEFAULTS['ip'], LISTEN_PORT,
    1029                          'localhost', op._env['socksport']))
    1030                 for i in range(connection_count):
    1031                     proxy = ('localhost', int(op._env['socksport']))
    1032                     tt.add(chutney.Traffic.Source(tt, bind_to, tmpdata, proxy,
    1033                                                   reps))
    1034         return exit_path_node_count
    1035 
    1036     # The HS redirects .onion connections made to hs_hostname:HS_PORT
    1037     # to the Traffic Tester's CHUTNEY_LISTEN_ADDRESS:LISTEN_PORT
    1038     # an arbitrary client / bridge client transmits via 8 nodes
    1039     # (including the client and hs) to each hidden service
    1040     # Instead of binding directly to LISTEN_PORT via an Exit relay,
    1041     # we bind to hs_hostname:HS_PORT via a hidden service connection
    1042     def _configure_hs(self, tt, tmpdata, reps, client_list, hs_list, HS_PORT,
    1043                       LISTEN_PORT):
    1044         CLIENT_HS_PATH_NODES = 8
    1045         connection_count = self._dfltEnv['connection_count']
    1046         hs_path_node_count = (len(hs_list) * CLIENT_HS_PATH_NODES *
    1047                               connection_count)
    1048         # Each client in hs_client_list connects to each hs
    1049         if self._dfltEnv['hs_multi_client']:
    1050             hs_client_list = client_list
    1051             hs_path_node_count *= len(client_list)
    1052         else:
    1053             # only use the first client in the list
    1054             hs_client_list = client_list[:1]
    1055         # Setup the connections from each client in hs_client_list to each hs
    1056         for hs in hs_list:
    1057             hs_bind_to = (hs._env['hs_hostname'], HS_PORT)
    1058             for client in hs_client_list:
    1059                 print("  HS to %s:%d (%s:%d) via client %s:%s"
    1060                       % (hs._env['hs_hostname'], HS_PORT,
    1061                          DEFAULTS['ip'], LISTEN_PORT,
    1062                          'localhost', client._env['socksport']))
    1063                 for i in range(connection_count):
    1064                     proxy = ('localhost', int(client._env['socksport']))
    1065                     tt.add(chutney.Traffic.Source(tt, hs_bind_to, tmpdata,
    1066                                                   proxy, reps))
    1067         return hs_path_node_count
    1068 
    1069     # calculate the single stream bandwidth and overall tor bandwidth
    1070     # the single stream bandwidth is the bandwidth of the
    1071     # slowest stream of all the simultaneously transmitted streams
    1072     # the overall bandwidth estimates the simultaneous bandwidth between
    1073     # all tor nodes over all simultaneous streams, assuming:
    1074     # * minimum path lengths (no cannibalized circuits)
    1075     # * unlimited network bandwidth (that is, localhost)
    1076     # * tor performance is CPU-limited
    1077     # This be used to estimate the bandwidth capacity of a CPU-bound
    1078     # tor relay running on this machine
    1079     def _report_bandwidth(self, data_length, total_path_node_count,
    1080                           start_time, end_time):
    1081         # otherwise, if we sent at least 5 MB cumulative total, and
    1082         # it took us at least a second to send, report bandwidth
    1083         MIN_BWDATA = 5 * 1024 * 1024  # Octets.
    1084         MIN_ELAPSED_TIME = 1.0        # Seconds.
    1085         cumulative_data_sent = total_path_node_count * data_length
    1086         elapsed_time = end_time - start_time
    1087         if (cumulative_data_sent >= MIN_BWDATA and
    1088                 elapsed_time >= MIN_ELAPSED_TIME):
    1089             # Report megabytes per second
    1090             BWDIVISOR = 1024*1024
    1091             single_stream_bandwidth = (data_length / elapsed_time / BWDIVISOR)
    1092             overall_bandwidth = (cumulative_data_sent / elapsed_time /
    1093                                  BWDIVISOR)
    1094             print("Single Stream Bandwidth: %.2f MBytes/s"
    1095                   % single_stream_bandwidth)
    1096             print("Overall tor Bandwidth: %.2f MBytes/s"
    1097                   % overall_bandwidth)
    1098 
    1099907
    1100908def ConfigureNodes(nodelist):
    1101909    network = _THE_NETWORK
    def ConfigureNodes(nodelist): 
    1106914            network._dfltEnv['hasbridgeauth'] = True
    1107915
    1108916
     917def getTests():
     918    tests = []
     919    for x in os.listdir("scripts/chutney_tests/"):
     920        if not x.startswith("_") and os.path.splitext(x)[1] == ".py":
     921            tests.append(os.path.splitext(x)[0])
     922    return tests
     923
     924
    1109925def usage(network):
    1110     return "\n".join(["Usage: chutney {command} {networkfile}",
     926    return "\n".join(["Usage: chutney {command/test} {networkfile}",
    1111927                      "Known commands are: %s" % (
    1112928                          " ".join(x for x in dir(network)
    1113                                    if not x.startswith("_")))])
     929                                   if not x.startswith("_"))),
     930                      "Known tests are: %s" % (
     931                          " ".join(getTests()))
     932                      ])
    1114933
    1115934
    1116935def exit_on_error(err_msg):
    def runConfigFile(verb, data): 
    1128947    exec(data, _GLOBALS)
    1129948    network = _GLOBALS['_THE_NETWORK']
    1130949
     950    # let's check if the verb is a valid test and run it
     951    if verb in getTests():
     952        test_module = importlib.import_module("chutney_tests.{}".format(verb))
     953        try:
     954            return test_module.run_test(network)
     955        except AttributeError:
     956            print("Test {!r} has no 'run_test(network)' function".format(verb))
     957            return False
     958
     959    # tell the user we don't know what their verb meant
    1131960    if not hasattr(network, verb):
    1132961        print(usage(network))
    1133962        print("Error: I don't know how to %s." % verb)
  • new file scripts/chutney_tests/verify.py

    diff --git a/scripts/chutney_tests/__init__.py b/scripts/chutney_tests/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/scripts/chutney_tests/verify.py b/scripts/chutney_tests/verify.py
    new file mode 100644
    index 0000000..480cf97
    - +  
     1import time
     2import chutney
     3
     4
     5def run_test(network):
     6    print("Verifying data transmission:")
     7    status = _verify_traffic(network)
     8    print("Transmission: %s" % ("Success" if status else "Failure"))
     9    if not status:
     10        # TODO: allow the debug flag to be passed as an argument to
     11        # src/test/test-network.sh and chutney
     12        print("Set 'debug_flag = True' in Traffic.py to diagnose.")
     13    return status
     14
     15
     16def _verify_traffic(network):
     17    """Verify (parts of) the network by sending traffic through it
     18    and verify what is received."""
     19    LISTEN_ADDR = network._dfltEnv['ip']
     20    LISTEN_PORT = 4747  # FIXME: Do better! Note the default exit policy.
     21    # HSs must have a HiddenServiceDir with
     22    # "HiddenServicePort <HS_PORT> <CHUTNEY_LISTEN_ADDRESS>:<LISTEN_PORT>"
     23    HS_PORT = 5858
     24    # The amount of data to send between each source-sink pair,
     25    # each time the source connects.
     26    # We create a source-sink pair for each (bridge) client to an exit,
     27    # and a source-sink pair for a (bridge) client to each hidden service
     28    DATALEN = network._dfltEnv['data_bytes']
     29    # Print a dot each time a sink verifies this much data
     30    DOTDATALEN = 5 * 1024 * 1024  # Octets.
     31    TIMEOUT = 3                   # Seconds.
     32    # Calculate the amount of random data we should use
     33    randomlen = _calculate_randomlen(DATALEN)
     34    reps = _calculate_reps(DATALEN, randomlen)
     35    connection_count = network._dfltEnv['connection_count']
     36    # sanity check
     37    if reps == 0:
     38        DATALEN = 0
     39    # Get the random data
     40    if randomlen > 0:
     41        # print a dot after every DOTDATALEN data is verified, rounding up
     42        dot_reps = _calculate_reps(DOTDATALEN, randomlen)
     43        # make sure we get at least one dot per transmission
     44        dot_reps = min(reps, dot_reps)
     45        with open('/dev/urandom', 'r') as randfp:
     46            tmpdata = randfp.read(randomlen)
     47    else:
     48        dot_reps = 0
     49        tmpdata = {}
     50    # now make the connections
     51    bind_to = (LISTEN_ADDR, LISTEN_PORT)
     52    tt = chutney.Traffic.TrafficTester(bind_to, tmpdata, TIMEOUT, reps,
     53                                       dot_reps)
     54    client_list = filter(lambda n:
     55                         n._env['tag'] == 'c' or n._env['tag'] == 'bc',
     56                         network._nodes)
     57    exit_list = filter(lambda n:
     58                       ('exit' in n._env.keys()) and n._env['exit'] == 1,
     59                       network._nodes)
     60    hs_list = filter(lambda n:
     61                     n._env['tag'] == 'h',
     62                     network._nodes)
     63    if len(client_list) == 0:
     64        print("  Unable to verify network: no client nodes available")
     65        return False
     66    if len(exit_list) == 0 and len(hs_list) == 0:
     67        print("  Unable to verify network: no exit/hs nodes available")
     68        print("  Exit nodes must be declared 'relay=1, exit=1'")
     69        print("  HS nodes must be declared 'tag=\"hs\"'")
     70        return False
     71    print("Connecting:")
     72    # the number of tor nodes in paths which will send DATALEN data
     73    # if a node is used in two paths, we count it twice
     74    # this is a lower bound, as cannabilised circuits are one node longer
     75    total_path_node_count = 0
     76    total_path_node_count += _configure_exits(tt, bind_to, tmpdata, reps,
     77                                              client_list, exit_list,
     78                                              LISTEN_ADDR, LISTEN_PORT,
     79                                              connection_count)
     80    total_path_node_count += _configure_hs(tt, tmpdata, reps, client_list,
     81                                           hs_list, HS_PORT, LISTEN_ADDR,
     82                                           LISTEN_PORT, connection_count,
     83                                           network._dfltEnv['hs_multi_client'])
     84    print("Transmitting Data:")
     85    start_time = time.clock()
     86    status = tt.run()
     87    end_time = time.clock()
     88    # if we fail, don't report the bandwidth
     89    if not status:
     90        return status
     91    # otherwise, report bandwidth used, if sufficient data was transmitted
     92    _report_bandwidth(DATALEN, total_path_node_count, start_time, end_time)
     93    return status
     94
     95
     96# In order to performance test a tor network, we need to transmit
     97# several hundred megabytes of data or more. Passing around this
     98# much data in Python has its own performance impacts, so we provide
     99# a smaller amount of random data instead, and repeat it to DATALEN
     100def _calculate_randomlen(datalen):
     101    MAX_RANDOMLEN = 128 * 1024   # Octets.
     102    if datalen > MAX_RANDOMLEN:
     103        return MAX_RANDOMLEN
     104    else:
     105        return datalen
     106
     107
     108def _calculate_reps(datalen, replen):
     109    # sanity checks
     110    if datalen == 0 or replen == 0:
     111        return 0
     112    # effectively rounds datalen up to the nearest replen
     113    if replen < datalen:
     114        return (datalen + replen - 1) / replen
     115    else:
     116        return 1
     117
     118
     119# if there are any exits, each client / bridge client transmits
     120# via 4 nodes (including the client) to an arbitrary exit
     121# Each client binds directly to <CHUTNEY_LISTEN_ADDRESS>:LISTEN_PORT
     122# via an Exit relay
     123def _configure_exits(tt, bind_to, tmpdata, reps, client_list, exit_list,
     124                     LISTEN_ADDR, LISTEN_PORT, connection_count):
     125    CLIENT_EXIT_PATH_NODES = 4
     126    exit_path_node_count = 0
     127    if len(exit_list) > 0:
     128        exit_path_node_count += (len(client_list) *
     129                                 CLIENT_EXIT_PATH_NODES *
     130                                 connection_count)
     131        for op in client_list:
     132            print("  Exit to %s:%d via client %s:%s"
     133                  % (LISTEN_ADDR, LISTEN_PORT,
     134                     'localhost', op._env['socksport']))
     135            for _ in range(connection_count):
     136                proxy = ('localhost', int(op._env['socksport']))
     137                tt.add(chutney.Traffic.Source(tt, bind_to, tmpdata, proxy,
     138                                              reps))
     139    return exit_path_node_count
     140
     141
     142# The HS redirects .onion connections made to hs_hostname:HS_PORT
     143# to the Traffic Tester's CHUTNEY_LISTEN_ADDRESS:LISTEN_PORT
     144# an arbitrary client / bridge client transmits via 8 nodes
     145# (including the client and hs) to each hidden service
     146# Instead of binding directly to LISTEN_PORT via an Exit relay,
     147# we bind to hs_hostname:HS_PORT via a hidden service connection
     148def _configure_hs(tt, tmpdata, reps, client_list, hs_list, HS_PORT,
     149                  LISTEN_ADDR, LISTEN_PORT, connection_count, hs_multi_client):
     150    CLIENT_HS_PATH_NODES = 8
     151    hs_path_node_count = (len(hs_list) * CLIENT_HS_PATH_NODES *
     152                          connection_count)
     153    # Each client in hs_client_list connects to each hs
     154    if hs_multi_client:
     155        hs_client_list = client_list
     156        hs_path_node_count *= len(client_list)
     157    else:
     158        # only use the first client in the list
     159        hs_client_list = client_list[:1]
     160    # Setup the connections from each client in hs_client_list to each hs
     161    for hs in hs_list:
     162        hs_bind_to = (hs._env['hs_hostname'], HS_PORT)
     163        for client in hs_client_list:
     164            print("  HS to %s:%d (%s:%d) via client %s:%s"
     165                  % (hs._env['hs_hostname'], HS_PORT,
     166                     LISTEN_ADDR, LISTEN_PORT,
     167                     'localhost', client._env['socksport']))
     168            for _ in range(connection_count):
     169                proxy = ('localhost', int(client._env['socksport']))
     170                tt.add(chutney.Traffic.Source(tt, hs_bind_to, tmpdata,
     171                                              proxy, reps))
     172    return hs_path_node_count
     173
     174
     175# calculate the single stream bandwidth and overall tor bandwidth
     176# the single stream bandwidth is the bandwidth of the
     177# slowest stream of all the simultaneously transmitted streams
     178# the overall bandwidth estimates the simultaneous bandwidth between
     179# all tor nodes over all simultaneous streams, assuming:
     180# * minimum path lengths (no cannibalized circuits)
     181# * unlimited network bandwidth (that is, localhost)
     182# * tor performance is CPU-limited
     183# This be used to estimate the bandwidth capacity of a CPU-bound
     184# tor relay running on this machine
     185def _report_bandwidth(data_length, total_path_node_count, start_time,
     186                      end_time):
     187    # otherwise, if we sent at least 5 MB cumulative total, and
     188    # it took us at least a second to send, report bandwidth
     189    MIN_BWDATA = 5 * 1024 * 1024  # Octets.
     190    MIN_ELAPSED_TIME = 1.0        # Seconds.
     191    cumulative_data_sent = total_path_node_count * data_length
     192    elapsed_time = end_time - start_time
     193    if (cumulative_data_sent >= MIN_BWDATA and
     194            elapsed_time >= MIN_ELAPSED_TIME):
     195        # Report megabytes per second
     196        BWDIVISOR = 1024*1024
     197        single_stream_bandwidth = (data_length / elapsed_time / BWDIVISOR)
     198        overall_bandwidth = (cumulative_data_sent / elapsed_time /
     199                             BWDIVISOR)
     200        print("Single Stream Bandwidth: %.2f MBytes/s"
     201              % single_stream_bandwidth)
     202        print("Overall tor Bandwidth: %.2f MBytes/s"
     203              % overall_bandwidth)