commit 14bb7b862174734fced8fc290d33ffb377d1df49
Author: cypherpunks <cypherpunks(a)torproject.org>
Date: Fri Jul 1 13:32:49 2016 +0000
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(-)
diff --git a/README b/README
index e0eb457..d11da52 100644
--- a/README
+++ b/README
@@ -69,3 +69,8 @@ The working files:
You can override the directory "./net" with the CHUTNEY_DATA_DIR
environment variable.
+
+Test scripts:
+ The test scripts are stored in the "scripts/chutney_tests" directory. These
+ Python files must define a "run_test(network)" function. Files starting with
+ an underscore ("_") are ignored.
diff --git a/chutney b/chutney
index 2076185..da8d97d 100755
--- a/chutney
+++ b/chutney
@@ -3,7 +3,7 @@
set -o errexit
set -o nounset
-export PYTHONPATH="$(dirname "${0}")/lib:${PYTHONPATH-}"
+export PYTHONPATH="$(dirname "${0}")/lib:$(dirname "${0}")/scripts:${PYTHONPATH-}"
binaries="python2 python"
diff --git a/lib/chutney/TorNet.py b/lib/chutney/TorNet.py
index a952944..87c5dab 100644
--- a/lib/chutney/TorNet.py
+++ b/lib/chutney/TorNet.py
@@ -19,6 +19,7 @@ import re
import errno
import time
import shutil
+import importlib
import chutney.Templating
import chutney.Traffic
@@ -903,199 +904,6 @@ class Network(object):
for c in controllers:
c.check(listNonRunning=False)
- def verify(self):
- print("Verifying data transmission:")
- status = self._verify_traffic()
- print("Transmission: %s" % ("Success" if status else "Failure"))
- if not status:
- # TODO: allow the debug flag to be passed as an argument to
- # src/test/test-network.sh and chutney
- print("Set 'debug_flag = True' in Traffic.py to diagnose.")
- return status
-
- def _verify_traffic(self):
- """Verify (parts of) the network by sending traffic through it
- and verify what is received."""
- LISTEN_PORT = 4747 # FIXME: Do better! Note the default exit policy.
- # HSs must have a HiddenServiceDir with
- # "HiddenServicePort <HS_PORT> <CHUTNEY_LISTEN_ADDRESS>:<LISTEN_PORT>"
- HS_PORT = 5858
- # The amount of data to send between each source-sink pair,
- # each time the source connects.
- # We create a source-sink pair for each (bridge) client to an exit,
- # and a source-sink pair for a (bridge) client to each hidden service
- DATALEN = self._dfltEnv['data_bytes']
- # Print a dot each time a sink verifies this much data
- DOTDATALEN = 5 * 1024 * 1024 # Octets.
- TIMEOUT = 3 # Seconds.
- # Calculate the amount of random data we should use
- randomlen = self._calculate_randomlen(DATALEN)
- reps = self._calculate_reps(DATALEN, randomlen)
- # sanity check
- if reps == 0:
- DATALEN = 0
- # Get the random data
- if randomlen > 0:
- # print a dot after every DOTDATALEN data is verified, rounding up
- dot_reps = self._calculate_reps(DOTDATALEN, randomlen)
- # make sure we get at least one dot per transmission
- dot_reps = min(reps, dot_reps)
- with open('/dev/urandom', 'r') as randfp:
- tmpdata = randfp.read(randomlen)
- else:
- dot_reps = 0
- tmpdata = {}
- # now make the connections
- bind_to = (DEFAULTS['ip'], LISTEN_PORT)
- tt = chutney.Traffic.TrafficTester(bind_to, tmpdata, TIMEOUT, reps,
- dot_reps)
- client_list = filter(lambda n:
- n._env['tag'] == 'c' or n._env['tag'] == 'bc',
- self._nodes)
- exit_list = filter(lambda n:
- ('exit' in n._env.keys()) and n._env['exit'] == 1,
- self._nodes)
- hs_list = filter(lambda n:
- n._env['tag'] == 'h',
- self._nodes)
- if len(client_list) == 0:
- print(" Unable to verify network: no client nodes available")
- return False
- if len(exit_list) == 0 and len(hs_list) == 0:
- print(" Unable to verify network: no exit/hs nodes available")
- print(" Exit nodes must be declared 'relay=1, exit=1'")
- print(" HS nodes must be declared 'tag=\"hs\"'")
- return False
- print("Connecting:")
- # the number of tor nodes in paths which will send DATALEN data
- # if a node is used in two paths, we count it twice
- # this is a lower bound, as cannabilised circuits are one node longer
- total_path_node_count = 0
- total_path_node_count += self._configure_exits(tt, bind_to, tmpdata,
- reps, client_list,
- exit_list, LISTEN_PORT)
- total_path_node_count += self._configure_hs(tt, tmpdata, reps,
- client_list, hs_list,
- HS_PORT, LISTEN_PORT)
- print("Transmitting Data:")
- start_time = time.clock()
- status = tt.run()
- end_time = time.clock()
- # if we fail, don't report the bandwidth
- if not status:
- return status
- # otherwise, report bandwidth used, if sufficient data was transmitted
- self._report_bandwidth(DATALEN, total_path_node_count,
- start_time, end_time)
- return status
-
- # In order to performance test a tor network, we need to transmit
- # several hundred megabytes of data or more. Passing around this
- # much data in Python has its own performance impacts, so we provide
- # a smaller amount of random data instead, and repeat it to DATALEN
- def _calculate_randomlen(self, datalen):
- MAX_RANDOMLEN = 128 * 1024 # Octets.
- if datalen > MAX_RANDOMLEN:
- return MAX_RANDOMLEN
- else:
- return datalen
-
- def _calculate_reps(self, datalen, replen):
- # sanity checks
- if datalen == 0 or replen == 0:
- return 0
- # effectively rounds datalen up to the nearest replen
- if replen < datalen:
- return (datalen + replen - 1) / replen
- else:
- return 1
-
- # if there are any exits, each client / bridge client transmits
- # via 4 nodes (including the client) to an arbitrary exit
- # Each client binds directly to <CHUTNEY_LISTEN_ADDRESS>:LISTEN_PORT
- # via an Exit relay
- def _configure_exits(self, tt, bind_to, tmpdata, reps, client_list,
- exit_list, LISTEN_PORT):
- CLIENT_EXIT_PATH_NODES = 4
- connection_count = self._dfltEnv['connection_count']
- exit_path_node_count = 0
- if len(exit_list) > 0:
- exit_path_node_count += (len(client_list) *
- CLIENT_EXIT_PATH_NODES *
- connection_count)
- for op in client_list:
- print(" Exit to %s:%d via client %s:%s"
- % (DEFAULTS['ip'], LISTEN_PORT,
- 'localhost', op._env['socksport']))
- for i in range(connection_count):
- proxy = ('localhost', int(op._env['socksport']))
- tt.add(chutney.Traffic.Source(tt, bind_to, tmpdata, proxy,
- reps))
- return exit_path_node_count
-
- # The HS redirects .onion connections made to hs_hostname:HS_PORT
- # to the Traffic Tester's CHUTNEY_LISTEN_ADDRESS:LISTEN_PORT
- # an arbitrary client / bridge client transmits via 8 nodes
- # (including the client and hs) to each hidden service
- # Instead of binding directly to LISTEN_PORT via an Exit relay,
- # we bind to hs_hostname:HS_PORT via a hidden service connection
- def _configure_hs(self, tt, tmpdata, reps, client_list, hs_list, HS_PORT,
- LISTEN_PORT):
- CLIENT_HS_PATH_NODES = 8
- connection_count = self._dfltEnv['connection_count']
- hs_path_node_count = (len(hs_list) * CLIENT_HS_PATH_NODES *
- connection_count)
- # Each client in hs_client_list connects to each hs
- if self._dfltEnv['hs_multi_client']:
- hs_client_list = client_list
- hs_path_node_count *= len(client_list)
- else:
- # only use the first client in the list
- hs_client_list = client_list[:1]
- # Setup the connections from each client in hs_client_list to each hs
- for hs in hs_list:
- hs_bind_to = (hs._env['hs_hostname'], HS_PORT)
- for client in hs_client_list:
- print(" HS to %s:%d (%s:%d) via client %s:%s"
- % (hs._env['hs_hostname'], HS_PORT,
- DEFAULTS['ip'], LISTEN_PORT,
- 'localhost', client._env['socksport']))
- for i in range(connection_count):
- proxy = ('localhost', int(client._env['socksport']))
- tt.add(chutney.Traffic.Source(tt, hs_bind_to, tmpdata,
- proxy, reps))
- return hs_path_node_count
-
- # calculate the single stream bandwidth and overall tor bandwidth
- # the single stream bandwidth is the bandwidth of the
- # slowest stream of all the simultaneously transmitted streams
- # the overall bandwidth estimates the simultaneous bandwidth between
- # all tor nodes over all simultaneous streams, assuming:
- # * minimum path lengths (no cannibalized circuits)
- # * unlimited network bandwidth (that is, localhost)
- # * tor performance is CPU-limited
- # This be used to estimate the bandwidth capacity of a CPU-bound
- # tor relay running on this machine
- def _report_bandwidth(self, data_length, total_path_node_count,
- start_time, end_time):
- # otherwise, if we sent at least 5 MB cumulative total, and
- # it took us at least a second to send, report bandwidth
- MIN_BWDATA = 5 * 1024 * 1024 # Octets.
- MIN_ELAPSED_TIME = 1.0 # Seconds.
- cumulative_data_sent = total_path_node_count * data_length
- elapsed_time = end_time - start_time
- if (cumulative_data_sent >= MIN_BWDATA and
- elapsed_time >= MIN_ELAPSED_TIME):
- # Report megabytes per second
- BWDIVISOR = 1024*1024
- single_stream_bandwidth = (data_length / elapsed_time / BWDIVISOR)
- overall_bandwidth = (cumulative_data_sent / elapsed_time /
- BWDIVISOR)
- print("Single Stream Bandwidth: %.2f MBytes/s"
- % single_stream_bandwidth)
- print("Overall tor Bandwidth: %.2f MBytes/s"
- % overall_bandwidth)
-
def ConfigureNodes(nodelist):
network = _THE_NETWORK
@@ -1106,11 +914,22 @@ def ConfigureNodes(nodelist):
network._dfltEnv['hasbridgeauth'] = True
+def getTests():
+ tests = []
+ for x in os.listdir("scripts/chutney_tests/"):
+ if not x.startswith("_") and os.path.splitext(x)[1] == ".py":
+ tests.append(os.path.splitext(x)[0])
+ return tests
+
+
def usage(network):
- return "\n".join(["Usage: chutney {command} {networkfile}",
+ return "\n".join(["Usage: chutney {command/test} {networkfile}",
"Known commands are: %s" % (
" ".join(x for x in dir(network)
- if not x.startswith("_")))])
+ if not x.startswith("_"))),
+ "Known tests are: %s" % (
+ " ".join(getTests()))
+ ])
def exit_on_error(err_msg):
@@ -1128,6 +947,16 @@ def runConfigFile(verb, data):
exec(data, _GLOBALS)
network = _GLOBALS['_THE_NETWORK']
+ # let's check if the verb is a valid test and run it
+ if verb in getTests():
+ test_module = importlib.import_module("chutney_tests.{}".format(verb))
+ try:
+ return test_module.run_test(network)
+ except AttributeError:
+ print("Test {!r} has no 'run_test(network)' function".format(verb))
+ return False
+
+ # tell the user we don't know what their verb meant
if not hasattr(network, verb):
print(usage(network))
print("Error: I don't know how to %s." % verb)
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
--- /dev/null
+++ b/scripts/chutney_tests/verify.py
@@ -0,0 +1,203 @@
+import time
+import chutney
+
+
+def run_test(network):
+ print("Verifying data transmission:")
+ status = _verify_traffic(network)
+ print("Transmission: %s" % ("Success" if status else "Failure"))
+ if not status:
+ # TODO: allow the debug flag to be passed as an argument to
+ # src/test/test-network.sh and chutney
+ print("Set 'debug_flag = True' in Traffic.py to diagnose.")
+ return status
+
+
+def _verify_traffic(network):
+ """Verify (parts of) the network by sending traffic through it
+ and verify what is received."""
+ LISTEN_ADDR = network._dfltEnv['ip']
+ LISTEN_PORT = 4747 # FIXME: Do better! Note the default exit policy.
+ # HSs must have a HiddenServiceDir with
+ # "HiddenServicePort <HS_PORT> <CHUTNEY_LISTEN_ADDRESS>:<LISTEN_PORT>"
+ HS_PORT = 5858
+ # The amount of data to send between each source-sink pair,
+ # each time the source connects.
+ # We create a source-sink pair for each (bridge) client to an exit,
+ # and a source-sink pair for a (bridge) client to each hidden service
+ DATALEN = network._dfltEnv['data_bytes']
+ # Print a dot each time a sink verifies this much data
+ DOTDATALEN = 5 * 1024 * 1024 # Octets.
+ TIMEOUT = 3 # Seconds.
+ # Calculate the amount of random data we should use
+ randomlen = _calculate_randomlen(DATALEN)
+ reps = _calculate_reps(DATALEN, randomlen)
+ connection_count = network._dfltEnv['connection_count']
+ # sanity check
+ if reps == 0:
+ DATALEN = 0
+ # Get the random data
+ if randomlen > 0:
+ # print a dot after every DOTDATALEN data is verified, rounding up
+ dot_reps = _calculate_reps(DOTDATALEN, randomlen)
+ # make sure we get at least one dot per transmission
+ dot_reps = min(reps, dot_reps)
+ with open('/dev/urandom', 'r') as randfp:
+ tmpdata = randfp.read(randomlen)
+ else:
+ dot_reps = 0
+ tmpdata = {}
+ # now make the connections
+ bind_to = (LISTEN_ADDR, LISTEN_PORT)
+ tt = chutney.Traffic.TrafficTester(bind_to, tmpdata, TIMEOUT, reps,
+ dot_reps)
+ client_list = filter(lambda n:
+ n._env['tag'] == 'c' or n._env['tag'] == 'bc',
+ network._nodes)
+ exit_list = filter(lambda n:
+ ('exit' in n._env.keys()) and n._env['exit'] == 1,
+ network._nodes)
+ hs_list = filter(lambda n:
+ n._env['tag'] == 'h',
+ network._nodes)
+ if len(client_list) == 0:
+ print(" Unable to verify network: no client nodes available")
+ return False
+ if len(exit_list) == 0 and len(hs_list) == 0:
+ print(" Unable to verify network: no exit/hs nodes available")
+ print(" Exit nodes must be declared 'relay=1, exit=1'")
+ print(" HS nodes must be declared 'tag=\"hs\"'")
+ return False
+ print("Connecting:")
+ # the number of tor nodes in paths which will send DATALEN data
+ # if a node is used in two paths, we count it twice
+ # this is a lower bound, as cannabilised circuits are one node longer
+ total_path_node_count = 0
+ total_path_node_count += _configure_exits(tt, bind_to, tmpdata, reps,
+ client_list, exit_list,
+ LISTEN_ADDR, LISTEN_PORT,
+ connection_count)
+ total_path_node_count += _configure_hs(tt, tmpdata, reps, client_list,
+ hs_list, HS_PORT, LISTEN_ADDR,
+ LISTEN_PORT, connection_count,
+ network._dfltEnv['hs_multi_client'])
+ print("Transmitting Data:")
+ start_time = time.clock()
+ status = tt.run()
+ end_time = time.clock()
+ # if we fail, don't report the bandwidth
+ if not status:
+ return status
+ # otherwise, report bandwidth used, if sufficient data was transmitted
+ _report_bandwidth(DATALEN, total_path_node_count, start_time, end_time)
+ return status
+
+
+# In order to performance test a tor network, we need to transmit
+# several hundred megabytes of data or more. Passing around this
+# much data in Python has its own performance impacts, so we provide
+# a smaller amount of random data instead, and repeat it to DATALEN
+def _calculate_randomlen(datalen):
+ MAX_RANDOMLEN = 128 * 1024 # Octets.
+ if datalen > MAX_RANDOMLEN:
+ return MAX_RANDOMLEN
+ else:
+ return datalen
+
+
+def _calculate_reps(datalen, replen):
+ # sanity checks
+ if datalen == 0 or replen == 0:
+ return 0
+ # effectively rounds datalen up to the nearest replen
+ if replen < datalen:
+ return (datalen + replen - 1) / replen
+ else:
+ return 1
+
+
+# if there are any exits, each client / bridge client transmits
+# via 4 nodes (including the client) to an arbitrary exit
+# Each client binds directly to <CHUTNEY_LISTEN_ADDRESS>:LISTEN_PORT
+# via an Exit relay
+def _configure_exits(tt, bind_to, tmpdata, reps, client_list, exit_list,
+ LISTEN_ADDR, LISTEN_PORT, connection_count):
+ CLIENT_EXIT_PATH_NODES = 4
+ exit_path_node_count = 0
+ if len(exit_list) > 0:
+ exit_path_node_count += (len(client_list) *
+ CLIENT_EXIT_PATH_NODES *
+ connection_count)
+ for op in client_list:
+ print(" Exit to %s:%d via client %s:%s"
+ % (LISTEN_ADDR, LISTEN_PORT,
+ 'localhost', op._env['socksport']))
+ for _ in range(connection_count):
+ proxy = ('localhost', int(op._env['socksport']))
+ tt.add(chutney.Traffic.Source(tt, bind_to, tmpdata, proxy,
+ reps))
+ return exit_path_node_count
+
+
+# The HS redirects .onion connections made to hs_hostname:HS_PORT
+# to the Traffic Tester's CHUTNEY_LISTEN_ADDRESS:LISTEN_PORT
+# an arbitrary client / bridge client transmits via 8 nodes
+# (including the client and hs) to each hidden service
+# Instead of binding directly to LISTEN_PORT via an Exit relay,
+# we bind to hs_hostname:HS_PORT via a hidden service connection
+def _configure_hs(tt, tmpdata, reps, client_list, hs_list, HS_PORT,
+ LISTEN_ADDR, LISTEN_PORT, connection_count, hs_multi_client):
+ CLIENT_HS_PATH_NODES = 8
+ hs_path_node_count = (len(hs_list) * CLIENT_HS_PATH_NODES *
+ connection_count)
+ # Each client in hs_client_list connects to each hs
+ if hs_multi_client:
+ hs_client_list = client_list
+ hs_path_node_count *= len(client_list)
+ else:
+ # only use the first client in the list
+ hs_client_list = client_list[:1]
+ # Setup the connections from each client in hs_client_list to each hs
+ for hs in hs_list:
+ hs_bind_to = (hs._env['hs_hostname'], HS_PORT)
+ for client in hs_client_list:
+ print(" HS to %s:%d (%s:%d) via client %s:%s"
+ % (hs._env['hs_hostname'], HS_PORT,
+ LISTEN_ADDR, LISTEN_PORT,
+ 'localhost', client._env['socksport']))
+ for _ in range(connection_count):
+ proxy = ('localhost', int(client._env['socksport']))
+ tt.add(chutney.Traffic.Source(tt, hs_bind_to, tmpdata,
+ proxy, reps))
+ return hs_path_node_count
+
+
+# calculate the single stream bandwidth and overall tor bandwidth
+# the single stream bandwidth is the bandwidth of the
+# slowest stream of all the simultaneously transmitted streams
+# the overall bandwidth estimates the simultaneous bandwidth between
+# all tor nodes over all simultaneous streams, assuming:
+# * minimum path lengths (no cannibalized circuits)
+# * unlimited network bandwidth (that is, localhost)
+# * tor performance is CPU-limited
+# This be used to estimate the bandwidth capacity of a CPU-bound
+# tor relay running on this machine
+def _report_bandwidth(data_length, total_path_node_count, start_time,
+ end_time):
+ # otherwise, if we sent at least 5 MB cumulative total, and
+ # it took us at least a second to send, report bandwidth
+ MIN_BWDATA = 5 * 1024 * 1024 # Octets.
+ MIN_ELAPSED_TIME = 1.0 # Seconds.
+ cumulative_data_sent = total_path_node_count * data_length
+ elapsed_time = end_time - start_time
+ if (cumulative_data_sent >= MIN_BWDATA and
+ elapsed_time >= MIN_ELAPSED_TIME):
+ # Report megabytes per second
+ BWDIVISOR = 1024*1024
+ single_stream_bandwidth = (data_length / elapsed_time / BWDIVISOR)
+ overall_bandwidth = (cumulative_data_sent / elapsed_time /
+ BWDIVISOR)
+ print("Single Stream Bandwidth: %.2f MBytes/s"
+ % single_stream_bandwidth)
+ print("Overall tor Bandwidth: %.2f MBytes/s"
+ % overall_bandwidth)