simulator.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. #!/usr/bin/env python3
  2. import random # For simulation, not cryptography!
  3. import math
  4. import sys
  5. import os
  6. import logging
  7. import resource
  8. import network
  9. import dirauth
  10. import relay
  11. import client
  12. class Simulator:
  13. def __init__(self, relaytarget, clienttarget, statslogger):
  14. self.relaytarget = relaytarget
  15. self.clienttarget = clienttarget
  16. self.statslogger = statslogger
  17. # Some (for now) hard-coded parameters
  18. # The number of directory authorities
  19. numdirauths = 9
  20. # The fraction of relays that are fallback relays
  21. # Taken from the live network in Jan 2020
  22. fracfallbackrelays = 0.023
  23. # Mean number of circuits created per client per epoch
  24. self.gamma = 8.9
  25. # Churn is controlled by three parameters:
  26. # newmean: the mean number of new arrivals per epoch
  27. # newstddev: the stddev number of new arrivals per epoch
  28. # oldprob: the probability any given existing one leaves per epoch
  29. # If target is the desired steady state number, then it should
  30. # be the case that target * oldprob = newmean. That way, if the
  31. # current number is below target, on average you add more than
  32. # you remove, and if the current number is above target, on
  33. # average you add fewer than you remove.
  34. # For relays, looking at all the consensuses for Nov and Dec
  35. # 2019, newmean is about 1.0% of the network size, and newstddev
  36. # is about 0.3% of the network size.
  37. self.relay_newmean = 0.010 * self.relaytarget
  38. self.relay_newstddev = 0.003 * self.relaytarget
  39. self.relay_oldprob = 0.010
  40. # For clients, looking at how many clients request a consensus
  41. # with an if-modified-since date more than 3 hours old (and so
  42. # we treat them as "new") over several days in late Dec 2019,
  43. # newmean is about 16% of all clients, and newstddev is about 4%
  44. # of all clients.
  45. # if the environment variable WOSIM_CLIENT_CHURN is set to 0,
  46. # don't churn clients at all. This allows us to see the effect
  47. # of client churn on relay bandwidth.
  48. if os.getenv('WOSIM_CLIENT_CHURN', '1') == '0':
  49. self.client_newmean = 0
  50. self.client_newstddev = 0
  51. self.client_oldprob = 0
  52. else:
  53. self.client_newmean = 0.16 * self.clienttarget
  54. self.client_newstddev = 0.04 * self.clienttarget
  55. self.client_oldprob = 0.16
  56. # Start some dirauths
  57. self.dirauthaddrs = []
  58. self.dirauths = []
  59. for i in range(numdirauths):
  60. dira = dirauth.DirAuth(i, numdirauths)
  61. self.dirauths.append(dira)
  62. self.dirauthaddrs.append(dira.netaddr)
  63. # Start some relays
  64. self.relays = []
  65. for i in range(self.relaytarget):
  66. # Relay bandwidths (at least the ones fast enough to get used)
  67. # in the live Tor network (as of Dec 2019) are well approximated
  68. # by (200000-(200000-25000)/3*log10(x)) where x is a
  69. # uniform integer in [1,2500]
  70. x = random.randint(1,2500)
  71. bw = int(200000-(200000-25000)/3*math.log10(x))
  72. self.relays.append(relay.Relay(self.dirauthaddrs, bw, 0))
  73. # The fallback relays are a hardcoded list of a small fraction
  74. # of the relays, used by clients for bootstrapping
  75. numfallbackrelays = int(self.relaytarget * fracfallbackrelays) + 1
  76. fallbackrelays = random.sample(self.relays, numfallbackrelays)
  77. for r in fallbackrelays:
  78. r.set_is_fallbackrelay()
  79. network.thenetwork.setfallbackrelays(fallbackrelays)
  80. # Tick the epoch to build the first consensus
  81. network.thenetwork.nextepoch()
  82. # Start some clients
  83. self.clients = []
  84. for i in range(clienttarget):
  85. self.clients.append(client.Client(self.dirauthaddrs))
  86. # Throw away all the performance statistics to this point
  87. for d in self.dirauths: d.perfstats.reset()
  88. for r in self.relays: r.perfstats.reset()
  89. # The clients' stats are already at 0, but they have the
  90. # "bootstrapping" flag set, which we want to keep, so we
  91. # won't reset them.
  92. # Tick the epoch to bootstrap the clients
  93. network.thenetwork.nextepoch()
  94. def one_epoch(self):
  95. """Simulate one epoch."""
  96. epoch = network.thenetwork.getepoch()
  97. # Each client will start a random number of circuits in a
  98. # Poisson distribution with mean gamma. To randomize the order
  99. # of the clients creating each circuit, we actually use a
  100. # Poisson distribution with mean (gamma*num_clients), and assign
  101. # each event to a uniformly random client. (This does in fact
  102. # give the required distribution.)
  103. numclients = len(self.clients)
  104. # simtime is the simulated time, measured in epochs (i.e.,
  105. # 0=start of this epoch; 1=end of this epoch)
  106. simtime = 0
  107. numcircs = 0
  108. allcircs = []
  109. lastpercent = -1
  110. while simtime < 1.0:
  111. try:
  112. allcircs.append(
  113. random.choice(self.clients).channelmgr.new_circuit())
  114. except ValueError as e:
  115. self.statslogger.error(str(e))
  116. raise e
  117. simtime += random.expovariate(self.gamma * numclients)
  118. numcircs += 1
  119. percent = int(100*simtime)
  120. #if percent != lastpercent:
  121. if numcircs % 100 == 0:
  122. logging.info("Creating circuits in epoch %s: %d%% (%d circuits)",
  123. epoch, percent, numcircs)
  124. lastpercent = percent
  125. # gather stats
  126. totsent = 0
  127. totrecv = 0
  128. dirasent = 0
  129. dirarecv = 0
  130. relaysent = 0
  131. relayrecv = 0
  132. clisent = 0
  133. clirecv = 0
  134. dirastats = network.PerfStatsStats()
  135. for d in self.dirauths:
  136. logging.debug("%s", d.perfstats)
  137. dirasent += d.perfstats.bytes_sent
  138. dirarecv += d.perfstats.bytes_received
  139. dirastats.accum(d.perfstats)
  140. totsent += dirasent
  141. totrecv += dirarecv
  142. relaystats = network.PerfStatsStats(True)
  143. relaybstats = network.PerfStatsStats(True)
  144. relaynbstats = network.PerfStatsStats(True)
  145. relayfbstats = network.PerfStatsStats(True)
  146. for r in self.relays:
  147. logging.debug("%s", r.perfstats)
  148. relaysent += r.perfstats.bytes_sent
  149. relayrecv += r.perfstats.bytes_received
  150. relaystats.accum(r.perfstats)
  151. if r.is_fallbackrelay:
  152. relayfbstats.accum(r.perfstats)
  153. else:
  154. if r.perfstats.is_bootstrapping:
  155. relaybstats.accum(r.perfstats)
  156. else:
  157. relaynbstats.accum(r.perfstats)
  158. totsent += relaysent
  159. totrecv += relayrecv
  160. clistats = network.PerfStatsStats()
  161. clibstats = network.PerfStatsStats()
  162. clinbstats = network.PerfStatsStats()
  163. for c in self.clients:
  164. logging.debug("%s", c.perfstats)
  165. clisent += c.perfstats.bytes_sent
  166. clirecv += c.perfstats.bytes_received
  167. clistats.accum(c.perfstats)
  168. if c.perfstats.is_bootstrapping:
  169. clibstats.accum(c.perfstats)
  170. else:
  171. clinbstats.accum(c.perfstats)
  172. totsent += clisent
  173. totrecv += clirecv
  174. self.statslogger.info("DirAuths sent=%s recv=%s bytes=%s" % \
  175. (dirasent, dirarecv, dirasent+dirarecv))
  176. self.statslogger.info("Relays sent=%s recv=%s bytes=%s" % \
  177. (relaysent, relayrecv, relaysent+relayrecv))
  178. self.statslogger.info("Client sent=%s recv=%s bytes=%s" % \
  179. (clisent, clirecv, clisent+clirecv))
  180. self.statslogger.info("Total sent=%s recv=%s bytes=%s" % \
  181. (totsent, totrecv, totsent+totrecv))
  182. numdirauths = len(self.dirauths)
  183. numrelays = len(self.relays)
  184. numclients = len(self.clients)
  185. self.statslogger.info("Dirauths %s", dirastats)
  186. self.statslogger.info("Relays %s", relaystats)
  187. self.statslogger.info("Relays(FB) %s", relayfbstats)
  188. self.statslogger.info("Relays(B) %s", relaybstats)
  189. self.statslogger.info("Relays(NB) %s", relaynbstats)
  190. self.statslogger.info("Clients %s", clistats)
  191. self.statslogger.info("Clients(B) %s", clibstats)
  192. self.statslogger.info("Clients(NB) %s", clinbstats)
  193. # Close circuits
  194. for c in allcircs:
  195. c.close()
  196. # Clear bootstrapping flag
  197. for d in self.dirauths: d.perfstats.is_bootstrapping = False
  198. for r in self.relays: r.perfstats.is_bootstrapping = False
  199. for c in self.clients: c.perfstats.is_bootstrapping = False
  200. # Churn relays
  201. # Stop some of the (non-fallback) relays
  202. relays_remaining = []
  203. numrelays = len(self.relays)
  204. numrelaysterminated = 0
  205. lastpercent = 0
  206. logging.info("Terminating some relays")
  207. for i, r in enumerate(self.relays):
  208. percent = int(100*(i+1)/numrelays)
  209. if not r.is_fallbackrelay and \
  210. random.random() < self.relay_oldprob:
  211. r.terminate()
  212. numrelaysterminated += 1
  213. else:
  214. # Keep this relay
  215. relays_remaining.append(r)
  216. if percent != lastpercent:
  217. lastpercent = percent
  218. logging.info("%d%% relays considered, %d terminated",
  219. percent, numrelaysterminated)
  220. self.relays = relays_remaining
  221. # Start some new relays
  222. relays_new = int(random.normalvariate(self.relay_newmean,
  223. self.relay_newstddev))
  224. logging.info("Starting %d new relays", relays_new)
  225. if relays_new > 0:
  226. for i in range(relays_new):
  227. x = random.randint(1,2500)
  228. bw = int(200000-(200000-25000)/3*math.log10(x))
  229. self.relays.append(relay.Relay(self.dirauthaddrs, bw, 0))
  230. # churn clients
  231. if self.client_oldprob > 0:
  232. # Stop some of the clients
  233. clients_remaining = []
  234. numclients = len(self.clients)
  235. numclientsterminated = 0
  236. lastpercent = 0
  237. logging.info("Terminating some clients")
  238. for i, c in enumerate(self.clients):
  239. percent = int(100*(i+1)/numclients)
  240. if random.random() < self.client_oldprob:
  241. c.terminate()
  242. numclientsterminated += 1
  243. else:
  244. # Keep this client
  245. clients_remaining.append(c)
  246. if percent != lastpercent:
  247. lastpercent = percent
  248. logging.info("%d%% clients considered, %d terminated",
  249. percent, numclientsterminated)
  250. self.clients = clients_remaining
  251. # Start some new clients
  252. clients_new = int(random.normalvariate(self.client_newmean,
  253. self.client_newstddev))
  254. logging.info("Starting %d new clients", clients_new)
  255. if clients_new > 0:
  256. for i in range(clients_new):
  257. self.clients.append(client.Client(self.dirauthaddrs))
  258. # Reset stats
  259. for d in self.dirauths: d.perfstats.reset()
  260. for r in self.relays: r.perfstats.reset()
  261. for c in self.clients: c.perfstats.reset()
  262. # Tick the epoch
  263. network.thenetwork.nextepoch()
  264. if __name__ == '__main__':
  265. # Args: womode snipauthmode networkscale numepochs randseed logdir
  266. if len(sys.argv) != 7:
  267. sys.stderr.write("Usage: womode snipauthmode networkscale numepochs randseed logdir\n")
  268. sys.exit(1)
  269. womode = network.WOMode[sys.argv[1].upper()]
  270. snipauthmode = network.SNIPAuthMode[sys.argv[2].upper()]
  271. networkscale = float(sys.argv[3])
  272. numepochs = int(sys.argv[4])
  273. randseed = int(sys.argv[5])
  274. logfile = "%s/%s_%s_%f_%s_%s.log" % (sys.argv[6], womode.name,
  275. snipauthmode.name, networkscale, numepochs, randseed)
  276. # Seed the PRNG. On Ubuntu 18.04, this in fact makes future calls
  277. # to (non-cryptographic) random numbers deterministic. On Ubuntu
  278. # 16.04, it does not.
  279. random.seed(randseed)
  280. loglevel = logging.INFO
  281. # Uncomment to see all the debug messages
  282. # loglevel = logging.DEBUG
  283. logging.basicConfig(level=loglevel,
  284. format="%(asctime)s:%(levelname)s:%(message)s")
  285. # The gathered statistics get logged separately
  286. statslogger = logging.getLogger("simulator")
  287. handler = logging.FileHandler(logfile)
  288. handler.setFormatter(logging.Formatter("%(asctime)s:%(message)s"))
  289. statslogger.addHandler(handler)
  290. statslogger.setLevel(logging.INFO)
  291. statslogger.info("Starting simulation %s", logfile)
  292. # Set the Walking Onions style to use
  293. network.thenetwork.set_wo_style(womode, snipauthmode)
  294. # The steady-state numbers of relays and clients
  295. relaytarget = math.ceil(6500 * networkscale)
  296. clienttarget = math.ceil(2500000 * networkscale)
  297. # Create the simulation
  298. simulator = Simulator(relaytarget, clienttarget, statslogger)
  299. for e in range(numepochs):
  300. statslogger.info("Starting epoch %s simulation", e+3)
  301. simulator.one_epoch()
  302. maxmemmib = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
  303. statslogger.info("%d MiB used", maxmemmib)