relay.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. #!/usr/bin/env python3
  2. import random # For simulation, not cryptography!
  3. import math
  4. import nacl.utils
  5. import nacl.signing
  6. import nacl.public
  7. import network
  8. import dirauth
  9. class RelayNetMsg(network.NetMsg):
  10. """The subclass of NetMsg for messages between relays and either
  11. relays or clients."""
  12. class RelayGetConsensusMsg(RelayNetMsg):
  13. """The subclass of RelayNetMsg for fetching the consensus."""
  14. class RelayConsensusMsg(RelayNetMsg):
  15. """The subclass of RelayNetMsg for returning the consensus."""
  16. def __init__(self, consensus):
  17. self.consensus = consensus
  18. class RelayRandomHopMsg(RelayNetMsg):
  19. """A message used for testing, that hops from relay to relay
  20. randomly until its TTL expires."""
  21. def __init__(self, ttl):
  22. self.ttl = ttl
  23. def __str__(self):
  24. return "RandomHop TTL=%d" % self.ttl
  25. class CircuitCellMsg(RelayNetMsg):
  26. """Send a message tagged with a circuit id."""
  27. def __init__(self, circuitid, cell):
  28. self.circid = circuitid
  29. self.cell = cell
  30. def __str__(self):
  31. return "C%d:%s" % (self.circid, self.cell)
  32. class Channel(network.Connection):
  33. """A class representing a channel between a relay and either a
  34. client or a relay, transporting cells from various circuits."""
  35. def __init__(self):
  36. super().__init__()
  37. # The CellRelay managing this Channel
  38. self.cellrelay = None
  39. # The Channel at the other end
  40. self.peer = None
  41. # The function to call when the connection closes
  42. self.closer = lambda: 0
  43. def closed(self):
  44. self.closer()
  45. self.peer = None
  46. def close(self):
  47. if self.peer is not None:
  48. self.peer.closed()
  49. self.closed()
  50. def send_cell(self, circid, cell):
  51. """Send the given message, tagged for the given circuit id."""
  52. msg = CircuitCellMsg(circid, cell)
  53. self.send_msg(msg)
  54. def send_msg(self, msg):
  55. """Send the given NetMsg on the channel."""
  56. self.peer.received(self.cellrelay.myaddr, msg)
  57. def received(self, peeraddr, msg):
  58. """Callback when a message is received from the network."""
  59. if isinstance(msg, CircuitCellMsg):
  60. circid, cell = msg.circid, msg.cell
  61. self.cellrelay.received_cell(circid, cell, peeraddr, self.peer)
  62. else:
  63. self.cellrelay.received_msg(msg, peeraddr, self.peer)
  64. class CellRelay:
  65. """The class that manages the channels to other relays and clients.
  66. Relays and clients both use this class to both create on-demand
  67. channels to relays, to gracefully handle the closing of channels,
  68. and to handle commands received over the channels."""
  69. def __init__(self, myaddr, dirauthaddrs):
  70. # A dictionary of Channels to other hosts, indexed by NetAddr
  71. self.channels = dict()
  72. self.myaddr = myaddr
  73. self.dirauthaddrs = dirauthaddrs
  74. self.consensus = None
  75. def get_consensus(self):
  76. """Download a fresh consensus from a random dirauth."""
  77. a = random.choice(self.dirauthaddrs)
  78. c = network.thenetwork.connect(self, a)
  79. self.consensus = c.getconsensus()
  80. print('consensus downloaded:', self.consensus)
  81. c.close()
  82. def add_channel(self, channel, peeraddr):
  83. """Add the given channel to the list of channels we are
  84. managing. If we are already managing a channel to the same
  85. peer, close it first."""
  86. if peeraddr in self.channels:
  87. self.channels[peeraddr].close()
  88. channel.cellrelay = self
  89. self.channels[peeraddr] = channel
  90. def get_channel_to(self, addr):
  91. """Get the Channel connected to the given NetAddr, creating one
  92. if none exists right now."""
  93. if addr in self.channels:
  94. return self.channels[addr]
  95. # Create the new connection
  96. newconn = network.thenetwork.connect(self.myaddr, addr)
  97. self.channels[addr] = newconn
  98. newconn.closer = lambda: self.channels.pop(addr)
  99. newconn.cellrelay = self
  100. return newconn
  101. def received_msg(self, msg, peeraddr, peer):
  102. """Callback when a NetMsg not specific to a circuit is
  103. received."""
  104. print("Node %s received msg %s from %s" % (self.myaddr, msg, peeraddr))
  105. if isinstance(msg, RelayRandomHopMsg):
  106. if msg.ttl > 0:
  107. # Pick a random next hop from the consensus
  108. nexthop = random.choice(self.consensus.consdict['relays'])
  109. nextaddr = nexthop.descdict['addr']
  110. self.send_msg(RelayRandomHopMsg(msg.ttl-1), nextaddr)
  111. def received_cell(self, circid, cell, peeraddr, peer):
  112. """Callback with a circuit-specific cell is received."""
  113. print("Node %s received cell on circ %d: %s from %s" % (self.myaddr, circid, cell, peeraddr))
  114. def send_msg(self, msg, peeraddr):
  115. """Send a message to the peer with the given address."""
  116. conn = self.get_channel_to(peeraddr)
  117. conn.send_msg(msg)
  118. def send_cell(self, circid, cell, peeraddr):
  119. """Send a cell on the given circuit to the peer with the given
  120. address."""
  121. conn = self.get_channel_to(peeraddr)
  122. conn.send_cell(circid, cell)
  123. class Relay(network.Server):
  124. """The class representing an onion relay."""
  125. def __init__(self, dirauthaddrs, bw, flags):
  126. # Create the identity and onion keys
  127. self.idkey = nacl.signing.SigningKey.generate()
  128. self.onionkey = nacl.public.PrivateKey.generate()
  129. self.name = self.idkey.verify_key.encode(encoder=nacl.encoding.HexEncoder).decode("ascii")
  130. # Bind to the network to get a network address
  131. self.netaddr = network.thenetwork.bind(self)
  132. # Our bandwidth and flags
  133. self.bw = bw
  134. self.flags = flags
  135. # Register for epoch change notification
  136. network.thenetwork.wantepochticks(self, True, end=True)
  137. network.thenetwork.wantepochticks(self, True)
  138. # Create the CellRelay connection manager
  139. self.cellrelay = CellRelay(self.netaddr, dirauthaddrs)
  140. self.uploaddesc()
  141. def epoch_ending(self, epoch):
  142. # Download the new consensus, which will have been created
  143. # already since the dirauths' epoch_ending callbacks happened
  144. # before the relays'.
  145. self.cellrelay.get_consensus()
  146. def newepoch(self, epoch):
  147. self.uploaddesc()
  148. def uploaddesc(self):
  149. # Upload the descriptor for the epoch to come
  150. descdict = dict();
  151. descdict["epoch"] = network.thenetwork.getepoch() + 1
  152. descdict["idkey"] = self.idkey.verify_key
  153. descdict["onionkey"] = self.onionkey.public_key
  154. descdict["addr"] = self.netaddr
  155. descdict["bw"] = self.bw
  156. descdict["flags"] = self.flags
  157. desc = dirauth.RelayDescriptor(descdict)
  158. desc.sign(self.idkey)
  159. desc.verify()
  160. descmsg = dirauth.DirAuthUploadDescMsg(desc)
  161. # Upload them
  162. for a in self.cellrelay.dirauthaddrs:
  163. c = network.thenetwork.connect(self, a)
  164. c.sendmsg(descmsg)
  165. c.close()
  166. def connected(self, peer):
  167. """Callback invoked when someone (client or relay) connects to
  168. us. Create a pair of linked Channels and return the peer half
  169. to the peer."""
  170. # Create the linked pair
  171. peerchannel = Channel()
  172. ourchannel = Channel()
  173. peerchannel.peer = ourchannel
  174. ourchannel.peer = peerchannel
  175. # Add our channel to the CellRelay
  176. self.cellrelay.add_channel(ourchannel, peer)
  177. return peerchannel
  178. if __name__ == '__main__':
  179. # Start some dirauths
  180. numdirauths = 9
  181. dirauthaddrs = []
  182. for i in range(numdirauths):
  183. dira = dirauth.DirAuth(i, numdirauths)
  184. dirauthaddrs.append(network.thenetwork.bind(dira))
  185. # Start some relays
  186. numrelays = 10
  187. relays = []
  188. for i in range(numrelays):
  189. # Relay bandwidths (at least the ones fast enough to get used)
  190. # in the live Tor network (as of Dec 2019) are well approximated
  191. # by (200000-(200000-25000)/3*log10(x)) where x is a
  192. # uniform integer in [1,2500]
  193. x = random.randint(1,2500)
  194. bw = int(200000-(200000-25000)/3*math.log10(x))
  195. relays.append(Relay(dirauthaddrs, bw, 0))
  196. # The fallback relays are a hardcoded list of about 5% of the
  197. # relays, used by clients for bootstrapping
  198. numfallbackrelays = int(numrelays * 0.05) + 1
  199. fallbackrelays = random.sample(relays, numfallbackrelays)
  200. network.thenetwork.setfallbackrelays(fallbackrelays)
  201. # Tick the epoch
  202. network.thenetwork.nextepoch()
  203. dirauth.DirAuth.consensus.verify(network.thenetwork.dirauthkeys())
  204. print('ticked; epoch=', network.thenetwork.getepoch())
  205. relays[3].cellrelay.send_msg(RelayRandomHopMsg(30), relays[5].netaddr)
  206. # See what channels exist
  207. for r in relays:
  208. print("%s: %s" % (r.netaddr, [ str(k) for k in r.cellrelay.channels.keys()]))
  209. #relays[3].cellrelay.send_cell(1, network.StringNetMsg("test"), relays[3].consensus.consdict['relays'][5].descdict['addr'])
  210. #relays[3].cellrelay.send_cell(2, network.StringNetMsg("cell"), relays[3].consensus.consdict['relays'][6].descdict['addr'])
  211. #relays[3].cellrelay.send_cell(2, network.StringNetMsg("again"), relays[3].consensus.consdict['relays'][1].descdict['addr'])
  212. #relays[3].cellrelay.send_cell(2, network.StringNetMsg("and again"), relays[3].consensus.consdict['relays'][5].descdict['addr'])