simulator.py 13 KB

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