magic-wormhole/src/wormhole/test/dilate/test_manager.py
Brian Warner d1aefa815d fix subchannel open/close, add test
I think I just managed to forget that inbound_close requires we respond with
a close ourselves. Also outbound open means we must add the subchannel to the
inbound table, so we can receive any data on it at all.
2019-07-06 01:50:29 -07:00

668 lines
26 KiB
Python

from __future__ import print_function, unicode_literals
from zope.interface import alsoProvides
from twisted.trial import unittest
from twisted.internet.defer import Deferred
from twisted.internet.task import Clock, Cooperator
from twisted.internet.interfaces import IAddress
import mock
from ...eventual import EventualQueue
from ..._interfaces import ISend, IDilationManager, ITerminator
from ...util import dict_to_bytes
from ..._dilation import roles
from ..._dilation.manager import (Dilator, Manager, make_side,
OldPeerCannotDilateError,
UnknownDilationMessageType,
UnexpectedKCM,
UnknownMessageType)
from ..._dilation.connection import Open, Data, Close, Ack, KCM, Ping, Pong
from .common import clear_mock_calls
def make_dilator():
reactor = object()
clock = Clock()
eq = EventualQueue(clock)
term = mock.Mock(side_effect=lambda: True) # one write per Eventual tick
def term_factory():
return term
coop = Cooperator(terminationPredicateFactory=term_factory,
scheduler=eq.eventually)
send = mock.Mock()
alsoProvides(send, ISend)
dil = Dilator(reactor, eq, coop)
terminator = mock.Mock()
alsoProvides(terminator, ITerminator)
dil.wire(send, terminator)
return dil, send, reactor, eq, clock, coop
class TestDilator(unittest.TestCase):
def test_manager_and_endpoints(self):
dil, send, reactor, eq, clock, coop = make_dilator()
d1 = dil.dilate()
d2 = dil.dilate()
self.assertNoResult(d1)
self.assertNoResult(d2)
key = b"key"
transit_key = object()
with mock.patch("wormhole._dilation.manager.derive_key",
return_value=transit_key) as dk:
dil.got_key(key)
self.assertEqual(dk.mock_calls, [mock.call(key, b"dilation-v1", 32)])
self.assertIdentical(dil._transit_key, transit_key)
self.assertNoResult(d1)
self.assertNoResult(d2)
host_addr = dil._host_addr
peer_addr = object()
m_sca = mock.patch("wormhole._dilation.manager._SubchannelAddress",
return_value=peer_addr)
sc = mock.Mock()
m_sc = mock.patch("wormhole._dilation.manager.SubChannel",
return_value=sc)
scid0 = 0
m = mock.Mock()
alsoProvides(m, IDilationManager)
m.when_first_connected.return_value = wfc_d = Deferred()
with mock.patch("wormhole._dilation.manager.Manager",
return_value=m) as ml:
with mock.patch("wormhole._dilation.manager.make_side",
return_value="us"):
with m_sca, m_sc as m_sc_m:
dil.got_wormhole_versions({"can-dilate": ["1"]})
# that should create the Manager
self.assertEqual(ml.mock_calls, [mock.call(send, "us", transit_key,
None, reactor, eq, coop, host_addr, False)])
# and create subchannel0
self.assertEqual(m_sc_m.mock_calls,
[mock.call(scid0, m, host_addr, peer_addr)])
# and tell it to start, and get wait-for-it-to-connect Deferred
self.assertEqual(m.mock_calls, [mock.call.set_subchannel_zero(scid0, sc),
mock.call.start(),
mock.call.when_first_connected(),
])
clear_mock_calls(m)
self.assertNoResult(d1)
self.assertNoResult(d2)
ce = mock.Mock()
m_ce = mock.patch("wormhole._dilation.manager.ControlEndpoint",
return_value=ce)
lep = object()
m_sle = mock.patch("wormhole._dilation.manager.SubchannelListenerEndpoint",
return_value=lep)
with m_ce as m_ce_m, m_sle as m_sle_m:
wfc_d.callback(None)
eq.flush_sync()
self.assertEqual(m_ce_m.mock_calls, [mock.call(peer_addr)])
self.assertEqual(ce.mock_calls, [mock.call._subchannel_zero_opened(sc)])
self.assertEqual(m_sle_m.mock_calls, [mock.call(m, host_addr)])
self.assertEqual(m.mock_calls,
[mock.call.set_listener_endpoint(lep),
])
clear_mock_calls(m)
eps = self.successResultOf(d1)
self.assertEqual(eps, self.successResultOf(d2))
d3 = dil.dilate()
eq.flush_sync()
self.assertEqual(eps, self.successResultOf(d3))
# all subsequent DILATE-n messages should get passed to the manager
self.assertEqual(m.mock_calls, [])
pleasemsg = dict(type="please", side="them")
dil.received_dilate(dict_to_bytes(pleasemsg))
self.assertEqual(m.mock_calls, [mock.call.rx_PLEASE(pleasemsg)])
clear_mock_calls(m)
hintmsg = dict(type="connection-hints")
dil.received_dilate(dict_to_bytes(hintmsg))
self.assertEqual(m.mock_calls, [mock.call.rx_HINTS(hintmsg)])
clear_mock_calls(m)
# we're nominally the LEADER, and the leader would not normally be
# receiving a RECONNECT, but since we've mocked out the Manager it
# won't notice
dil.received_dilate(dict_to_bytes(dict(type="reconnect")))
self.assertEqual(m.mock_calls, [mock.call.rx_RECONNECT()])
clear_mock_calls(m)
dil.received_dilate(dict_to_bytes(dict(type="reconnecting")))
self.assertEqual(m.mock_calls, [mock.call.rx_RECONNECTING()])
clear_mock_calls(m)
dil.received_dilate(dict_to_bytes(dict(type="unknown")))
self.assertEqual(m.mock_calls, [])
self.flushLoggedErrors(UnknownDilationMessageType)
def test_peer_cannot_dilate(self):
dil, send, reactor, eq, clock, coop = make_dilator()
d1 = dil.dilate()
self.assertNoResult(d1)
dil._transit_key = b"\x01" * 32
dil.got_wormhole_versions({}) # missing "can-dilate"
eq.flush_sync()
f = self.failureResultOf(d1)
f.check(OldPeerCannotDilateError)
def test_disjoint_versions(self):
dil, send, reactor, eq, clock, coop = make_dilator()
d1 = dil.dilate()
self.assertNoResult(d1)
dil._transit_key = b"key"
dil.got_wormhole_versions({"can-dilate": [-1]})
eq.flush_sync()
f = self.failureResultOf(d1)
f.check(OldPeerCannotDilateError)
def test_early_dilate_messages(self):
dil, send, reactor, eq, clock, coop = make_dilator()
dil._transit_key = b"key"
d1 = dil.dilate()
host_addr = dil._host_addr
self.assertNoResult(d1)
pleasemsg = dict(type="please", side="them")
dil.received_dilate(dict_to_bytes(pleasemsg))
hintmsg = dict(type="connection-hints")
dil.received_dilate(dict_to_bytes(hintmsg))
m = mock.Mock()
alsoProvides(m, IDilationManager)
m.when_first_connected.return_value = Deferred()
scid0 = 0
sc = mock.Mock()
m_sc = mock.patch("wormhole._dilation.manager.SubChannel",
return_value=sc)
with mock.patch("wormhole._dilation.manager.Manager",
return_value=m) as ml:
with mock.patch("wormhole._dilation.manager.make_side",
return_value="us"):
with m_sc:
dil.got_wormhole_versions({"can-dilate": ["1"]})
self.assertEqual(ml.mock_calls, [mock.call(send, "us", b"key",
None, reactor, eq, coop, host_addr, False)])
self.assertEqual(m.mock_calls, [mock.call.set_subchannel_zero(scid0, sc),
mock.call.start(),
mock.call.rx_PLEASE(pleasemsg),
mock.call.rx_HINTS(hintmsg),
mock.call.when_first_connected()])
def test_transit_relay(self):
dil, send, reactor, eq, clock, coop = make_dilator()
dil._transit_key = b"key"
host_addr = dil._host_addr
relay = object()
d1 = dil.dilate(transit_relay_location=relay)
self.assertNoResult(d1)
scid0 = 0
sc = mock.Mock()
m_sc = mock.patch("wormhole._dilation.manager.SubChannel",
return_value=sc)
with mock.patch("wormhole._dilation.manager.Manager") as ml:
with mock.patch("wormhole._dilation.manager.make_side",
return_value="us"):
with m_sc:
dil.got_wormhole_versions({"can-dilate": ["1"]})
self.assertEqual(ml.mock_calls, [mock.call(send, "us", b"key",
relay, reactor, eq, coop, host_addr, False),
mock.call().set_subchannel_zero(scid0, sc),
mock.call().start(),
mock.call().when_first_connected()])
LEADER = "ff3456abcdef"
FOLLOWER = "123456abcdef"
def make_manager(leader=True):
class Holder:
pass
h = Holder()
h.send = mock.Mock()
alsoProvides(h.send, ISend)
if leader:
side = LEADER
else:
side = FOLLOWER
h.key = b"\x00" * 32
h.relay = None
h.reactor = object()
h.clock = Clock()
h.eq = EventualQueue(h.clock)
term = mock.Mock(side_effect=lambda: True) # one write per Eventual tick
def term_factory():
return term
h.coop = Cooperator(terminationPredicateFactory=term_factory,
scheduler=h.eq.eventually)
h.inbound = mock.Mock()
h.Inbound = mock.Mock(return_value=h.inbound)
h.outbound = mock.Mock()
h.Outbound = mock.Mock(return_value=h.outbound)
h.hostaddr = mock.Mock()
alsoProvides(h.hostaddr, IAddress)
with mock.patch("wormhole._dilation.manager.Inbound", h.Inbound):
with mock.patch("wormhole._dilation.manager.Outbound", h.Outbound):
m = Manager(h.send, side, h.key, h.relay, h.reactor, h.eq, h.coop, h.hostaddr)
return m, h
class TestManager(unittest.TestCase):
def test_make_side(self):
side = make_side()
self.assertEqual(type(side), type(u""))
self.assertEqual(len(side), 2 * 6)
def test_create(self):
m, h = make_manager()
def test_leader(self):
m, h = make_manager(leader=True)
self.assertEqual(h.send.mock_calls, [])
self.assertEqual(h.Inbound.mock_calls, [mock.call(m, h.hostaddr)])
self.assertEqual(h.Outbound.mock_calls, [mock.call(m, h.coop)])
m.start()
self.assertEqual(h.send.mock_calls, [
mock.call.send("dilate-0",
dict_to_bytes({"type": "please", "side": LEADER}))
])
clear_mock_calls(h.send)
wfc_d = m.when_first_connected()
self.assertNoResult(wfc_d)
# ignore early hints
m.rx_HINTS({})
self.assertEqual(h.send.mock_calls, [])
c = mock.Mock()
connector = mock.Mock(return_value=c)
with mock.patch("wormhole._dilation.manager.Connector", connector):
# receiving this PLEASE triggers creation of the Connector
m.rx_PLEASE({"side": FOLLOWER})
self.assertEqual(h.send.mock_calls, [])
self.assertEqual(connector.mock_calls, [
mock.call(b"\x00" * 32, None, m, h.reactor, h.eq,
False, # no_listen
None, # tor
None, # timing
LEADER, roles.LEADER),
])
self.assertEqual(c.mock_calls, [mock.call.start()])
clear_mock_calls(connector, c)
self.assertNoResult(wfc_d)
# now any inbound hints should get passed to our Connector
with mock.patch("wormhole._dilation.manager.parse_hint",
side_effect=["p1", None, "p3"]) as ph:
m.rx_HINTS({"hints": [1, 2, 3]})
self.assertEqual(ph.mock_calls, [mock.call(1), mock.call(2), mock.call(3)])
self.assertEqual(c.mock_calls, [mock.call.got_hints(["p1", "p3"])])
clear_mock_calls(ph, c)
# and we send out any (listening) hints from our Connector
m.send_hints([1, 2])
self.assertEqual(h.send.mock_calls, [
mock.call.send("dilate-1",
dict_to_bytes({"type": "connection-hints",
"hints": [1, 2]}))
])
clear_mock_calls(h.send)
# the first successful connection fires when_first_connected(), so
# the Dilator can create and return the endpoints
c1 = mock.Mock()
m.connector_connection_made(c1)
self.assertEqual(h.inbound.mock_calls, [mock.call.use_connection(c1)])
self.assertEqual(h.outbound.mock_calls, [mock.call.use_connection(c1)])
clear_mock_calls(h.inbound, h.outbound)
h.eq.flush_sync()
self.successResultOf(wfc_d) # fires with None
wfc_d2 = m.when_first_connected()
h.eq.flush_sync()
self.successResultOf(wfc_d2)
scid0 = 0
sc0 = mock.Mock()
m.set_subchannel_zero(scid0, sc0)
listen_ep = mock.Mock()
m.set_listener_endpoint(listen_ep)
self.assertEqual(h.inbound.mock_calls, [
mock.call.set_subchannel_zero(scid0, sc0),
mock.call.set_listener_endpoint(listen_ep),
])
clear_mock_calls(h.inbound)
# the Leader making a new outbound channel should get scid=1
scid1 = 1
self.assertEqual(m.allocate_subchannel_id(), scid1)
r1 = Open(10, scid1) # seqnum=10
h.outbound.build_record = mock.Mock(return_value=r1)
m.send_open(scid1)
self.assertEqual(h.outbound.mock_calls, [
mock.call.build_record(Open, scid1),
mock.call.queue_and_send_record(r1),
])
clear_mock_calls(h.outbound)
r2 = Data(11, scid1, b"data")
h.outbound.build_record = mock.Mock(return_value=r2)
m.send_data(scid1, b"data")
self.assertEqual(h.outbound.mock_calls, [
mock.call.build_record(Data, scid1, b"data"),
mock.call.queue_and_send_record(r2),
])
clear_mock_calls(h.outbound)
r3 = Close(12, scid1)
h.outbound.build_record = mock.Mock(return_value=r3)
m.send_close(scid1)
self.assertEqual(h.outbound.mock_calls, [
mock.call.build_record(Close, scid1),
mock.call.queue_and_send_record(r3),
])
clear_mock_calls(h.outbound)
# ack the OPEN
m.got_record(Ack(10))
self.assertEqual(h.outbound.mock_calls, [
mock.call.handle_ack(10)
])
clear_mock_calls(h.outbound)
# test that inbound records get acked and routed to Inbound
h.inbound.is_record_old = mock.Mock(return_value=False)
scid2 = 2
o200 = Open(200, scid2)
m.got_record(o200)
self.assertEqual(h.outbound.mock_calls, [
mock.call.send_if_connected(Ack(200))
])
self.assertEqual(h.inbound.mock_calls, [
mock.call.is_record_old(o200),
mock.call.update_ack_watermark(200),
mock.call.handle_open(scid2),
])
clear_mock_calls(h.outbound, h.inbound)
# old (duplicate) records should provoke new Acks, but not get
# forwarded
h.inbound.is_record_old = mock.Mock(return_value=True)
m.got_record(o200)
self.assertEqual(h.outbound.mock_calls, [
mock.call.send_if_connected(Ack(200))
])
self.assertEqual(h.inbound.mock_calls, [
mock.call.is_record_old(o200),
])
clear_mock_calls(h.outbound, h.inbound)
# check Data and Close too
h.inbound.is_record_old = mock.Mock(return_value=False)
d201 = Data(201, scid2, b"data")
m.got_record(d201)
self.assertEqual(h.outbound.mock_calls, [
mock.call.send_if_connected(Ack(201))
])
self.assertEqual(h.inbound.mock_calls, [
mock.call.is_record_old(d201),
mock.call.update_ack_watermark(201),
mock.call.handle_data(scid2, b"data"),
])
clear_mock_calls(h.outbound, h.inbound)
c202 = Close(202, scid2)
m.got_record(c202)
self.assertEqual(h.outbound.mock_calls, [
mock.call.send_if_connected(Ack(202))
])
self.assertEqual(h.inbound.mock_calls, [
mock.call.is_record_old(c202),
mock.call.update_ack_watermark(202),
mock.call.handle_close(scid2),
])
clear_mock_calls(h.outbound, h.inbound)
# Now we lose the connection. The Leader should tell the other side
# that we're reconnecting.
m.connector_connection_lost()
self.assertEqual(h.send.mock_calls, [
mock.call.send("dilate-2",
dict_to_bytes({"type": "reconnect"}))
])
self.assertEqual(h.inbound.mock_calls, [
mock.call.stop_using_connection()
])
self.assertEqual(h.outbound.mock_calls, [
mock.call.stop_using_connection()
])
clear_mock_calls(h.send, h.inbound, h.outbound)
# leader does nothing (stays in FLUSHING) until the follower acks by
# sending RECONNECTING
# inbound hints should be ignored during FLUSHING
with mock.patch("wormhole._dilation.manager.parse_hint",
return_value=None) as ph:
m.rx_HINTS({"hints": [1, 2, 3]})
self.assertEqual(ph.mock_calls, []) # ignored
c2 = mock.Mock()
connector2 = mock.Mock(return_value=c2)
with mock.patch("wormhole._dilation.manager.Connector", connector2):
# this triggers creation of a new Connector
m.rx_RECONNECTING()
self.assertEqual(h.send.mock_calls, [])
self.assertEqual(connector2.mock_calls, [
mock.call(b"\x00" * 32, None, m, h.reactor, h.eq,
False, # no_listen
None, # tor
None, # timing
LEADER, roles.LEADER),
])
self.assertEqual(c2.mock_calls, [mock.call.start()])
clear_mock_calls(connector2, c2)
self.assertEqual(h.inbound.mock_calls, [])
self.assertEqual(h.outbound.mock_calls, [])
# and a new connection should re-register with Inbound/Outbound,
# which are responsible for re-sending unacked queued messages
c3 = mock.Mock()
m.connector_connection_made(c3)
self.assertEqual(h.inbound.mock_calls, [mock.call.use_connection(c3)])
self.assertEqual(h.outbound.mock_calls, [mock.call.use_connection(c3)])
clear_mock_calls(h.inbound, h.outbound)
def test_follower(self):
m, h = make_manager(leader=False)
m.start()
self.assertEqual(h.send.mock_calls, [
mock.call.send("dilate-0",
dict_to_bytes({"type": "please", "side": FOLLOWER}))
])
clear_mock_calls(h.send)
c = mock.Mock()
connector = mock.Mock(return_value=c)
with mock.patch("wormhole._dilation.manager.Connector", connector):
# receiving this PLEASE triggers creation of the Connector
m.rx_PLEASE({"side": LEADER})
self.assertEqual(h.send.mock_calls, [])
self.assertEqual(connector.mock_calls, [
mock.call(b"\x00" * 32, None, m, h.reactor, h.eq,
False, # no_listen
None, # tor
None, # timing
FOLLOWER, roles.FOLLOWER),
])
self.assertEqual(c.mock_calls, [mock.call.start()])
clear_mock_calls(connector, c)
# get connected, then lose the connection
c1 = mock.Mock()
m.connector_connection_made(c1)
self.assertEqual(h.inbound.mock_calls, [mock.call.use_connection(c1)])
self.assertEqual(h.outbound.mock_calls, [mock.call.use_connection(c1)])
clear_mock_calls(h.inbound, h.outbound)
# now lose the connection. As the follower, we don't notify the
# leader, we just wait for them to notice
m.connector_connection_lost()
self.assertEqual(h.send.mock_calls, [])
self.assertEqual(h.inbound.mock_calls, [
mock.call.stop_using_connection()
])
self.assertEqual(h.outbound.mock_calls, [
mock.call.stop_using_connection()
])
clear_mock_calls(h.send, h.inbound, h.outbound)
# now we get a RECONNECT: we should send RECONNECTING
c2 = mock.Mock()
connector2 = mock.Mock(return_value=c2)
with mock.patch("wormhole._dilation.manager.Connector", connector2):
m.rx_RECONNECT()
self.assertEqual(h.send.mock_calls, [
mock.call.send("dilate-1",
dict_to_bytes({"type": "reconnecting"}))
])
self.assertEqual(connector2.mock_calls, [
mock.call(b"\x00" * 32, None, m, h.reactor, h.eq,
False, # no_listen
None, # tor
None, # timing
FOLLOWER, roles.FOLLOWER),
])
self.assertEqual(c2.mock_calls, [mock.call.start()])
clear_mock_calls(connector2, c2)
# while we're trying to connect, we get told to stop again, so we
# should abandon the connection attempt and start another
c3 = mock.Mock()
connector3 = mock.Mock(return_value=c3)
with mock.patch("wormhole._dilation.manager.Connector", connector3):
m.rx_RECONNECT()
self.assertEqual(c2.mock_calls, [mock.call.stop()])
self.assertEqual(connector3.mock_calls, [
mock.call(b"\x00" * 32, None, m, h.reactor, h.eq,
False, # no_listen
None, # tor
None, # timing
FOLLOWER, roles.FOLLOWER),
])
self.assertEqual(c3.mock_calls, [mock.call.start()])
clear_mock_calls(c2, connector3, c3)
m.connector_connection_made(c3)
# finally if we're already connected, rx_RECONNECT means we should
# abandon this connection (even though it still looks ok to us), then
# when the attempt is finished stopping, we should start another
m.rx_RECONNECT()
c4 = mock.Mock()
connector4 = mock.Mock(return_value=c4)
with mock.patch("wormhole._dilation.manager.Connector", connector4):
m.connector_connection_lost()
self.assertEqual(c3.mock_calls, [mock.call.disconnect()])
self.assertEqual(connector4.mock_calls, [
mock.call(b"\x00" * 32, None, m, h.reactor, h.eq,
False, # no_listen
None, # tor
None, # timing
FOLLOWER, roles.FOLLOWER),
])
self.assertEqual(c4.mock_calls, [mock.call.start()])
clear_mock_calls(c3, connector4, c4)
def test_mirror(self):
# receive a PLEASE with the same side as us: shouldn't happen
m, h = make_manager(leader=True)
m.start()
clear_mock_calls(h.send)
e = self.assertRaises(ValueError, m.rx_PLEASE, {"side": LEADER})
self.assertEqual(str(e), "their side shouldn't be equal: reflection?")
def test_ping_pong(self):
m, h = make_manager(leader=False)
m.got_record(KCM())
self.flushLoggedErrors(UnexpectedKCM)
m.got_record(Ping(1))
self.assertEqual(h.outbound.mock_calls,
[mock.call.send_if_connected(Pong(1))])
clear_mock_calls(h.outbound)
m.got_record(Pong(2))
# currently ignored, will eventually update a timer
m.got_record("not recognized")
e = self.flushLoggedErrors(UnknownMessageType)
self.assertEqual(len(e), 1)
self.assertEqual(str(e[0].value), "not recognized")
m.send_ping(3)
self.assertEqual(h.outbound.mock_calls,
[mock.call.send_if_connected(Pong(3))])
clear_mock_calls(h.outbound)
def test_subchannel(self):
m, h = make_manager(leader=True)
sc = object()
m.subchannel_pauseProducing(sc)
self.assertEqual(h.inbound.mock_calls, [
mock.call.subchannel_pauseProducing(sc)])
clear_mock_calls(h.inbound)
m.subchannel_resumeProducing(sc)
self.assertEqual(h.inbound.mock_calls, [
mock.call.subchannel_resumeProducing(sc)])
clear_mock_calls(h.inbound)
m.subchannel_stopProducing(sc)
self.assertEqual(h.inbound.mock_calls, [
mock.call.subchannel_stopProducing(sc)])
clear_mock_calls(h.inbound)
p = object()
streaming = object()
m.subchannel_registerProducer(sc, p, streaming)
self.assertEqual(h.outbound.mock_calls, [
mock.call.subchannel_registerProducer(sc, p, streaming)])
clear_mock_calls(h.outbound)
m.subchannel_unregisterProducer(sc)
self.assertEqual(h.outbound.mock_calls, [
mock.call.subchannel_unregisterProducer(sc)])
clear_mock_calls(h.outbound)
m.subchannel_closed(4, sc)
self.assertEqual(h.inbound.mock_calls, [
mock.call.subchannel_closed(4, sc)])
self.assertEqual(h.outbound.mock_calls, [
mock.call.subchannel_closed(4, sc)])
clear_mock_calls(h.inbound, h.outbound)