1037 lines
45 KiB
Python
1037 lines
45 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import random # For simulation, not cryptography!
|
|
import math
|
|
import sys
|
|
import logging
|
|
import resource
|
|
|
|
import nacl.utils
|
|
import nacl.signing
|
|
import nacl.public
|
|
import nacl.hash
|
|
import nacl.bindings
|
|
|
|
import network
|
|
import dirauth
|
|
|
|
import msg as simmsg
|
|
import cell as simcell
|
|
|
|
from connection import Connection
|
|
|
|
vrftable = dict()
|
|
|
|
# Simulated VRF. The output and proof are of the correct size (32 bytes
|
|
# for the output, 48 bytes for the proof), and we charge the right
|
|
# number of group operations (1 keygen, 2 DH for proving, 3 DH for
|
|
# verifiying -- that last number charges 1 DH for a (s*G+c*Y) multiexp,
|
|
# but 2 for a (s*A+c*B) multiexp)
|
|
class VRF:
|
|
@staticmethod
|
|
def get_output(vrf_privkey, vrf_input, perfstats):
|
|
"""Given the VRF private key and the input, produce
|
|
the output value and the proof."""
|
|
# nacl can't do the group operations we need to actually
|
|
# compute the VRF, so we just fake it for the simulation. The
|
|
# output value is sha256(privkey, input), and the proof is
|
|
# sha256(pubkey, input, output) + 16 bytes of 0.
|
|
# ***THIS IS NOT A REAL VRF!***
|
|
val = nacl.hash.sha256(bytes(vrf_privkey) + vrf_input,
|
|
encoder=nacl.encoding.RawEncoder)
|
|
vrf_pubkey = vrf_privkey.public_key
|
|
proof = nacl.hash.sha256(bytes(vrf_pubkey) + val + vrf_input,
|
|
encoder=nacl.encoding.RawEncoder) + bytes(16)
|
|
perfstats.keygens += 1
|
|
perfstats.dhs += 2
|
|
vrftable[proof] = (bytes(vrf_privkey), vrf_input, val, bytes(vrf_pubkey))
|
|
return val, proof
|
|
|
|
@staticmethod
|
|
def check_output(vrf_pubkey, vrf_input, vrf_output, perfstats):
|
|
"""Given the VRF public key, the input, and the claimed output
|
|
and proof, raise an exception if the proof fails to check.
|
|
Returns the VRF output."""
|
|
# Again, NOT A REAL VRF!
|
|
val, proof = vrf_output
|
|
if nacl.hash.sha256(vrf_pubkey + val + vrf_input,
|
|
encoder=nacl.encoding.RawEncoder) + bytes(16) != \
|
|
proof:
|
|
t = [nacl.encoding.HexEncoder.encode(x) for x in vrftable[proof]]
|
|
raise ValueError("VRF proof did not verify: %s %s %s 00x16 ?= %s: %s" % \
|
|
(nacl.encoding.HexEncoder.encode(vrf_pubkey),
|
|
nacl.encoding.HexEncoder.encode(val),
|
|
nacl.encoding.HexEncoder.encode(vrf_input),
|
|
nacl.encoding.HexEncoder.encode(proof),
|
|
t))
|
|
perfstats.dhs += 3
|
|
del vrftable[proof]
|
|
return val
|
|
|
|
|
|
class RelayFallbackTerminationError(Exception):
|
|
"""An exception raised when someone tries to terminate a fallback
|
|
relay."""
|
|
|
|
|
|
class Sphinx:
|
|
"""Implement the public-key reblinding technique based on Sphinx.
|
|
This does a few more public-key operations than it would strictly
|
|
need to if we were using a group implementation that (unlike nacl)
|
|
supported the operations we needed directly. The biggest issue is
|
|
that nacl insists the high bit is set on private keys, which means
|
|
we can't just multiply private keys together to get a new private
|
|
key, and do a single DH operation with that resulting key; we have
|
|
to perform a linear number of DH operations instead, per node in the
|
|
circuit, so a quadratic number of DH operations total."""
|
|
|
|
@staticmethod
|
|
def makeblindkey(shared_secret, domain_separator, perfstats):
|
|
"""Create a Sphinx reblinding key (a PrivateKey) out of a shared
|
|
secret and a domain separator (both bytestrings). The domain
|
|
separator is just a constant bytestring like b'data' or
|
|
b'circuit' for the data-protecting and circuit-protecting
|
|
public-key elements respectively."""
|
|
rawkey = nacl.hash.sha256(domain_separator + shared_secret,
|
|
encoder=nacl.encoding.RawEncoder)
|
|
perfstats.keygens += 1
|
|
|
|
# The PrivateKey constructor does the Curve25519 pinning of
|
|
# certain bits of the key to 0 and 1
|
|
return nacl.public.PrivateKey(rawkey)
|
|
|
|
@staticmethod
|
|
def reblindpubkey(blindkey, pubkey, perfstats):
|
|
"""Create a Sphinx reblinded PublicKey out of a reblinding key
|
|
(output by makeblindkey) and a (possibly already reblinded)
|
|
PublicKey."""
|
|
new_pubkey = nacl.bindings.crypto_scalarmult(bytes(blindkey),
|
|
bytes(pubkey))
|
|
perfstats.dhs += 1
|
|
|
|
return nacl.public.PublicKey(new_pubkey)
|
|
|
|
@staticmethod
|
|
def client(client_privkey, blindkey_list, server_pubkey,
|
|
domain_separator, is_last, perfstats):
|
|
"""Given the client's PrivateKey, a (possibly empty) list of
|
|
reblinding keys, and the server's PublicKey, produce the shared
|
|
secret and the new blinding key (to add to the list). The
|
|
domain separator is as above. If is_last is true, don't bother
|
|
creating the new blinding key, since this is the last iteration,
|
|
and we won't be using it."""
|
|
if type(server_pubkey) is bytes:
|
|
server_pubkey = nacl.public.PublicKey(server_pubkey)
|
|
reblinded_server_pubkey = server_pubkey
|
|
for blindkey in blindkey_list:
|
|
reblinded_server_pubkey = Sphinx.reblindpubkey(blindkey,
|
|
reblinded_server_pubkey, perfstats)
|
|
|
|
sharedsecret = nacl.public.Box(client_privkey,
|
|
reblinded_server_pubkey).shared_key()
|
|
perfstats.dhs += 1
|
|
|
|
if is_last:
|
|
blindkey = None
|
|
else:
|
|
blindkey = Sphinx.makeblindkey(sharedsecret,
|
|
domain_separator, perfstats)
|
|
|
|
return sharedsecret, blindkey
|
|
|
|
@staticmethod
|
|
def server(client_pubkey, server_privkey, domain_separator, is_last,
|
|
perfstats):
|
|
"""Given the client's PublicKey and the server's PrivateKey,
|
|
produce the shared secret and the new reblinded client
|
|
PublicKey. The domain separator is as above. If is_last is
|
|
True, don't bother generating the new PublicKey, since we're the
|
|
last server in the chain, and won't be using it."""
|
|
sharedsecret = nacl.public.Box(server_privkey,
|
|
client_pubkey).shared_key()
|
|
perfstats.dhs += 1
|
|
|
|
if is_last:
|
|
blinded_pubkey = None
|
|
else:
|
|
blindkey = Sphinx.makeblindkey(sharedsecret, domain_separator,
|
|
perfstats)
|
|
|
|
blinded_pubkey = Sphinx.reblindpubkey(blindkey, client_pubkey,
|
|
perfstats)
|
|
|
|
return sharedsecret, blinded_pubkey
|
|
|
|
|
|
class NTor:
|
|
"""A class implementing the ntor one-way authenticated key agreement
|
|
scheme. The details are not exactly the same as either the ntor
|
|
paper or Tor's implementation, but it will agree on keys and have
|
|
the same number of public key operations."""
|
|
|
|
def __init__(self, perfstats):
|
|
self.perfstats = perfstats
|
|
# Only used for Single-Pass Walking Onions; it is the sequence
|
|
# of blinding keys used by Sphinx
|
|
self.blinding_keys = []
|
|
|
|
def request(self):
|
|
"""Create the ntor request message: X = g^x."""
|
|
self.client_ephem_key = nacl.public.PrivateKey.generate()
|
|
self.perfstats.keygens += 1
|
|
return bytes(self.client_ephem_key.public_key)
|
|
|
|
@staticmethod
|
|
def reply(onion_privkey, idpubkey, client_pubkey, perfstats,
|
|
sphinx_domainsep=None):
|
|
"""The server calls this static method to produce the ntor reply
|
|
message: (Y = g^y, B = g^b, A = H(M, "verify")) and the shared
|
|
secret S = H(M, "secret") for M = (X^y,X^b,ID,B,X,Y). If
|
|
sphinx_domainsep is not None, also compute and return the Sphinx
|
|
reblinded client request to pass to the next server."""
|
|
if type(idpubkey) is not bytes:
|
|
idpubkey = bytes(idpubkey)
|
|
if type(client_pubkey) is bytes:
|
|
client_pubkey = nacl.public.PublicKey(client_pubkey)
|
|
server_ephem_key = nacl.public.PrivateKey.generate()
|
|
perfstats.keygens += 1
|
|
xykey = nacl.public.Box(server_ephem_key, client_pubkey).shared_key()
|
|
xbkey = nacl.public.Box(onion_privkey, client_pubkey).shared_key()
|
|
perfstats.dhs += 2
|
|
M = xykey + xbkey + \
|
|
idpubkey + \
|
|
onion_privkey.public_key.encode(encoder=nacl.encoding.RawEncoder) + \
|
|
server_ephem_key.public_key.encode(encoder=nacl.encoding.RawEncoder)
|
|
A = nacl.hash.sha256(M + b'verify', encoder=nacl.encoding.RawEncoder)
|
|
S = nacl.hash.sha256(M + b'secret', encoder=nacl.encoding.RawEncoder)
|
|
|
|
if sphinx_domainsep is not None:
|
|
blindkey = Sphinx.makeblindkey(S, sphinx_domainsep, perfstats)
|
|
blinded_client_pubkey = Sphinx.reblindpubkey(blindkey,
|
|
client_pubkey, perfstats)
|
|
return ((bytes(server_ephem_key.public_key),
|
|
bytes(onion_privkey.public_key), A),
|
|
S), blinded_client_pubkey
|
|
else:
|
|
return ((bytes(server_ephem_key.public_key),
|
|
bytes(onion_privkey.public_key), A), S)
|
|
|
|
def verify(self, reply, onion_pubkey, idpubkey, sphinx_domainsep=None):
|
|
"""The client calls this method to verify the ntor reply
|
|
message, passing the onion and id public keys for the server
|
|
it's expecting to be talking to. If sphinx_domainsep is not
|
|
None, also compute the reblinding key so that the client can
|
|
reuse this same NTor object for the next server. Returns the
|
|
shared secret on success, or raises ValueError on failure."""
|
|
server_ephem_pubkey, server_onion_pubkey, authtag = reply
|
|
if type(idpubkey) is not bytes:
|
|
idpubkey = bytes(idpubkey)
|
|
if type(server_ephem_pubkey) is bytes:
|
|
server_ephem_pubkey = nacl.public.PublicKey(server_ephem_pubkey)
|
|
if type(server_onion_pubkey) is bytes:
|
|
server_onion_pubkey = nacl.public.PublicKey(server_onion_pubkey)
|
|
if type(onion_pubkey) is bytes:
|
|
onion_pubkey = nacl.public.PublicKey(onion_pubkey)
|
|
if onion_pubkey != server_onion_pubkey:
|
|
raise ValueError("NTor onion pubkey mismatch")
|
|
# We use the blinding keys if present; if they're not present
|
|
# (because we're not in Single-Pass Walking Onions), the loops
|
|
# are just empty anyway, so everything will work in the usual
|
|
# unblinded way.
|
|
reblinded_server_ephem_pubkey = server_ephem_pubkey
|
|
for blindkey in self.blinding_keys:
|
|
reblinded_server_ephem_pubkey = Sphinx.reblindpubkey(blindkey,
|
|
reblinded_server_ephem_pubkey, self.perfstats)
|
|
xykey = nacl.public.Box(self.client_ephem_key,
|
|
reblinded_server_ephem_pubkey).shared_key()
|
|
reblinded_onion_pubkey = onion_pubkey
|
|
for blindkey in self.blinding_keys:
|
|
reblinded_onion_pubkey = Sphinx.reblindpubkey(blindkey,
|
|
reblinded_onion_pubkey, self.perfstats)
|
|
xbkey = nacl.public.Box(self.client_ephem_key,
|
|
reblinded_onion_pubkey).shared_key()
|
|
self.perfstats.dhs += 2
|
|
M = xykey + xbkey + \
|
|
idpubkey + \
|
|
onion_pubkey.encode(encoder=nacl.encoding.RawEncoder) + \
|
|
server_ephem_pubkey.encode(encoder=nacl.encoding.RawEncoder)
|
|
Acheck = nacl.hash.sha256(M + b'verify', encoder=nacl.encoding.RawEncoder)
|
|
S = nacl.hash.sha256(M + b'secret', encoder=nacl.encoding.RawEncoder)
|
|
if Acheck != authtag:
|
|
raise ValueError("NTor auth mismatch")
|
|
|
|
if sphinx_domainsep is not None:
|
|
blindkey = Sphinx.makeblindkey(S, sphinx_domainsep,
|
|
self.perfstats)
|
|
self.blinding_keys.append(blindkey)
|
|
return S
|
|
|
|
|
|
class VanillaExtendCircuitHandler:
|
|
"""A handler for VanillaExtendCircuitCell cells. It allocates a new
|
|
circuit id on the Channel to the requested next hop, connects the
|
|
existing and new circuits together, and forwards a
|
|
VanillaCreateCircuitMsg to the next hop."""
|
|
|
|
def received_cell(self, circhandler, cell):
|
|
# Remove ourselves from handling a second
|
|
# VanillaExtendCircuitCell on this circuit
|
|
circhandler.replace_celltype_handler(simcell.VanillaExtendCircuitCell, None)
|
|
|
|
# Allocate a new circuit id to the requested next hop
|
|
channelmgr = circhandler.channel.channelmgr
|
|
nexthopchannel = channelmgr.get_channel_to(cell.hopaddr)
|
|
newcircid, newcirchandler = nexthopchannel.new_circuit()
|
|
|
|
# Connect the existing and new circuits together
|
|
circhandler.adjacent_circuit_handler = newcirchandler
|
|
newcirchandler.adjacent_circuit_handler = circhandler
|
|
|
|
# Set up a handler for when the VanillaCreatedCircuitCell comes
|
|
# back
|
|
newcirchandler.replace_celltype_handler(
|
|
simcell.VanillaCreatedCircuitCell,
|
|
VanillaCreatedRelayHandler())
|
|
|
|
# Forward a VanillaCreateCircuitMsg to the next hop
|
|
nexthopchannel.send_msg(
|
|
simmsg.VanillaCreateCircuitMsg(newcircid, cell.ntor_request))
|
|
|
|
|
|
class TelescopingExtendCircuitHandler:
|
|
"""A handler for TelescopingExtendCircuitCell cells. It allocates a new
|
|
circuit id on the Channel to the requested next hop, connects the
|
|
existing and new circuits together, and forwards a
|
|
TelescopingCreateCircuitMsg to the next hop."""
|
|
|
|
def __init__(self, relaypicker, current_relay_idkey):
|
|
self.relaypicker = relaypicker
|
|
self.current_relay_idkey = bytes(current_relay_idkey)
|
|
|
|
def received_cell(self, circhandler, cell):
|
|
# Remove ourselves from handling a second
|
|
# TelescopingExtendCircuitCell on this circuit
|
|
circhandler.replace_celltype_handler(simcell.TelescopingExtendCircuitCell, None)
|
|
|
|
# Find the SNIP corresponding to the index sent by the client
|
|
next_snip = self.relaypicker.pick_relay_by_uniform_index(cell.idx)
|
|
|
|
# Check to make sure that we aren't extending to ourselves. If we are,
|
|
# close the circuit.
|
|
if next_snip.snipdict["idkey"] == self.current_relay_idkey:
|
|
logging.debug("Client requested extending the circuit to a relay already in the path; aborting. my circid: %s", str(circhandler.circid))
|
|
circhandler.close()
|
|
return
|
|
|
|
|
|
# Allocate a new circuit id to the requested next hop
|
|
channelmgr = circhandler.channel.channelmgr
|
|
nexthopchannel = channelmgr.get_channel_to(next_snip.snipdict["addr"])
|
|
newcircid, newcirchandler = nexthopchannel.new_circuit()
|
|
|
|
# Connect the existing and new circuits together
|
|
circhandler.adjacent_circuit_handler = newcirchandler
|
|
newcirchandler.adjacent_circuit_handler = circhandler
|
|
|
|
|
|
# Set up a handler for when the TelescopingCreatedCircuitCell comes
|
|
# back
|
|
newcirchandler.replace_celltype_handler(
|
|
simcell.TelescopingCreatedCircuitCell,
|
|
TelescopingCreatedRelayHandler(next_snip))
|
|
|
|
# Forward a TelescopingCreateCircuitMsg to the next hop
|
|
nexthopchannel.send_msg(
|
|
simmsg.TelescopingCreateCircuitMsg(newcircid, cell.ntor_request))
|
|
|
|
|
|
class VanillaCreatedRelayHandler:
|
|
"""Handle a VanillaCreatedCircuitCell received by a _relay_ that
|
|
recently received a VanillaExtendCircuitCell from a client, and so
|
|
forwarded a VanillaCreateCircuitCell to the next hop."""
|
|
|
|
def received_cell(self, circhandler, cell):
|
|
# Remove ourselves from handling a second
|
|
# VanillaCreatedCircuitCell on this circuit
|
|
circhandler.replace_celltype_handler(simcell.VanillaCreatedCircuitCell, None)
|
|
|
|
# Just forward a VanillaExtendedCircuitCell back towards the
|
|
# client
|
|
circhandler.adjacent_circuit_handler.send_cell(
|
|
simcell.VanillaExtendedCircuitCell(cell.ntor_reply))
|
|
|
|
|
|
class TelescopingCreatedRelayHandler:
|
|
"""Handle a TelescopingCreatedCircuitCell received by a _relay_ that
|
|
recently received a TelescopingExtendCircuitCell from a client, and so
|
|
forwarded a TelescopingCreateCircuitCell to the next hop."""
|
|
|
|
def __init__(self, next_snip):
|
|
self.next_snip = next_snip
|
|
|
|
def received_cell(self, circhandler, cell):
|
|
logging.debug("Handle a TelescopingCreatedCircuit received by a relay")
|
|
# Remove ourselves from handling a second
|
|
# TelescopingCreatedCircuitCell on this circuit
|
|
circhandler.replace_celltype_handler(simcell.TelescopingCreatedCircuitCell, None)
|
|
|
|
# Just forward a TelescopingExtendedCircuitCell back towards the
|
|
# client
|
|
circhandler.adjacent_circuit_handler.send_cell(
|
|
simcell.TelescopingExtendedCircuitCell(cell.ntor_reply, self.next_snip))
|
|
|
|
|
|
class SinglePassCreatedRelayHandler:
|
|
"""Handle a SinglePassCreatedCircuitCell received by a _relay_ that
|
|
recently received a SinglePassCreateCircuitMsg from this relay."""
|
|
|
|
def __init__(self, ntorreply, next_snip, vrf_output, createdkey):
|
|
self.ntorreply = ntorreply
|
|
self.next_snip = next_snip
|
|
self.vrf_output = vrf_output
|
|
self.createdkey = createdkey
|
|
|
|
def received_cell(self, circhandler, cell):
|
|
logging.debug("Handle a SinglePassCreatedCircuitCell received by a relay")
|
|
# Remove ourselves from handling a second
|
|
# SinglePassCreatedCircuitCell on this circuit
|
|
circhandler.replace_celltype_handler(simcell.SinglePassCreatedCircuitCell, None)
|
|
logging.debug("Sending a SinglePassCreatedCell after receiving one from %s", self.next_snip.snipdict['addr'])
|
|
|
|
# Forward a SinglePassCreatedCircuitCell back towards the client
|
|
circhandler.adjacent_circuit_handler.channel_send_cell(
|
|
simcell.SinglePassCreatedCircuitCell(self.ntorreply,
|
|
simcell.SinglePassCreatedEnc(self.createdkey, self.next_snip,
|
|
self.vrf_output, cell)))
|
|
|
|
|
|
class CircuitHandler:
|
|
"""A class for managing sending and receiving encrypted cells on a
|
|
particular circuit."""
|
|
|
|
class NoCryptLayer:
|
|
def encrypt_msg(self, message): return message
|
|
def decrypt_msg(self, message): return message
|
|
|
|
class CryptLayer:
|
|
def __init__(self, enckey, deckey, next_layer):
|
|
self.enckey = enckey
|
|
self.deckey = deckey
|
|
self.next_layer = next_layer
|
|
def encrypt_msg(self, message):
|
|
return self.next_layer.encrypt_msg(simcell.EncryptedCell(self.enckey, message))
|
|
def decrypt_msg(self, message):
|
|
return self.next_layer.decrypt_msg(message).decrypt(self.deckey)
|
|
|
|
def __init__(self, channel, circid):
|
|
self.channel = channel
|
|
self.circid = circid
|
|
# The list of relay descriptors that form the circuit so far
|
|
# (client side only)
|
|
self.circuit_descs = []
|
|
# The dispatch table is indexed by type, and the values are
|
|
# objects with received_cell(circhandler, cell) methods.
|
|
self.cell_dispatch_table = dict()
|
|
# The topmost crypt layer. This is an object with
|
|
# encrypt_msg(msg) and decrypt_msg(msg) methods that returns the
|
|
# en/decrypted messages respectively. Messages are encrypted
|
|
# starting with the last layer that was added (the keys for the
|
|
# furthest away relay in the circuit) and are decrypted starting
|
|
# with the first layer that was added (the keys for the guard).
|
|
self.crypt_layer = self.NoCryptLayer()
|
|
# The adjacent CircuitHandler that's connected to this one. If
|
|
# we get a cell on one, we forward it to the other (if it's not
|
|
# meant for us to handle directly)
|
|
self.adjacent_circuit_handler = None
|
|
# The function to call when this circuit closes
|
|
self.closer = lambda: self.channel.circuithandlers.pop(circid)
|
|
|
|
def close(self):
|
|
"""Close the circuit. Sends a CloseCell on the circuit (and its
|
|
adjacent circuit, if present) and closes both."""
|
|
adjcirchandler = self.adjacent_circuit_handler
|
|
self.adjacent_circuit_handler = None
|
|
logging.debug("Closing circuit. circid: %s", str(self.circid))
|
|
if adjcirchandler is not None:
|
|
adjcirchandler.adjacent_circuit_handler = None
|
|
self.closer()
|
|
self.channel_send_cell(simcell.CloseCell())
|
|
if adjcirchandler is not None:
|
|
adjcirchandler.closer()
|
|
adjcirchandler.channel_send_cell(simcell.CloseCell())
|
|
|
|
def send_cell(self, cell):
|
|
"""Send a cell on this circuit, encrypting as needed."""
|
|
self.channel_send_cell(self.crypt_layer.encrypt_msg(cell))
|
|
|
|
def channel_send_cell(self, cell):
|
|
"""Send a cell on this circuit directly without encrypting it
|
|
first."""
|
|
self.channel.send_msg(simmsg.CircuitCellMsg(self.circid, cell))
|
|
|
|
def received_cell(self, cell):
|
|
"""A cell has been received on this circuit. Dispatch it
|
|
according to its type."""
|
|
if isinstance(cell, simcell.EncryptedCell):
|
|
cell = self.crypt_layer.decrypt_msg(cell)
|
|
|
|
logging.debug("CircuitHandler: %s received cell %s on circuit %d from %s" % (self.channel.channelmgr.myaddr, cell, self.circid, self.channel.peer.channelmgr.myaddr))
|
|
|
|
# If it's still encrypted, it's for sure meant for forwarding to
|
|
# our adjacent hop, which had better exist.
|
|
if isinstance(cell, simcell.EncryptedCell):
|
|
self.adjacent_circuit_handler.send_cell(cell)
|
|
else:
|
|
# This is a plaintext cell meant for us. Handle it
|
|
# according to the table.
|
|
celltype = type(cell)
|
|
if celltype in self.cell_dispatch_table:
|
|
self.cell_dispatch_table[celltype].received_cell(self, cell)
|
|
|
|
elif isinstance(cell, simcell.StringCell):
|
|
# Default handler; just print the message in the cell
|
|
logging.debug("CircuitHandler: %s received '%s' on circuit %d from %s" \
|
|
% (self.channel.channelmgr.myaddr, cell,
|
|
self.circid, self.channel.peer.channelmgr.myaddr))
|
|
|
|
elif isinstance(cell, simcell.CloseCell):
|
|
logging.debug("Received CloseCell on circuit %s", str(self.circid))
|
|
# Forward the CloseCell (without encryption) to the
|
|
# adjacent circuit, if any, and close both this and the
|
|
# adjacent circuit
|
|
adjcirchandler = self.adjacent_circuit_handler
|
|
self.adjacent_circuit_handler = None
|
|
if adjcirchandler is not None:
|
|
adjcirchandler.adjacent_circuit_handler = None
|
|
self.closer()
|
|
if adjcirchandler is not None:
|
|
adjcirchandler.closer()
|
|
adjcirchandler.channel_send_cell(cell)
|
|
|
|
else:
|
|
# I don't know how to handle this cell?
|
|
raise ValueError("CircuitHandler: %s received unknown cell type %s on circuit %d from %s" \
|
|
% (self.channel.channelmgr.myaddr, cell,
|
|
self.circid, self.channel.peer.channelmgr.myaddr))
|
|
|
|
def replace_celltype_handler(self, celltype, handler):
|
|
"""Add an object with a received_cell(circhandler, cell) method
|
|
to the cell dispatch table. It replaces anything that's already
|
|
there. Passing None as the handler removes the dispatcher for
|
|
that cell type."""
|
|
if handler is None:
|
|
del self.cell_dispatch_table[celltype]
|
|
else:
|
|
self.cell_dispatch_table[celltype] = handler
|
|
|
|
def add_crypt_layer(self, enckey, deckey):
|
|
"""Add a processing layer to this CircuitHandler so that cells
|
|
we send will get encrypted with the first given key, and cells
|
|
we receive will be decrypted with the other given key."""
|
|
current_crypt_layer = self.crypt_layer
|
|
self.crypt_layer = self.CryptLayer(enckey, deckey, current_crypt_layer)
|
|
|
|
|
|
class Channel(Connection):
|
|
"""A class representing a channel between a relay and either a
|
|
client or a relay, transporting cells from various circuits."""
|
|
def __init__(self):
|
|
super().__init__()
|
|
# The RelayChannelManager managing this Channel
|
|
self.channelmgr = None
|
|
# The Channel at the other end
|
|
self.peer = None
|
|
# The function to call when the connection closes
|
|
self.closer = lambda: None
|
|
# The next circuit id to use on this channel. The party that
|
|
# opened the channel uses even numbers; the receiving party uses
|
|
# odd numbers.
|
|
self.next_circid = None
|
|
# A map for CircuitHandlers to use for each open circuit on the
|
|
# channel
|
|
self.circuithandlers = dict()
|
|
|
|
def closed(self):
|
|
# Close each circuithandler we're managing
|
|
while self.circuithandlers:
|
|
chitems = iter(self.circuithandlers.items())
|
|
circid, circhandler = next(chitems)
|
|
logging.debug('closing circuit %s', circid)
|
|
circhandler.close()
|
|
self.closer()
|
|
self.peer = None
|
|
|
|
def close(self):
|
|
peer = self.peer
|
|
self.closed()
|
|
if peer is not None and peer is not self:
|
|
peer.closed()
|
|
|
|
def new_circuit(self):
|
|
"""Allocate a new circuit on this channel, returning the new
|
|
circuit's id and the new CircuitHandler."""
|
|
circid = self.next_circid
|
|
self.next_circid += 2
|
|
circuithandler = CircuitHandler(self, circid)
|
|
self.circuithandlers[circid] = circuithandler
|
|
return circid, circuithandler
|
|
|
|
def is_circuit_open(self, circid):
|
|
is_open = (circid in self.circuithandlers) and (self.circuithandlers[circid] is not None)
|
|
return is_open
|
|
|
|
def new_circuit_with_circid(self, circid):
|
|
"""Allocate a new circuit on this channel, with the circuit id
|
|
received from our peer. Return the new CircuitHandler"""
|
|
circuithandler = CircuitHandler(self, circid)
|
|
self.circuithandlers[circid] = circuithandler
|
|
return circuithandler
|
|
|
|
def send_cell(self, circid, cell):
|
|
"""Send the given message on the given circuit, encrypting or
|
|
decrypting as needed."""
|
|
self.circuithandlers[circid].send_cell(cell)
|
|
|
|
def send_raw_cell(self, circid, cell):
|
|
"""Send the given message, tagged for the given circuit id. No
|
|
encryption or decryption is done."""
|
|
self.send_msg(simmsg.CircuitCellMsg(self.circid, self.cell))
|
|
|
|
def send_msg(self, message):
|
|
"""Send the given NetMsg on the channel."""
|
|
self.channelmgr.perfstats.bytes_sent += message.size()
|
|
self.peer.received(self.channelmgr.myaddr, message)
|
|
|
|
def received(self, peeraddr, message):
|
|
"""Callback when a message is received from the network."""
|
|
logging.debug('Channel: %s received %s from %s' % (self.channelmgr.myaddr, message, peeraddr))
|
|
self.channelmgr.perfstats.bytes_received += message.size()
|
|
if isinstance(message, simmsg.CircuitCellMsg):
|
|
circid, cell = message.circid, message.cell
|
|
self.circuithandlers[circid].received_cell(cell)
|
|
else:
|
|
self.channelmgr.received_msg(message, peeraddr, self)
|
|
|
|
|
|
class ChannelManager:
|
|
"""The class that manages the channels to other relays and clients.
|
|
Relays and clients both use subclasses of this class to both create
|
|
on-demand channels to relays, to gracefully handle the closing of
|
|
channels, and to handle commands received over the channels."""
|
|
|
|
def __init__(self, myaddr, dirauthaddrs, perfstats):
|
|
# A dictionary of Channels to other hosts, indexed by NetAddr
|
|
self.channels = dict()
|
|
self.myaddr = myaddr
|
|
self.dirauthaddrs = dirauthaddrs
|
|
self.consensus = None
|
|
self.relaypicker = None
|
|
self.perfstats = perfstats
|
|
|
|
def terminate(self):
|
|
"""Close all connections we're managing."""
|
|
while self.channels:
|
|
channelitems = iter(self.channels.items())
|
|
addr, channel = next(channelitems)
|
|
logging.debug('closing channel %s %s', addr, channel)
|
|
channel.close()
|
|
|
|
def add_channel(self, channel, peeraddr):
|
|
"""Add the given channel to the list of channels we are
|
|
managing. If we are already managing a channel to the same
|
|
peer, close it first."""
|
|
if peeraddr in self.channels:
|
|
self.channels[peeraddr].close()
|
|
|
|
channel.channelmgr = self
|
|
self.channels[peeraddr] = channel
|
|
channel.closer = lambda: self.channels.pop(peeraddr)
|
|
|
|
def get_channel_to(self, addr):
|
|
"""Get the Channel connected to the given NetAddr, creating one
|
|
if none exists right now."""
|
|
if addr in self.channels:
|
|
return self.channels[addr]
|
|
|
|
# Create the new channel
|
|
logging.debug('getting channel from %s to %s',self.myaddr,addr)
|
|
newchannel = network.thenetwork.connect(self.myaddr, addr,
|
|
self.perfstats)
|
|
logging.debug('got channel from %s to %s',self.myaddr,addr)
|
|
self.channels[addr] = newchannel
|
|
newchannel.closer = lambda: self.channels.pop(addr)
|
|
newchannel.channelmgr = self
|
|
|
|
return newchannel
|
|
|
|
def received_msg(self, message, peeraddr, channel):
|
|
"""Callback when a NetMsg not specific to a circuit is
|
|
received."""
|
|
logging.debug("ChannelManager: Node %s received msg %s from %s" % (self.myaddr, message, peeraddr))
|
|
|
|
def received_cell(self, circid, cell, peeraddr, channel):
|
|
"""Callback with a circuit-specific cell is received."""
|
|
logging.debug("ChannelManager: Node %s received cell on circ %d: %s from %s" % (self.myaddr, circid, cell, peeraddr))
|
|
|
|
def send_msg(self, message, peeraddr):
|
|
"""Send a message to the peer with the given address."""
|
|
channel = self.get_channel_to(peeraddr)
|
|
channel.send_msg(message)
|
|
|
|
def send_cell(self, circid, cell, peeraddr):
|
|
"""Send a cell on the given circuit to the peer with the given
|
|
address."""
|
|
channel = self.get_channel_to(peeraddr)
|
|
channel.send_cell(circid, cell)
|
|
|
|
|
|
class RelayChannelManager(ChannelManager):
|
|
"""The subclass of ChannelManager for relays."""
|
|
|
|
def __init__(self, myaddr, dirauthaddrs, onionprivkey, idpubkey,
|
|
desc_getter, path_selection_key_getter, perfstats):
|
|
super().__init__(myaddr, dirauthaddrs, perfstats)
|
|
self.onionkey = onionprivkey
|
|
self.idpubkey = idpubkey
|
|
if network.thenetwork.womode != network.WOMode.VANILLA:
|
|
self.endive = None
|
|
self.desc_getter = desc_getter
|
|
|
|
if network.thenetwork.womode == network.WOMode.SINGLEPASS:
|
|
self.path_selection_key_getter = path_selection_key_getter
|
|
|
|
def get_consensus(self):
|
|
"""Download a fresh consensus (and ENDIVE if using Walking
|
|
Onions) from a random dirauth."""
|
|
a = random.choice(self.dirauthaddrs)
|
|
c = network.thenetwork.connect(self, a, self.perfstats)
|
|
if network.thenetwork.womode == network.WOMode.VANILLA:
|
|
if self.consensus is not None and \
|
|
len(self.consensus.consdict['relays']) > 0:
|
|
self.consensus = c.getconsensusdiff()
|
|
else:
|
|
self.consensus = c.getconsensus()
|
|
|
|
logging.debug("Relay %s requests consensus from %s", self.myaddr, str(network.thenetwork.dirauthkeys()))
|
|
self.relaypicker = dirauth.Consensus.verify(self.consensus,
|
|
network.thenetwork.dirauthkeys(), self.perfstats)
|
|
else:
|
|
self.consensus = c.getconsensus()
|
|
if self.endive is not None and \
|
|
len(self.endive.enddict['snips']) > 0:
|
|
self.endive = c.getendivediff()
|
|
else:
|
|
self.endive = c.getendive()
|
|
self.relaypicker = dirauth.ENDIVE.verify(self.endive,
|
|
self.consensus, network.thenetwork.dirauthkeys(),
|
|
self.perfstats)
|
|
c.close()
|
|
|
|
def received_msg(self, message, peeraddr, channel):
|
|
"""Callback when a NetMsg not specific to a circuit is
|
|
received."""
|
|
logging.debug("RelayChannelManager: Node %s received msg %s from %s" % (self.myaddr, message, peeraddr))
|
|
if isinstance(message, simmsg.RelayRandomHopMsg):
|
|
if message.ttl > 0:
|
|
# Pick a random next hop from the consensus
|
|
nexthop = self.relaypicker.pick_weighted_relay()
|
|
if network.thenetwork.womode == network.WOMode.VANILLA:
|
|
nextaddr = nexthop.descdict["addr"]
|
|
else:
|
|
nextaddr = nexthop.snipdict["addr"]
|
|
self.send_msg(simmsg.RelayRandomHopMsg(message.ttl-1), nextaddr)
|
|
elif isinstance(message, simmsg.RelayGetConsensusMsg):
|
|
self.send_msg(simmsg.RelayConsensusMsg(self.consensus), peeraddr)
|
|
elif isinstance(message, simmsg.RelayGetConsensusDiffMsg):
|
|
self.send_msg(simmsg.RelayConsensusDiffMsg(self.consensus), peeraddr)
|
|
elif isinstance(message, simmsg.RelayGetDescMsg):
|
|
self.send_msg(simmsg.RelayDescMsg(self.desc_getter()), peeraddr)
|
|
elif isinstance(message, simmsg.VanillaCreateCircuitMsg):
|
|
# A new circuit has arrived
|
|
circhandler = channel.new_circuit_with_circid(message.circid)
|
|
# Create the ntor reply
|
|
reply, secret = NTor.reply(self.onionkey, self.idpubkey,
|
|
message.ntor_request, self.perfstats)
|
|
# Set up the circuit to use the shared secret
|
|
enckey = nacl.hash.sha256(secret + b'downstream')
|
|
deckey = nacl.hash.sha256(secret + b'upstream')
|
|
circhandler.add_crypt_layer(enckey, deckey)
|
|
# Add a handler for if an Extend Cell arrives (there should
|
|
# be at most one on this circuit).
|
|
circhandler.replace_celltype_handler(
|
|
simcell.VanillaExtendCircuitCell, VanillaExtendCircuitHandler())
|
|
# Send the ntor reply
|
|
self.send_msg(simmsg.CircuitCellMsg(message.circid,
|
|
simcell.VanillaCreatedCircuitCell(reply)), peeraddr)
|
|
elif isinstance(message, simmsg.TelescopingCreateCircuitMsg):
|
|
# A new circuit has arrived
|
|
circhandler = channel.new_circuit_with_circid(message.circid)
|
|
# Create the ntor reply
|
|
reply, secret = NTor.reply(self.onionkey, self.idpubkey,
|
|
message.ntor_request, self.perfstats)
|
|
# Set up the circuit to use the shared secret
|
|
enckey = nacl.hash.sha256(secret + b'downstream')
|
|
deckey = nacl.hash.sha256(secret + b'upstream')
|
|
circhandler.add_crypt_layer(enckey, deckey)
|
|
# Add a handler for if an Extend Cell arrives (there should
|
|
# be at most one on this circuit).
|
|
circhandler.replace_celltype_handler(
|
|
simcell.TelescopingExtendCircuitCell,
|
|
TelescopingExtendCircuitHandler(self.relaypicker,
|
|
self.idpubkey))
|
|
|
|
# Send the ntor reply
|
|
self.send_msg(simmsg.CircuitCellMsg(message.circid,
|
|
simcell.TelescopingCreatedCircuitCell(reply)), peeraddr)
|
|
elif isinstance(message, simmsg.SinglePassCreateCircuitMsg) and message.ttl == 0:
|
|
# we are the end of the circuit, just establish a shared key and
|
|
# return
|
|
logging.debug("RelayChannelManager: Single-Pass TTL is 0, replying without extending")
|
|
|
|
# A new circuit has arrived
|
|
circhandler = channel.new_circuit_with_circid(message.circid)
|
|
|
|
# Create the ntor reply
|
|
reply, secret = NTor.reply(self.onionkey, self.idpubkey,
|
|
message.ntor_request, self.perfstats)
|
|
# Set up the circuit to use the shared secret
|
|
enckey = nacl.hash.sha256(secret + b'downstream')
|
|
deckey = nacl.hash.sha256(secret + b'upstream')
|
|
circhandler.add_crypt_layer(enckey, deckey)
|
|
|
|
# Send the ntor reply, but no need to send the snip for the next
|
|
# relay or vrf proof, as this is the last relay in the circuit.
|
|
self.send_msg(simmsg.CircuitCellMsg(message.circid,
|
|
simcell.SinglePassCreatedCircuitCell(reply, None)), peeraddr)
|
|
|
|
elif isinstance(message, simmsg.SinglePassCreateCircuitMsg) and message.ttl > 0:
|
|
logging.debug("RelayChannelManager: Single-Pass TTL is greater than 0; extending")
|
|
|
|
# A new circuit has arrived
|
|
circhandler = channel.new_circuit_with_circid(message.circid)
|
|
|
|
# Create the ntor reply for the circuit-extension key, and derive
|
|
# the client's next blinded key
|
|
(ntorreply, secret), blinded_client_encr_key = \
|
|
NTor.reply(self.onionkey, self.idpubkey,
|
|
message.ntor_request, self.perfstats, b'circuit')
|
|
|
|
# Set up the circuit to use the shared secret established from the
|
|
# circuit extension key
|
|
enckey = nacl.hash.sha256(secret + b'downstream')
|
|
deckey = nacl.hash.sha256(secret + b'upstream')
|
|
createdkey = nacl.hash.sha256(secret + b'created')
|
|
|
|
# Here, we will directly extend the circuit ourselves, after
|
|
# determining the next relay using the client's path selection
|
|
# key in conjunction with our own
|
|
pathsel_rand, blinded_client_path_selection_key = \
|
|
Sphinx.server(nacl.public.PublicKey(message.clipathselkey),
|
|
self.onionkey, b'pathsel', False, self.perfstats)
|
|
|
|
pathselkey = self.path_selection_key_getter()
|
|
# Simulate the VRF output for now (but it has the right
|
|
# size, and charges the right number of group operations to
|
|
# the perfstats)
|
|
vrf_output = VRF.get_output(pathselkey, pathsel_rand,
|
|
self.perfstats)
|
|
|
|
index = int.from_bytes(vrf_output[0][:4], 'big', signed=False)
|
|
|
|
next_hop = self.relaypicker.pick_relay_by_uniform_index(index)
|
|
if next_hop == None:
|
|
logging.debug("Client requested extending the circuit to a relay index that results in None, aborting. my circid: %s", str(circhandler.circid))
|
|
circhandler.close()
|
|
elif next_hop.snipdict["idkey"] == bytes(self.idpubkey):
|
|
logging.debug("Client requested extending the circuit to a relay already in the path; aborting. my circid: %s", str(circhandler.circid))
|
|
circhandler.close()
|
|
return
|
|
|
|
# Allocate a new circuit id to the requested next hop
|
|
channelmgr = circhandler.channel.channelmgr
|
|
nexthopchannel = channelmgr.get_channel_to(next_hop.snipdict["addr"])
|
|
newcircid, newcirchandler = nexthopchannel.new_circuit()
|
|
|
|
# Connect the existing and new circuits together
|
|
circhandler.adjacent_circuit_handler = newcirchandler
|
|
newcirchandler.adjacent_circuit_handler = circhandler
|
|
|
|
# Add a handler for once the next relay replies to say that the
|
|
# circuit has been created
|
|
newcirchandler.replace_celltype_handler(
|
|
simcell.SinglePassCreatedCircuitCell,
|
|
SinglePassCreatedRelayHandler(ntorreply, next_hop,
|
|
vrf_output, createdkey))
|
|
|
|
# Send the next create message to the next hop
|
|
nexthopchannel.send_msg(simmsg.SinglePassCreateCircuitMsg(newcircid,
|
|
blinded_client_encr_key, blinded_client_path_selection_key,
|
|
message.ttl-1))
|
|
|
|
# Now set up the crypto
|
|
circhandler.add_crypt_layer(enckey, deckey)
|
|
else:
|
|
return super().received_msg(message, peeraddr, channel)
|
|
|
|
def received_cell(self, circid, cell, peeraddr, channel):
|
|
"""Callback with a circuit-specific cell is received."""
|
|
logging.debug("RelayChannelManager: Node %s received cell on circ %d: %s from %s" % (self.myaddr, circid, cell, peeraddr))
|
|
return super().received_cell(circid, cell, peeraddr, channel)
|
|
|
|
|
|
class Relay(network.Server):
|
|
"""The class representing an onion relay."""
|
|
|
|
def __init__(self, dirauthaddrs, bw, flags):
|
|
idkey = nacl.signing.SigningKey.generate()
|
|
name = idkey.verify_key.encode(encoder=nacl.encoding.HexEncoder).decode("ascii")
|
|
|
|
# Gather performance statistics
|
|
self.perfstats = network.PerfStats(network.EntType.RELAY, bw)
|
|
self.perfstats.is_bootstrapping = True
|
|
|
|
# Create the identity and onion keys
|
|
self.idkey = idkey
|
|
self.onionkey = nacl.public.PrivateKey.generate()
|
|
self.perfstats.keygens += 2
|
|
self.name = name
|
|
|
|
# Bind to the network to get a network address
|
|
self.netaddr = network.thenetwork.bind(self)
|
|
self.perfstats.name = "Relay at %s" % self.netaddr
|
|
|
|
# Our bandwidth and flags
|
|
self.bw = bw
|
|
self.flags = flags
|
|
|
|
# Register for epoch change notification
|
|
network.thenetwork.wantepochticks(self, True, priority=True, end=True)
|
|
network.thenetwork.wantepochticks(self, True, priority=True)
|
|
|
|
self.current_desc = None
|
|
self.next_desc = None
|
|
|
|
# Create the path selection key for Single-Pass Walking Onions
|
|
if network.thenetwork.womode == network.WOMode.SINGLEPASS:
|
|
self.path_selection_key = nacl.public.PrivateKey.generate()
|
|
self.next_path_selection_key = self.path_selection_key
|
|
self.perfstats.keygens += 1
|
|
else:
|
|
self.path_selection_key = None
|
|
|
|
# Create the RelayChannelManager connection manager
|
|
self.channelmgr = RelayChannelManager(self.netaddr, dirauthaddrs,
|
|
self.onionkey, self.idkey.verify_key,
|
|
lambda: self.current_desc, lambda: self.path_selection_key,
|
|
self.perfstats)
|
|
|
|
# Initially, we're not a fallback relay
|
|
self.is_fallbackrelay = False
|
|
|
|
self.uploaddesc()
|
|
|
|
|
|
def terminate(self):
|
|
"""Stop this relay."""
|
|
|
|
if self.is_fallbackrelay:
|
|
# Fallback relays must not (for now) terminate
|
|
raise RelayFallbackTerminationError(self)
|
|
|
|
# Stop listening for epoch ticks
|
|
network.thenetwork.wantepochticks(self, False, priority=True, end=True)
|
|
network.thenetwork.wantepochticks(self, False, priority=True)
|
|
|
|
# Tell the dirauths we're going away
|
|
self.uploaddesc(False)
|
|
|
|
# Close connections to other relays
|
|
self.channelmgr.terminate()
|
|
|
|
# Stop listening to our own bound port
|
|
self.close()
|
|
|
|
|
|
def set_is_fallbackrelay(self, isfallback = True):
|
|
"""Set this relay to be a fallback relay (or unset if passed
|
|
False)."""
|
|
self.is_fallbackrelay = isfallback
|
|
|
|
def epoch_ending(self, epoch):
|
|
# Download the new consensus, which will have been created
|
|
# already since the dirauths' epoch_ending callbacks happened
|
|
# before the relays'.
|
|
self.channelmgr.get_consensus()
|
|
|
|
def newepoch(self, epoch):
|
|
# Rotate the path selection key for Single-Pass Walking Onions
|
|
if network.thenetwork.womode == network.WOMode.SINGLEPASS:
|
|
self.path_selection_key = self.next_path_selection_key
|
|
self.next_path_selection_key = nacl.public.PrivateKey.generate()
|
|
self.perfstats.keygens += 1
|
|
# Upload the descriptor for the *next* epoch (the one after the
|
|
# one that just started)
|
|
self.uploaddesc()
|
|
|
|
def uploaddesc(self, upload=True):
|
|
# Upload the descriptor for the epoch to come, or delete a
|
|
# previous upload if upload=False
|
|
descdict = dict()
|
|
descdict["epoch"] = network.thenetwork.getepoch() + 1
|
|
descdict["idkey"] = bytes(self.idkey.verify_key)
|
|
descdict["onionkey"] = bytes(self.onionkey.public_key)
|
|
descdict["addr"] = self.netaddr
|
|
descdict["bw"] = self.bw
|
|
descdict["flags"] = self.flags
|
|
|
|
if network.thenetwork.womode == network.WOMode.SINGLEPASS:
|
|
descdict["pathselkey"] = \
|
|
bytes(self.next_path_selection_key.public_key)
|
|
|
|
desc = dirauth.RelayDescriptor(descdict)
|
|
desc.sign(self.idkey, self.perfstats)
|
|
dirauth.RelayDescriptor.verify(desc, self.perfstats)
|
|
self.current_desc = self.next_desc
|
|
self.next_desc = desc
|
|
|
|
if upload:
|
|
descmsg = simmsg.DirAuthUploadDescMsg(desc)
|
|
else:
|
|
# Note that this relies on signatures being deterministic;
|
|
# otherwise we'd need to save the descriptor we uploaded
|
|
# before so we could tell the airauths to delete the exact
|
|
# one
|
|
descmsg = simmsg.DirAuthDelDescMsg(desc)
|
|
|
|
# Upload them
|
|
for a in self.channelmgr.dirauthaddrs:
|
|
c = network.thenetwork.connect(self, a, self.perfstats)
|
|
c.sendmsg(descmsg)
|
|
c.close()
|
|
|
|
def connected(self, peer):
|
|
"""Callback invoked when someone (client or relay) connects to
|
|
us. Create a pair of linked Channels and return the peer half
|
|
to the peer."""
|
|
|
|
# Create the linked pair
|
|
if peer is self.netaddr:
|
|
# A self-loop? We'll allow it.
|
|
peerchannel = Channel()
|
|
peerchannel.peer = peerchannel
|
|
peerchannel.next_circid = 2
|
|
return peerchannel
|
|
|
|
peerchannel = Channel()
|
|
ourchannel = Channel()
|
|
peerchannel.peer = ourchannel
|
|
peerchannel.next_circid = 2
|
|
ourchannel.peer = peerchannel
|
|
ourchannel.next_circid = 1
|
|
|
|
# Add our channel to the RelayChannelManager
|
|
self.channelmgr.add_channel(ourchannel, peer)
|
|
|
|
return peerchannel
|