[sans-io] add tunnel tests

This commit is contained in:
Maximilian Hils 2020-12-12 21:23:42 +01:00
parent f0bdf887fc
commit 8f49cde627
2 changed files with 297 additions and 20 deletions

View File

@ -24,6 +24,11 @@ class TunnelLayer(layer.Layer):
"""The 'inner' connection which provides data I/O""" """The 'inner' connection which provides data I/O"""
tunnel_state: TunnelState = TunnelState.INACTIVE tunnel_state: TunnelState = TunnelState.INACTIVE
command_to_reply_to: Optional[commands.OpenConnection] = None command_to_reply_to: Optional[commands.OpenConnection] = None
_event_queue: List[events.Event]
"""
If the connection already exists when we receive the start event,
we buffer commands until we have established the tunnel.
"""
def __init__( def __init__(
self, self,
@ -35,6 +40,7 @@ class TunnelLayer(layer.Layer):
self.tunnel_connection = tunnel_connection self.tunnel_connection = tunnel_connection
self.conn = conn self.conn = conn
self.child_layer = layer.NextLayer(self.context) self.child_layer = layer.NextLayer(self.context)
self._event_queue = []
def __repr__(self): def __repr__(self):
return f"{type(self).__name__}({self.tunnel_state.name.lower()})" return f"{type(self).__name__}({self.tunnel_state.name.lower()})"
@ -55,13 +61,12 @@ class TunnelLayer(layer.Layer):
if done: if done:
if self.conn != self.tunnel_connection: if self.conn != self.tunnel_connection:
self.conn.state = context.ConnectionState.OPEN self.conn.state = context.ConnectionState.OPEN
self.tunnel_state = TunnelState.OPEN
if err: if err:
self.tunnel_state = TunnelState.CLOSED if self.conn != self.tunnel_connection:
self.conn.state = context.ConnectionState.CLOSED
yield from self.on_handshake_error(err) yield from self.on_handshake_error(err)
if (done or err) and self.command_to_reply_to: if done or err:
yield from self.event_to_child(events.OpenConnectionReply(self.command_to_reply_to, err)) yield from self._handshake_finished(err)
self.command_to_reply_to = None
else: else:
yield from self.receive_data(event.data) yield from self.receive_data(event.data)
elif isinstance(event, events.ConnectionClosed): elif isinstance(event, events.ConnectionClosed):
@ -69,13 +74,32 @@ class TunnelLayer(layer.Layer):
if self.tunnel_state is TunnelState.OPEN: if self.tunnel_state is TunnelState.OPEN:
yield from self.receive_close() yield from self.receive_close()
elif self.tunnel_state is TunnelState.ESTABLISHING: elif self.tunnel_state is TunnelState.ESTABLISHING:
yield from self.on_handshake_error("connection closed without notice") err = "connection closed without notice"
else: yield from self.on_handshake_error(err)
yield from self._handshake_finished(err)
self.tunnel_state = TunnelState.CLOSED
else: # pragma: no cover
raise AssertionError(f"Unexpected event: {event}") raise AssertionError(f"Unexpected event: {event}")
else: else:
yield from self.event_to_child(event) yield from self.event_to_child(event)
def _handshake_finished(self, err: Optional[str]):
if err:
self.tunnel_state = TunnelState.CLOSED
else:
self.tunnel_state = TunnelState.OPEN
if self.command_to_reply_to:
yield from self.event_to_child(events.OpenConnectionReply(self.command_to_reply_to, err))
self.command_to_reply_to = None
else:
for evt in self._event_queue:
yield from self.event_to_child(evt)
self._event_queue.clear()
def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]:
if self.tunnel_state is TunnelState.ESTABLISHING and not self.command_to_reply_to:
self._event_queue.append(event)
return
for command in self.child_layer.handle_event(event): for command in self.child_layer.handle_event(event):
if isinstance(command, commands.ConnectionCommand) and command.connection == self.conn: if isinstance(command, commands.ConnectionCommand) and command.connection == self.conn:
if isinstance(command, commands.SendData): if isinstance(command, commands.SendData):
@ -89,14 +113,15 @@ class TunnelLayer(layer.Layer):
yield from self.send_close(command.half_close) yield from self.send_close(command.half_close)
elif isinstance(command, commands.OpenConnection): elif isinstance(command, commands.OpenConnection):
# create our own OpenConnection command object that blocks here. # create our own OpenConnection command object that blocks here.
self.command_to_reply_to = command
self.tunnel_state = TunnelState.ESTABLISHING self.tunnel_state = TunnelState.ESTABLISHING
err = yield commands.OpenConnection(self.tunnel_connection) err = yield commands.OpenConnection(self.tunnel_connection)
if err: if err:
yield from self.event_to_child(events.OpenConnectionReply(command, err)) yield from self.event_to_child(events.OpenConnectionReply(command, err))
self.tunnel_state = TunnelState.CLOSED
else: else:
self.command_to_reply_to = command
yield from self.start_handshake() yield from self.start_handshake()
else: else: # pragma: no cover
raise AssertionError(f"Unexpected command: {command}") raise AssertionError(f"Unexpected command: {command}")
else: else:
yield command yield command
@ -105,6 +130,7 @@ class TunnelLayer(layer.Layer):
yield from self._handle_event(events.DataReceived(self.tunnel_connection, b"")) yield from self._handle_event(events.DataReceived(self.tunnel_connection, b""))
def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]:
"""returns a (done, err) tuple"""
yield from () yield from ()
return True, None return True, None
@ -142,14 +168,3 @@ class LayerStack:
self._stack.append(other) self._stack.append(other)
return self return self
class OpenConnectionStub(layer.Layer):
done = False
err = None
def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]:
if isinstance(event, events.Start):
self.err = yield commands.OpenConnection(self.context.server)
self.done = not self.err
else:
self.err = event

View File

@ -0,0 +1,262 @@
from typing import Tuple, Optional
import pytest
from mitmproxy.proxy2 import tunnel, layer
from mitmproxy.proxy2.commands import SendData, Log, CloseConnection, OpenConnection
from mitmproxy.proxy2.context import Context, Server, ConnectionState
from mitmproxy.proxy2.events import Event, DataReceived, Start, ConnectionClosed
from test.mitmproxy.proxy2.tutils import Playbook, reply
class TChildLayer(layer.Layer):
child_layer: Optional[layer.Layer] = None
def _handle_event(self, event: Event) -> layer.CommandGenerator[None]:
if isinstance(event, Start):
yield Log(f"Got start. Server state: {self.context.server.state.name}")
elif isinstance(event, DataReceived) and event.data == b"client-hello":
yield SendData(self.context.client, b"client-hello-reply")
elif isinstance(event, DataReceived) and event.data == b"server-hello":
yield SendData(self.context.server, b"server-hello-reply")
elif isinstance(event, DataReceived) and event.data == b"open":
err = yield OpenConnection(self.context.server)
yield Log(f"Opened: {err=}. Server state: {self.context.server.state.name}")
elif isinstance(event, DataReceived) and event.data == b"half-close":
err = yield CloseConnection(event.connection, half_close=True)
elif isinstance(event, ConnectionClosed):
yield Log(f"Got {event.connection.__class__.__name__.lower()} close.")
yield CloseConnection(event.connection)
else:
raise AssertionError
class TTunnelLayer(tunnel.TunnelLayer):
def start_handshake(self) -> layer.CommandGenerator[None]:
yield SendData(self.tunnel_connection, b"handshake-hello")
def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]:
yield SendData(self.tunnel_connection, data)
if data == b"handshake-success":
return True, None
else:
return False, "handshake error"
def send_data(self, data: bytes) -> layer.CommandGenerator[None]:
yield SendData(self.tunnel_connection, b"tunneled-" + data)
def receive_data(self, data: bytes) -> layer.CommandGenerator[None]:
yield from self.event_to_child(
DataReceived(self.conn, data.replace(b"tunneled-", b""))
)
@pytest.mark.parametrize("success", ["success", "fail"])
def test_tunnel_handshake_start(tctx: Context, success):
server = Server(("proxy", 1234))
server.state = ConnectionState.OPEN
tl = TTunnelLayer(tctx, server, tctx.server)
tl.child_layer = TChildLayer(tctx)
assert repr(tl)
playbook = Playbook(tl, logs=True)
(
playbook
<< SendData(server, b"handshake-hello")
>> DataReceived(tctx.client, b"client-hello")
>> DataReceived(server, b"handshake-" + success.encode())
<< SendData(server, b"handshake-" + success.encode())
)
if success == "success":
playbook << Log("Got start. Server state: OPEN")
else:
playbook << CloseConnection(server)
playbook << Log("Got start. Server state: CLOSED")
playbook << SendData(tctx.client, b"client-hello-reply")
if success == "success":
playbook >> DataReceived(server, b"tunneled-server-hello")
playbook << SendData(server, b"tunneled-server-hello-reply")
assert playbook
@pytest.mark.parametrize("success", ["success", "fail"])
def test_tunnel_handshake_command(tctx: Context, success):
server = Server(("proxy", 1234))
tl = TTunnelLayer(tctx, server, tctx.server)
tl.child_layer = TChildLayer(tctx)
playbook = Playbook(tl, logs=True)
(
playbook
<< Log("Got start. Server state: CLOSED")
>> DataReceived(tctx.client, b"client-hello")
<< SendData(tctx.client, b"client-hello-reply")
>> DataReceived(tctx.client, b"open")
<< OpenConnection(server)
>> reply(None)
<< SendData(server, b"handshake-hello")
>> DataReceived(server, b"handshake-" + success.encode())
<< SendData(server, b"handshake-" + success.encode())
)
if success == "success":
assert (
playbook
<< Log(f"Opened: err=None. Server state: OPEN")
>> DataReceived(server, b"tunneled-server-hello")
<< SendData(server, b"tunneled-server-hello-reply")
>> ConnectionClosed(tctx.client)
<< Log("Got client close.")
<< CloseConnection(tctx.client)
)
assert tl.tunnel_state is tunnel.TunnelState.OPEN
assert (
playbook
>> ConnectionClosed(server)
<< Log("Got server close.")
<< CloseConnection(server)
)
assert tl.tunnel_state is tunnel.TunnelState.CLOSED
else:
assert (
playbook
<< CloseConnection(server)
<< Log("Opened: err='handshake error'. Server state: CLOSED")
)
assert tl.tunnel_state is tunnel.TunnelState.CLOSED
def test_tunnel_default_impls(tctx: Context):
tctx.server.state = ConnectionState.OPEN
tl = tunnel.TunnelLayer(tctx, tctx.server, Server(None))
tl.child_layer = TChildLayer(tctx)
playbook = Playbook(tl, logs=True)
assert (
playbook
<< Log("Got start. Server state: OPEN")
>> DataReceived(tctx.server, b"server-hello")
<< SendData(tctx.server, b"server-hello-reply")
)
assert tl.tunnel_state is tunnel.TunnelState.OPEN
assert (
playbook
>> ConnectionClosed(tctx.server)
<< Log("Got server close.")
<< CloseConnection(tctx.server)
)
assert tl.tunnel_state is tunnel.TunnelState.CLOSED
assert (
playbook
>> DataReceived(tctx.client, b"open")
<< OpenConnection(tctx.server)
>> reply(None)
<< Log("Opened: err=None. Server state: OPEN")
>> DataReceived(tctx.server, b"half-close")
<< CloseConnection(tctx.server, half_close=True)
)
def test_tunnel_openconnection_error(tctx: Context):
server = Server(("proxy", 1234))
tl = TTunnelLayer(tctx, server, tctx.server)
tl.child_layer = TChildLayer(tctx)
playbook = Playbook(tl, logs=True)
assert (
playbook
<< Log("Got start. Server state: CLOSED")
>> DataReceived(tctx.client, b"open")
<< OpenConnection(server)
)
assert tl.tunnel_state is tunnel.TunnelState.ESTABLISHING
assert (
playbook
>> reply("IPoAC packet dropped.")
<< Log("Opened: err='IPoAC packet dropped.'. Server state: CLOSED")
)
assert tl.tunnel_state is tunnel.TunnelState.CLOSED
@pytest.mark.parametrize("disconnect", ["client", "server"])
def test_disconnect_during_handshake_start(tctx: Context, disconnect):
server = Server(("proxy", 1234))
server.state = ConnectionState.OPEN
tl = TTunnelLayer(tctx, server, tctx.server)
tl.child_layer = TChildLayer(tctx)
playbook = Playbook(tl, logs=True)
assert (
playbook
<< SendData(server, b"handshake-hello")
)
if disconnect == "client":
assert (
playbook
>> ConnectionClosed(tctx.client)
>> ConnectionClosed(server) # proxyserver will cancel all other connections as well.
<< CloseConnection(server)
<< Log("Got start. Server state: CLOSED")
<< Log("Got client close.")
<< CloseConnection(tctx.client)
)
else:
assert (
playbook
>> ConnectionClosed(server)
<< CloseConnection(server)
<< Log("Got start. Server state: CLOSED")
)
@pytest.mark.parametrize("disconnect", ["client", "server"])
def test_disconnect_during_handshake_command(tctx: Context, disconnect):
server = Server(("proxy", 1234))
tl = TTunnelLayer(tctx, server, tctx.server)
tl.child_layer = TChildLayer(tctx)
playbook = Playbook(tl, logs=True)
assert (
playbook
<< Log("Got start. Server state: CLOSED")
>> DataReceived(tctx.client, b"client-hello")
<< SendData(tctx.client, b"client-hello-reply")
>> DataReceived(tctx.client, b"open")
<< OpenConnection(server)
>> reply(None)
<< SendData(server, b"handshake-hello")
)
if disconnect == "client":
assert (
playbook
>> ConnectionClosed(tctx.client)
>> ConnectionClosed(server) # proxyserver will cancel all other connections as well.
<< CloseConnection(server)
<< Log("Opened: err='connection closed without notice'. Server state: CLOSED")
<< Log("Got client close.")
<< CloseConnection(tctx.client)
)
else:
assert (
playbook
>> ConnectionClosed(server)
<< CloseConnection(server)
<< Log("Opened: err='connection closed without notice'. Server state: CLOSED")
)
def test_layer_stack(tctx):
stack = tunnel.LayerStack()
a = TChildLayer(tctx)
b = TChildLayer(tctx)
stack /= a
stack /= b
assert stack[0] == a
assert a.child_layer is b