verify.py 9.8 KB

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