371 lines
14 KiB
Python
371 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import random # For simulation, not cryptography!
|
|
import math
|
|
import re
|
|
import statistics
|
|
import sys
|
|
import os
|
|
import logging
|
|
import resource
|
|
|
|
import network
|
|
import dirauth
|
|
import relay
|
|
import client
|
|
|
|
from bwparser import JansenBandwidthParser, KomloBandwidthParser
|
|
|
|
|
|
class Simulator:
|
|
def __init__(self, bw_parser, relaytarget, clienttarget, statslogger):
|
|
self.relaytarget = relaytarget
|
|
self.clienttarget = clienttarget
|
|
self.statslogger = statslogger
|
|
self.parser = bw_parser
|
|
self.network_size = self.parser.get_relay_num()
|
|
|
|
# Some (for now) hard-coded parameters
|
|
|
|
# The number of directory authorities
|
|
numdirauths = 9
|
|
|
|
# The fraction of relays that are fallback relays
|
|
# Taken from the live network in Jan 2020
|
|
fracfallbackrelays = 0.023
|
|
|
|
# Mean number of circuits created per client per epoch
|
|
self.gamma = 8.9
|
|
|
|
# Churn is controlled by three parameters:
|
|
# newmean: the mean number of new arrivals per epoch
|
|
# newstddev: the stddev number of new arrivals per epoch
|
|
# oldprob: the probability any given existing one leaves per epoch
|
|
|
|
# If target is the desired steady state number, then it should
|
|
# be the case that target * oldprob = newmean. That way, if the
|
|
# current number is below target, on average you add more than
|
|
# you remove, and if the current number is above target, on
|
|
# average you add fewer than you remove.
|
|
|
|
# For relays, looking at all the consensuses for Nov and Dec
|
|
# 2019, newmean is about 1.0% of the network size, and newstddev
|
|
# is about 0.3% of the network size.
|
|
self.relay_newmean = 0.010 * self.relaytarget
|
|
self.relay_newstddev = 0.003 * self.relaytarget
|
|
self.relay_oldprob = 0.010
|
|
|
|
# For clients, looking at how many clients request a consensus
|
|
# with an if-modified-since date more than 3 hours old (and so
|
|
# we treat them as "new") over several days in late Dec 2019,
|
|
# newmean is about 16% of all clients, and newstddev is about 4%
|
|
# of all clients.
|
|
|
|
# if the environment variable WOSIM_CLIENT_CHURN is set to 0,
|
|
# don't churn clients at all. This allows us to see the effect
|
|
# of client churn on relay bandwidth.
|
|
if os.getenv('WOSIM_CLIENT_CHURN', '1') == '0':
|
|
self.client_newmean = 0
|
|
self.client_newstddev = 0
|
|
self.client_oldprob = 0
|
|
else:
|
|
self.client_newmean = 0.16 * self.clienttarget
|
|
self.client_newstddev = 0.04 * self.clienttarget
|
|
self.client_oldprob = 0.16
|
|
|
|
# Start some dirauths
|
|
self.dirauthaddrs = []
|
|
self.dirauths = []
|
|
for i in range(numdirauths):
|
|
dira = dirauth.DirAuth(i, numdirauths)
|
|
self.dirauths.append(dira)
|
|
self.dirauthaddrs.append(dira.netaddr)
|
|
|
|
# Start some relays
|
|
self.relays = []
|
|
bw = self.calculate_relay_bandwidth()
|
|
for i in range(self.relaytarget):
|
|
new_relay = relay.Relay(self.dirauthaddrs, bw[i], 0)
|
|
self.relays.append(new_relay)
|
|
|
|
# The fallback relays are a hardcoded list of a small fraction
|
|
# of the relays, used by clients for bootstrapping
|
|
numfallbackrelays = int(self.relaytarget * fracfallbackrelays) + 1
|
|
fallbackrelays = random.sample(self.relays, numfallbackrelays)
|
|
for r in fallbackrelays:
|
|
r.set_is_fallbackrelay()
|
|
network.thenetwork.setfallbackrelays(fallbackrelays)
|
|
|
|
# Tick the epoch to build the first consensus
|
|
network.thenetwork.nextepoch()
|
|
|
|
# Start some clients
|
|
self.clients = []
|
|
for i in range(clienttarget):
|
|
self.clients.append(client.Client(self.dirauthaddrs))
|
|
|
|
# Throw away all the performance statistics to this point
|
|
for d in self.dirauths: d.perfstats.reset()
|
|
for r in self.relays: r.perfstats.reset()
|
|
# The clients' stats are already at 0, but they have the
|
|
# "bootstrapping" flag set, which we want to keep, so we
|
|
# won't reset them.
|
|
|
|
# Tick the epoch to bootstrap the clients
|
|
network.thenetwork.nextepoch()
|
|
|
|
def one_epoch(self):
|
|
"""Simulate one epoch."""
|
|
|
|
epoch = network.thenetwork.getepoch()
|
|
|
|
# Each client will start a random number of circuits in a
|
|
# Poisson distribution with mean gamma. To randomize the order
|
|
# of the clients creating each circuit, we actually use a
|
|
# Poisson distribution with mean (gamma*num_clients), and assign
|
|
# each event to a uniformly random client. (This does in fact
|
|
# give the required distribution.)
|
|
|
|
numclients = len(self.clients)
|
|
|
|
# simtime is the simulated time, measured in epochs (i.e.,
|
|
# 0=start of this epoch; 1=end of this epoch)
|
|
simtime = 0
|
|
numcircs = 0
|
|
|
|
allcircs = []
|
|
|
|
lastpercent = -1
|
|
while simtime < 1.0:
|
|
try:
|
|
allcircs.append(
|
|
random.choice(self.clients).channelmgr.new_circuit())
|
|
except ValueError as e:
|
|
self.statslogger.error(str(e))
|
|
raise e
|
|
|
|
simtime += random.expovariate(self.gamma * numclients)
|
|
numcircs += 1
|
|
percent = int(100*simtime)
|
|
#if percent != lastpercent:
|
|
if numcircs % 100 == 0:
|
|
logging.info("Creating circuits in epoch %s: %d%% (%d circuits)",
|
|
epoch, percent, numcircs)
|
|
lastpercent = percent
|
|
|
|
# gather stats
|
|
totsent = 0
|
|
totrecv = 0
|
|
dirasent = 0
|
|
dirarecv = 0
|
|
relaysent = 0
|
|
relayrecv = 0
|
|
clisent = 0
|
|
clirecv = 0
|
|
dirastats = network.PerfStatsStats()
|
|
for d in self.dirauths:
|
|
logging.debug("%s", d.perfstats)
|
|
dirasent += d.perfstats.bytes_sent
|
|
dirarecv += d.perfstats.bytes_received
|
|
dirastats.accum(d.perfstats)
|
|
totsent += dirasent
|
|
totrecv += dirarecv
|
|
relaystats = network.PerfStatsStats(True)
|
|
relaybstats = network.PerfStatsStats(True)
|
|
relaynbstats = network.PerfStatsStats(True)
|
|
relayfbstats = network.PerfStatsStats(True)
|
|
for r in self.relays:
|
|
logging.debug("%s", r.perfstats)
|
|
relaysent += r.perfstats.bytes_sent
|
|
relayrecv += r.perfstats.bytes_received
|
|
relaystats.accum(r.perfstats)
|
|
if r.is_fallbackrelay:
|
|
relayfbstats.accum(r.perfstats)
|
|
else:
|
|
if r.perfstats.is_bootstrapping:
|
|
relaybstats.accum(r.perfstats)
|
|
else:
|
|
relaynbstats.accum(r.perfstats)
|
|
totsent += relaysent
|
|
totrecv += relayrecv
|
|
clistats = network.PerfStatsStats()
|
|
clibstats = network.PerfStatsStats()
|
|
clinbstats = network.PerfStatsStats()
|
|
for c in self.clients:
|
|
logging.debug("%s", c.perfstats)
|
|
clisent += c.perfstats.bytes_sent
|
|
clirecv += c.perfstats.bytes_received
|
|
clistats.accum(c.perfstats)
|
|
if c.perfstats.is_bootstrapping:
|
|
clibstats.accum(c.perfstats)
|
|
else:
|
|
clinbstats.accum(c.perfstats)
|
|
totsent += clisent
|
|
totrecv += clirecv
|
|
self.statslogger.info("DirAuths sent=%s recv=%s bytes=%s" % \
|
|
(dirasent, dirarecv, dirasent+dirarecv))
|
|
self.statslogger.info("Relays sent=%s recv=%s bytes=%s" % \
|
|
(relaysent, relayrecv, relaysent+relayrecv))
|
|
self.statslogger.info("Client sent=%s recv=%s bytes=%s" % \
|
|
(clisent, clirecv, clisent+clirecv))
|
|
self.statslogger.info("Total sent=%s recv=%s bytes=%s" % \
|
|
(totsent, totrecv, totsent+totrecv))
|
|
numdirauths = len(self.dirauths)
|
|
numrelays = len(self.relays)
|
|
numclients = len(self.clients)
|
|
self.statslogger.info("Dirauths %s", dirastats)
|
|
self.statslogger.info("Relays %s", relaystats)
|
|
self.statslogger.info("Relays(FB) %s", relayfbstats)
|
|
self.statslogger.info("Relays(B) %s", relaybstats)
|
|
self.statslogger.info("Relays(NB) %s", relaynbstats)
|
|
self.statslogger.info("Clients %s", clistats)
|
|
self.statslogger.info("Clients(B) %s", clibstats)
|
|
self.statslogger.info("Clients(NB) %s", clinbstats)
|
|
|
|
# Close circuits
|
|
for c in allcircs:
|
|
c.close()
|
|
|
|
# Clear bootstrapping flag
|
|
for d in self.dirauths: d.perfstats.is_bootstrapping = False
|
|
for r in self.relays: r.perfstats.is_bootstrapping = False
|
|
for c in self.clients: c.perfstats.is_bootstrapping = False
|
|
|
|
# Churn relays
|
|
|
|
# Stop some of the (non-fallback) relays
|
|
relays_remaining = []
|
|
numrelays = len(self.relays)
|
|
numrelaysterminated = 0
|
|
lastpercent = 0
|
|
logging.info("Terminating some relays")
|
|
for i, r in enumerate(self.relays):
|
|
percent = int(100*(i+1)/numrelays)
|
|
if not r.is_fallbackrelay and \
|
|
random.random() < self.relay_oldprob:
|
|
r.terminate()
|
|
numrelaysterminated += 1
|
|
else:
|
|
# Keep this relay
|
|
relays_remaining.append(r)
|
|
if percent != lastpercent:
|
|
lastpercent = percent
|
|
logging.info("%d%% relays considered, %d terminated",
|
|
percent, numrelaysterminated)
|
|
self.relays = relays_remaining
|
|
|
|
# Start some new relays
|
|
relays_new = int(random.normalvariate(self.relay_newmean,
|
|
self.relay_newstddev))
|
|
logging.info("Starting %d new relays", relays_new)
|
|
bw = self.calculate_relay_bandwidth()
|
|
if relays_new > 0:
|
|
for i in range(relays_new):
|
|
new_relay = relay.Relay(self.dirauthaddrs, bw[i], 0)
|
|
self.relays.append(new_relay)
|
|
|
|
# churn clients
|
|
|
|
if self.client_oldprob > 0:
|
|
# Stop some of the clients
|
|
clients_remaining = []
|
|
numclients = len(self.clients)
|
|
numclientsterminated = 0
|
|
lastpercent = 0
|
|
logging.info("Terminating some clients")
|
|
for i, c in enumerate(self.clients):
|
|
percent = int(100*(i+1)/numclients)
|
|
if random.random() < self.client_oldprob:
|
|
c.terminate()
|
|
numclientsterminated += 1
|
|
else:
|
|
# Keep this client
|
|
clients_remaining.append(c)
|
|
if percent != lastpercent:
|
|
lastpercent = percent
|
|
logging.info("%d%% clients considered, %d terminated",
|
|
percent, numclientsterminated)
|
|
self.clients = clients_remaining
|
|
|
|
# Start some new clients
|
|
clients_new = int(random.normalvariate(self.client_newmean,
|
|
self.client_newstddev))
|
|
logging.info("Starting %d new clients", clients_new)
|
|
if clients_new > 0:
|
|
for i in range(clients_new):
|
|
self.clients.append(client.Client(self.dirauthaddrs))
|
|
|
|
# Reset stats
|
|
for d in self.dirauths: d.perfstats.reset()
|
|
for r in self.relays: r.perfstats.reset()
|
|
for c in self.clients: c.perfstats.reset()
|
|
|
|
# Tick the epoch
|
|
network.thenetwork.nextepoch()
|
|
|
|
def calculate_relay_bandwidth(self):
|
|
return self.parser.get_distribution()
|
|
|
|
if __name__ == '__main__':
|
|
# Args: womode snipauthmode networkscale numepochs randseed logdir
|
|
if len(sys.argv) != 7:
|
|
sys.stderr.write("Usage: womode snipauthmode networkscale numepochs randseed logdir\n")
|
|
sys.exit(1)
|
|
|
|
# sensible defaults
|
|
bandwidth_file = os.getenv('BW_FILE')
|
|
|
|
womode = network.WOMode[sys.argv[1].upper()]
|
|
snipauthmode = network.SNIPAuthMode[sys.argv[2].upper()]
|
|
networkscale = float(sys.argv[3])
|
|
numepochs = int(sys.argv[4])
|
|
randseed = int(sys.argv[5])
|
|
logfile = "%s/%s_%s_%f_%s_%s.log" % (sys.argv[6], womode.name,
|
|
snipauthmode.name, networkscale, numepochs, randseed)
|
|
|
|
bw_parser = None
|
|
if os.getenv('BW_ALGO') != "komlo":
|
|
bw_parser = JansenBandwidthParser(bw_file=bandwidth_file, netscale=networkscale)
|
|
else:
|
|
# keep the original assumption
|
|
bw_parser = KomloBandwidthParser(sample_size=(math.ceil(6500 * networkscale)))
|
|
|
|
# Seed the PRNG. On Ubuntu 18.04, this in fact makes future calls
|
|
# to (non-cryptographic) random numbers deterministic. On Ubuntu
|
|
# 16.04, it does not.
|
|
random.seed(randseed)
|
|
|
|
loglevel = logging.INFO
|
|
# Uncomment to see all the debug messages
|
|
# loglevel = logging.DEBUG
|
|
|
|
logging.basicConfig(level=loglevel,
|
|
format="%(asctime)s:%(levelname)s:%(message)s")
|
|
|
|
# The gathered statistics get logged separately
|
|
statslogger = logging.getLogger("simulator")
|
|
handler = logging.FileHandler(logfile)
|
|
handler.setFormatter(logging.Formatter("%(asctime)s:%(message)s"))
|
|
statslogger.addHandler(handler)
|
|
statslogger.setLevel(logging.INFO)
|
|
|
|
statslogger.info("Starting simulation %s", logfile)
|
|
|
|
# Set the Walking Onions style to use
|
|
network.thenetwork.set_wo_style(womode, snipauthmode)
|
|
|
|
# The steady-state numbers of relays and clients
|
|
network_size = bw_parser.get_relay_num()
|
|
relaytarget = math.ceil(network_size * networkscale)
|
|
clienttarget = math.ceil(2500000 * networkscale)
|
|
|
|
# Create the simulation
|
|
simulator = Simulator(bw_parser, relaytarget, clienttarget, statslogger)
|
|
|
|
for e in range(numepochs):
|
|
statslogger.info("Starting epoch %s simulation", e+3)
|
|
simulator.one_epoch()
|
|
|
|
maxmemmib = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
|
|
statslogger.info("%d MiB used", maxmemmib)
|