|
@@ -690,6 +690,14 @@ DEFAULTS = {
|
|
|
# Used when poll_launch_time is None, but RunAsDaemon is not set
|
|
|
# Set low so that we don't interfere with the voting interval
|
|
|
'poll_launch_time_default': 0.1,
|
|
|
+ # the number of bytes of random data we send on each connection
|
|
|
+ 'data_bytes': int(os.environ.get('CHUTNEY_DATA_BYTES', 10 * 1024)),
|
|
|
+ # the number of times each client will connect
|
|
|
+ 'connection_count': int(os.environ.get('CHUTNEY_CONNECTIONS', 1)),
|
|
|
+ # Do we want every client to connect to every HS, or one client
|
|
|
+ # to connect to each HS?
|
|
|
+ # (Clients choose an exit at random, so this doesn't apply to exits.)
|
|
|
+ 'hs_multi_client': int(os.environ.get('CHUTNEY_HS_MULTI_CLIENT', 0)),
|
|
|
}
|
|
|
|
|
|
|
|
@@ -908,46 +916,201 @@ class Network(object):
|
|
|
# HSs must have a HiddenServiceDir with
|
|
|
# "HiddenServicePort <HS_PORT> 127.0.0.1:<LISTEN_PORT>"
|
|
|
HS_PORT = 5858
|
|
|
- DATALEN = 10 * 1024 # Octets.
|
|
|
- TIMEOUT = 3 # Seconds.
|
|
|
- with open('/dev/urandom', 'r') as randfp:
|
|
|
- tmpdata = randfp.read(DATALEN)
|
|
|
+ # 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 = ('127.0.0.1', LISTEN_PORT)
|
|
|
- tt = chutney.Traffic.TrafficTester(bind_to, tmpdata, TIMEOUT)
|
|
|
+ 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
|
|
|
- # Each client binds directly to 127.0.0.1:LISTEN_PORT via an Exit relay
|
|
|
- for op in client_list:
|
|
|
- print(" Exit to %s:%d via client %s:%s"
|
|
|
- % ('127.0.0.1', LISTEN_PORT,
|
|
|
- 'localhost', op._env['socksport']))
|
|
|
- tt.add(chutney.Traffic.Source(tt, bind_to, tmpdata,
|
|
|
- ('localhost',
|
|
|
- int(op._env['socksport']))))
|
|
|
- # The HS redirects .onion connections made to hs_hostname:HS_PORT
|
|
|
- # to the Traffic Tester's 127.0.0.1:LISTEN_PORT
|
|
|
- # We must have at least one working client for the hs test to succeed
|
|
|
- for hs in filter(lambda n:
|
|
|
- n._env['tag'] == 'h',
|
|
|
- self._nodes):
|
|
|
- # Instead of binding directly to LISTEN_PORT via an Exit relay,
|
|
|
- # we bind to hs_hostname:HS_PORT via a hidden service connection
|
|
|
- # through the first available client
|
|
|
- bind_to = (hs._env['hs_hostname'], HS_PORT)
|
|
|
- # Just choose the first client
|
|
|
- client = client_list[0]
|
|
|
- print(" HS to %s:%d (%s:%d) via client %s:%s"
|
|
|
- % (hs._env['hs_hostname'], HS_PORT,
|
|
|
+ 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 127.0.0.1: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"
|
|
|
+ % ('127.0.0.1', LISTEN_PORT,
|
|
|
+ 'localhost', op._env['socksport']))
|
|
|
+ for i in range(connection_count):
|
|
|
+ tt.add(chutney.Traffic.Source(tt,
|
|
|
+ bind_to,
|
|
|
+ tmpdata,
|
|
|
+ ('localhost',
|
|
|
+ int(op._env['socksport'])),
|
|
|
+ reps))
|
|
|
+ return exit_path_node_count
|
|
|
+
|
|
|
+ # The HS redirects .onion connections made to hs_hostname:HS_PORT
|
|
|
+ # to the Traffic Tester's 127.0.0.1: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,
|
|
|
'127.0.0.1', LISTEN_PORT,
|
|
|
'localhost', client._env['socksport']))
|
|
|
- tt.add(chutney.Traffic.Source(tt, bind_to, tmpdata,
|
|
|
+ for i in range(connection_count):
|
|
|
+ tt.add(chutney.Traffic.Source(tt,
|
|
|
+ hs_bind_to,
|
|
|
+ tmpdata,
|
|
|
('localhost',
|
|
|
- int(client._env['socksport']))))
|
|
|
- return tt.run()
|
|
|
+ int(client._env['socksport'])),
|
|
|
+ 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):
|