From 0d59b57791d4ab242d2d7be68e419bee46233694 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 13 Oct 2022 17:37:13 +0200 Subject: [PATCH 1/4] nits --- mitmproxy/proxy/layers/http/__init__.py | 5 ++++- mitmproxy/proxy/layers/modes.py | 11 ++++++----- mitmproxy/proxy/layers/quic.py | 12 ++++++++---- mitmproxy/tools/main.py | 1 + web/src/js/ducks/_options_gen.ts | 2 -- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/mitmproxy/proxy/layers/http/__init__.py b/mitmproxy/proxy/layers/http/__init__.py index d30b08f443..3cf117e017 100644 --- a/mitmproxy/proxy/layers/http/__init__.py +++ b/mitmproxy/proxy/layers/http/__init__.py @@ -45,6 +45,7 @@ from ._http1 import Http1Client, Http1Connection, Http1Server from ._http2 import Http2Client, Http2Server from ._http3 import Http3Client, Http3Server +from ..quic import QuicStreamEvent from ...context import Context from ...mode_specs import ReverseMode, UpstreamMode @@ -886,7 +887,7 @@ def _handle_event(self, event: events.Event): if isinstance(event, events.ConnectionClosed): # The peer has closed it - let's close it too! yield commands.CloseConnection(event.connection) - else: + elif isinstance(event, (events.DataReceived, QuicStreamEvent)): # The peer has sent data or another connection activity occurred. # This can happen with HTTP/2 servers that already send a settings frame. child_layer: HttpConnection @@ -899,6 +900,8 @@ def _handle_event(self, event: events.Event): self.connections[self.context.server] = child_layer yield from self.event_to_child(child_layer, events.Start()) yield from self.event_to_child(child_layer, event) + else: + raise AssertionError(f"Unexpected event: {event}") else: handler = self.connections[event.connection] yield from self.event_to_child(handler, event) diff --git a/mitmproxy/proxy/layers/modes.py b/mitmproxy/proxy/layers/modes.py index 3b3b7d53e3..1a58d10dfc 100644 --- a/mitmproxy/proxy/layers/modes.py +++ b/mitmproxy/proxy/layers/modes.py @@ -60,13 +60,14 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: assert isinstance(spec, ReverseMode) self.context.server.address = spec.address - if spec.scheme in ("https", "http3", "quic", "tls", "dtls"): + if spec.scheme in ("http3", "quic"): if not self.context.options.keep_host_header: self.context.server.sni = spec.address[0] - if spec.scheme == "http3" or spec.scheme == "quic": - self.child_layer = quic.ServerQuicLayer(self.context) - else: - self.child_layer = tls.ServerTLSLayer(self.context) + self.child_layer = quic.ServerQuicLayer(self.context) + elif spec.scheme in ("https", "tls", "dtls"): + if not self.context.options.keep_host_header: + self.context.server.sni = spec.address[0] + self.child_layer = tls.ServerTLSLayer(self.context) elif spec.scheme == "udp": self.child_layer = udp.UDPLayer(self.context) elif spec.scheme == "http" or spec.scheme == "tcp": diff --git a/mitmproxy/proxy/layers/quic.py b/mitmproxy/proxy/layers/quic.py index 6668442e93..979a907c58 100644 --- a/mitmproxy/proxy/layers/quic.py +++ b/mitmproxy/proxy/layers/quic.py @@ -605,21 +605,24 @@ def __init__(self, context: context.Context, conn: connection.Connection) -> Non conn.tls = True def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: - # turn Wakeup events into empty DataReceived events if ( isinstance(event, events.Wakeup) and event.command in self._wakeup_commands ): + # TunnelLayer has no understanding of wakeups, so we turn this into an empty DataReceived event + # which TunnelLayer recognizes as belonging to our connection. assert self.quic timer = self._wakeup_commands.pop(event.command) if self.quic._state is not QuicConnectionState.TERMINATED: self.quic.handle_timer(now=max(timer, self._loop.time())) - event = events.DataReceived(self.tunnel_connection, b"") - yield from super()._handle_event(event) + yield from super()._handle_event( + events.DataReceived(self.tunnel_connection, b"") + ) + else: + yield from super()._handle_event(event) def _handle_command(self, command: commands.Command) -> layer.CommandGenerator[None]: """Turns stream commands into aioquic connection invocations.""" - if ( isinstance(command, QuicStreamCommand) and command.connection is self.conn @@ -801,6 +804,7 @@ def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: quic_events.ConnectionIdIssued, quic_events.ConnectionIdRetired, quic_events.PingAcknowledged, + quic_events.ProtocolNegotiated, )): pass else: diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py index 749356a632..239689b4a3 100644 --- a/mitmproxy/tools/main.py +++ b/mitmproxy/tools/main.py @@ -54,6 +54,7 @@ async def main() -> T: logging.getLogger("tornado").setLevel(logging.WARNING) logging.getLogger("asyncio").setLevel(logging.WARNING) logging.getLogger("hpack").setLevel(logging.WARNING) + logging.getLogger("quic").setLevel(logging.WARNING) # aioquic uses a different prefix... debug.register_info_dumpers() opts = options.Options() diff --git a/web/src/js/ducks/_options_gen.ts b/web/src/js/ducks/_options_gen.ts index 64f821065b..32f7ae1bdc 100644 --- a/web/src/js/ducks/_options_gen.ts +++ b/web/src/js/ducks/_options_gen.ts @@ -43,7 +43,6 @@ export interface OptionsState { proxy_debug: boolean proxyauth: string | undefined rawtcp: boolean - rawudp: boolean readfile_filter: string | undefined rfile: string | undefined save_stream_file: string | undefined @@ -135,7 +134,6 @@ export const defaultState: OptionsState = { proxy_debug: false, proxyauth: undefined, rawtcp: true, - rawudp: true, readfile_filter: undefined, rfile: undefined, save_stream_file: undefined, From 0812b36f20ad35921760ea24d212d51e0d7fec47 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 13 Oct 2022 17:51:21 +0200 Subject: [PATCH 2/4] experimental: move `half_close` out of `CloseConnection` --- mitmproxy/proxy/commands.py | 2 ++ mitmproxy/proxy/layers/http/__init__.py | 3 ++- mitmproxy/proxy/layers/http/_http1.py | 2 +- mitmproxy/proxy/layers/quic.py | 4 ++-- mitmproxy/proxy/layers/tcp.py | 2 +- mitmproxy/proxy/layers/tls.py | 4 ++-- mitmproxy/proxy/server.py | 4 +++- mitmproxy/proxy/tunnel.py | 12 +++++------- test/mitmproxy/proxy/layers/test_tcp.py | 6 +++--- test/mitmproxy/proxy/test_tunnel.py | 6 +++--- test/mitmproxy/proxy/tutils.py | 9 ++++----- 11 files changed, 28 insertions(+), 26 deletions(-) diff --git a/mitmproxy/proxy/commands.py b/mitmproxy/proxy/commands.py index d4173ebc9b..e5c3627f30 100644 --- a/mitmproxy/proxy/commands.py +++ b/mitmproxy/proxy/commands.py @@ -94,6 +94,8 @@ class CloseConnection(ConnectionCommand): all other connections will ultimately be closed during cleanup. """ + +class CloseTcpConnection(CloseConnection): half_close: bool """ If True, only close our half of the connection by sending a FIN packet. diff --git a/mitmproxy/proxy/layers/http/__init__.py b/mitmproxy/proxy/layers/http/__init__.py index 3cf117e017..4f9cd26acd 100644 --- a/mitmproxy/proxy/layers/http/__init__.py +++ b/mitmproxy/proxy/layers/http/__init__.py @@ -791,7 +791,8 @@ def passthrough(self, event: events.Event) -> layer.CommandGenerator[None]: # The easiest approach for this is to just always full close for now. # Alternatively, we could signal that we want a half close only through ResponseProtocolError, # but that is more complex to implement. - command.half_close = False + if isinstance(command, commands.CloseTcpConnection): + command = commands.CloseConnection(command.connection) yield command else: yield command diff --git a/mitmproxy/proxy/layers/http/_http1.py b/mitmproxy/proxy/layers/http/_http1.py index 2cf410fb9a..e645b54695 100644 --- a/mitmproxy/proxy/layers/http/_http1.py +++ b/mitmproxy/proxy/layers/http/_http1.py @@ -369,7 +369,7 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: if "chunked" in self.request.headers.get("transfer-encoding", "").lower(): yield commands.SendData(self.conn, b"0\r\n\r\n") elif http1.expected_http_body_size(self.request, self.response) == -1: - yield commands.CloseConnection(self.conn, half_close=True) + yield commands.CloseTcpConnection(self.conn, half_close=True) yield from self.mark_done(request=True) else: raise AssertionError(f"Unexpected event: {event}") diff --git a/mitmproxy/proxy/layers/quic.py b/mitmproxy/proxy/layers/quic.py index 979a907c58..6e3393f4a6 100644 --- a/mitmproxy/proxy/layers/quic.py +++ b/mitmproxy/proxy/layers/quic.py @@ -824,7 +824,7 @@ def send_data(self, data: bytes) -> layer.CommandGenerator[None]: self.quic.send_datagram_frame(data) yield from self.tls_interact() - def send_close(self, half_close: bool) -> layer.CommandGenerator[None]: + def send_close(self, command: commands.CloseConnection) -> layer.CommandGenerator[None]: # properly close the QUIC connection if self.quic is not None: close_event = get_connection_error(self.conn) @@ -833,7 +833,7 @@ def send_close(self, half_close: bool) -> layer.CommandGenerator[None]: else: self.quic.close(close_event.error_code, close_event.frame_type, close_event.reason_phrase) yield from self.tls_interact() - yield from super().send_close(half_close) + yield from super().send_close(command) class ServerQuicLayer(QuicLayer): diff --git a/mitmproxy/proxy/layers/tcp.py b/mitmproxy/proxy/layers/tcp.py index d3c9f9bd69..2d1ff7305b 100644 --- a/mitmproxy/proxy/layers/tcp.py +++ b/mitmproxy/proxy/layers/tcp.py @@ -132,7 +132,7 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: yield TcpEndHook(self.flow) self.flow.live = False else: - yield commands.CloseConnection(send_to, half_close=True) + yield commands.CloseTcpConnection(send_to, half_close=True) else: raise AssertionError(f"Unexpected event: {event}") diff --git a/mitmproxy/proxy/layers/tls.py b/mitmproxy/proxy/layers/tls.py index ab63d51d35..d79cbcfc69 100644 --- a/mitmproxy/proxy/layers/tls.py +++ b/mitmproxy/proxy/layers/tls.py @@ -440,9 +440,9 @@ def send_data(self, data: bytes) -> layer.CommandGenerator[None]: pass yield from self.tls_interact() - def send_close(self, half_close: bool) -> layer.CommandGenerator[None]: + def send_close(self, command: commands.CloseConnection) -> layer.CommandGenerator[None]: # We should probably shutdown the TLS connection properly here. - yield from super().send_close(half_close) + yield from super().send_close(command) class ServerTLSLayer(TLSLayer): diff --git a/mitmproxy/proxy/server.py b/mitmproxy/proxy/server.py index 1341564ada..ca83be9ac0 100644 --- a/mitmproxy/proxy/server.py +++ b/mitmproxy/proxy/server.py @@ -369,8 +369,10 @@ def server_event(self, event: events.Event) -> None: assert writer if not writer.is_closing(): writer.write(command.data) - elif isinstance(command, commands.CloseConnection): + elif isinstance(command, commands.CloseTcpConnection): self.close_connection(command.connection, command.half_close) + elif isinstance(command, commands.CloseConnection): + self.close_connection(command.connection, False) elif isinstance(command, commands.StartHook): asyncio_utils.create_task( self.hook_task(command), diff --git a/mitmproxy/proxy/tunnel.py b/mitmproxy/proxy/tunnel.py index 7a481a7014..435693de6a 100644 --- a/mitmproxy/proxy/tunnel.py +++ b/mitmproxy/proxy/tunnel.py @@ -117,11 +117,9 @@ def _handle_command(self, command: commands.Command) -> layer.CommandGenerator[N yield from self.send_data(command.data) elif isinstance(command, commands.CloseConnection): if self.conn != self.tunnel_connection: - if command.half_close: - self.conn.state &= ~connection.ConnectionState.CAN_WRITE - else: - self.conn.state = connection.ConnectionState.CLOSED - yield from self.send_close(command.half_close) + self.conn.state &= ~connection.ConnectionState.CAN_WRITE + command.connection = self.tunnel_connection + yield from self.send_close(command) elif isinstance(command, commands.OpenConnection): # create our own OpenConnection command object that blocks here. self.command_to_reply_to = command @@ -172,8 +170,8 @@ def receive_close(self) -> layer.CommandGenerator[None]: def send_data(self, data: bytes) -> layer.CommandGenerator[None]: yield commands.SendData(self.tunnel_connection, data) - def send_close(self, half_close: bool) -> layer.CommandGenerator[None]: - yield commands.CloseConnection(self.tunnel_connection, half_close=half_close) + def send_close(self, command: commands.CloseConnection) -> layer.CommandGenerator[None]: + yield command class LayerStack: diff --git a/test/mitmproxy/proxy/layers/test_tcp.py b/test/mitmproxy/proxy/layers/test_tcp.py index d4f4947b09..df01fa9f98 100644 --- a/test/mitmproxy/proxy/layers/test_tcp.py +++ b/test/mitmproxy/proxy/layers/test_tcp.py @@ -1,6 +1,6 @@ import pytest -from mitmproxy.proxy.commands import CloseConnection, OpenConnection, SendData +from mitmproxy.proxy.commands import CloseConnection, CloseTcpConnection, OpenConnection, SendData from mitmproxy.proxy.events import ConnectionClosed, DataReceived from mitmproxy.proxy.layers import tcp from mitmproxy.proxy.layers.tcp import TcpMessageInjected @@ -52,7 +52,7 @@ def test_simple(tctx): >> reply() << SendData(tctx.client, b"hi") >> ConnectionClosed(tctx.server) - << CloseConnection(tctx.client, half_close=True) + << CloseTcpConnection(tctx.client, half_close=True) >> ConnectionClosed(tctx.client) << CloseConnection(tctx.server) << tcp.TcpEndHook(f) @@ -88,7 +88,7 @@ def test_receive_data_after_half_close(tctx): >> DataReceived(tctx.client, b"eof-delimited-request") << SendData(tctx.server, b"eof-delimited-request") >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.server, half_close=True) + << CloseTcpConnection(tctx.server, half_close=True) >> DataReceived(tctx.server, b"i'm late") << SendData(tctx.client, b"i'm late") >> ConnectionClosed(tctx.server) diff --git a/test/mitmproxy/proxy/test_tunnel.py b/test/mitmproxy/proxy/test_tunnel.py index 24103637c6..004c7ab69f 100644 --- a/test/mitmproxy/proxy/test_tunnel.py +++ b/test/mitmproxy/proxy/test_tunnel.py @@ -3,7 +3,7 @@ import pytest from mitmproxy.proxy import tunnel, layer -from mitmproxy.proxy.commands import SendData, Log, CloseConnection, OpenConnection +from mitmproxy.proxy.commands import CloseTcpConnection, SendData, Log, CloseConnection, OpenConnection from mitmproxy.connection import Server, ConnectionState from mitmproxy.proxy.context import Context from mitmproxy.proxy.events import Event, DataReceived, Start, ConnectionClosed @@ -24,7 +24,7 @@ def _handle_event(self, event: Event) -> layer.CommandGenerator[None]: 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) + err = yield CloseTcpConnection(event.connection, half_close=True) elif isinstance(event, ConnectionClosed): yield Log(f"Got {event.connection.__class__.__name__.lower()} close.") yield CloseConnection(event.connection) @@ -164,7 +164,7 @@ def test_tunnel_default_impls(tctx: Context): >> reply(None) << Log("Opened: err=None. Server state: OPEN") >> DataReceived(server, b"half-close") - << CloseConnection(server, half_close=True) + << CloseTcpConnection(server, half_close=True) ) diff --git a/test/mitmproxy/proxy/tutils.py b/test/mitmproxy/proxy/tutils.py index 61b7489b1a..087db71316 100644 --- a/test/mitmproxy/proxy/tutils.py +++ b/test/mitmproxy/proxy/tutils.py @@ -224,11 +224,10 @@ def __bool__(self): for cmd in cmds: pos += 1 assert self.actual[pos] == cmd - if isinstance(cmd, commands.CloseConnection): - if cmd.half_close: - cmd.connection.state &= ~ConnectionState.CAN_WRITE - else: - cmd.connection.state = ConnectionState.CLOSED + if isinstance(cmd, commands.CloseTcpConnection) and cmd.half_close: + cmd.connection.state &= ~ConnectionState.CAN_WRITE + elif isinstance(cmd, commands.CloseConnection): + cmd.connection.state = ConnectionState.CLOSED elif isinstance(cmd, commands.Log): need_to_emulate_log = ( not self.logs From eb494033c10d2c266dd3ab38cd1be424c083fb0c Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 13 Oct 2022 18:36:48 +0200 Subject: [PATCH 3/4] `get/set_connection_error` -> event/command subclasses --- mitmproxy/proxy/layers/http/_http3.py | 12 ++-- mitmproxy/proxy/layers/http/_http_h3.py | 13 ++-- mitmproxy/proxy/layers/quic.py | 86 +++++++++++++++++-------- 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/mitmproxy/proxy/layers/http/_http3.py b/mitmproxy/proxy/layers/http/_http3.py index f61cbb8a81..9c46d77f9b 100644 --- a/mitmproxy/proxy/layers/http/_http3.py +++ b/mitmproxy/proxy/layers/http/_http3.py @@ -12,9 +12,9 @@ from mitmproxy.net.http import status_codes from mitmproxy.proxy import commands, context, events, layer from mitmproxy.proxy.layers.quic import ( + QuicConnectionClosed, QuicStreamEvent, error_code_to_str, - get_connection_error, ) from mitmproxy.proxy.utils import expect @@ -164,12 +164,10 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: # report a protocol error for all remaining open streams when a connection is closed elif isinstance(event, events.ConnectionClosed): self._handle_event = self.done # type: ignore - close_event = get_connection_error(self.conn) - msg = ( - "peer closed connection" - if close_event is None else - close_event.reason_phrase or error_code_to_str(close_event.error_code) - ) + if isinstance(event, QuicConnectionClosed): + msg = event.reason_phrase or error_code_to_str(event.error_code) + else: + msg = "peer closed connection" for stream_id in self.h3_conn.get_reserved_stream_ids(): yield ReceiveHttp(self.ReceiveProtocolError(stream_id, msg)) diff --git a/mitmproxy/proxy/layers/http/_http_h3.py b/mitmproxy/proxy/layers/http/_http_h3.py index 56cd563060..78d82a40a2 100644 --- a/mitmproxy/proxy/layers/http/_http_h3.py +++ b/mitmproxy/proxy/layers/http/_http_h3.py @@ -17,12 +17,12 @@ from mitmproxy import connection from mitmproxy.proxy import commands, layer from mitmproxy.proxy.layers.quic import ( + CloseQuicConnection, QuicStreamDataReceived, QuicStreamEvent, QuicStreamReset, ResetQuicStream, SendQuicStreamData, - set_connection_error, ) @@ -87,13 +87,10 @@ def close( # we'll get closed if a protocol error occurs in `H3Connection.handle_event` # we note the error on the connection and yield a CloseConnection # this will then call `QuicConnection.close` with the proper values - # once the `Http3Connection` receives `ConnectionClosed`, it will send out `*ProtocolError` - set_connection_error(self.conn, ConnectionTerminated( - error_code=error_code, - frame_type=frame_type, - reason_phrase=reason_phrase, - )) - self.pending_commands.append(commands.CloseConnection(self.conn)) + # once the `Http3Connection` receives `ConnectionClosed`, it will send out `ProtocolError` + self.pending_commands.append( + CloseQuicConnection(self.conn, error_code, frame_type, reason_phrase) + ) def get_next_available_stream_id(self, is_unidirectional: bool = False) -> int: # since we always reserve the ID, we have to "find" the next ID like `QuicConnection` does diff --git a/mitmproxy/proxy/layers/quic.py b/mitmproxy/proxy/layers/quic.py index 6e3393f4a6..fd2afd9f40 100644 --- a/mitmproxy/proxy/layers/quic.py +++ b/mitmproxy/proxy/layers/quic.py @@ -177,6 +177,56 @@ def __init__(self, connection: connection.Connection, stream_id: int, error_code self.error_code = error_code +class CloseQuicConnection(commands.CloseConnection): + """Close a QUIC connection.""" + + error_code: int + "The error code which was specified when closing the connection." + + frame_type: int | None + "The frame type which caused the connection to be closed, or `None`." + + reason_phrase: str + "The human-readable reason for which the connection was closed." + + # XXX: A bit much boilerplate right now. Should switch to dataclasses. + def __init__( + self, + conn: connection.Connection, + error_code: int, + frame_type: int | None, + reason_phrase: str, + ): + super().__init__(conn) + self.error_code = error_code + self.frame_type = frame_type + self.reason_phrase = reason_phrase + + +class QuicConnectionClosed(events.ConnectionClosed): + """QUIC connection has been closed.""" + error_code: int + "The error code which was specified when closing the connection." + + frame_type: int | None + "The frame type which caused the connection to be closed, or `None`." + + reason_phrase: str + "The human-readable reason for which the connection was closed." + + def __init__( + self, + conn: connection.Connection, + error_code: int, + frame_type: int | None, + reason_phrase: str, + ): + super().__init__(conn) + self.error_code = error_code + self.frame_type = frame_type + self.reason_phrase = reason_phrase + + class QuicSecretsLogger: logger: tls.MasterSecretLogger @@ -208,28 +258,12 @@ def error_code_to_str(error_code: int) -> str: return f"unknown error (0x{error_code:x})" -def get_connection_error(conn: connection.Connection) -> quic_events.ConnectionTerminated | None: - """Returns the QUIC close event that is associated with the given connection.""" - - close_event = getattr(conn, "quic_error", None) - if close_event is None: - return None - assert isinstance(close_event, quic_events.ConnectionTerminated) - return close_event - - def is_success_error_code(error_code: int) -> bool: """Returns whether the given error code actually indicates no error.""" return error_code in (QuicErrorCode.NO_ERROR, H3ErrorCode.H3_NO_ERROR) -def set_connection_error(conn: connection.Connection, close_event: quic_events.ConnectionTerminated) -> None: - """Stores the given close event for the given connection.""" - - setattr(conn, "quic_error", close_event) - - @dataclass class QuicClientHello(Exception): """Helper error only used in `quic_parse_client_hello`.""" @@ -481,17 +515,13 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: # handle close events that target this context elif ( - isinstance(event, events.ConnectionClosed) + isinstance(event, QuicConnectionClosed) and ( event.connection is self.context.client or event.connection is self.context.server ) ): - # copy the connection error from_client = event.connection is self.context.client - close_event = get_connection_error(event.connection) - if close_event is not None: - set_connection_error(self.context.server if from_client else self.context.client, close_event) # always forward to the datagram layer yield from self.event_to_child(self.datagram_layer, event) @@ -552,7 +582,9 @@ def event_to_child(self, child_layer: layer.Layer, event: events.Event) -> layer if command.connection.state & connection.ConnectionState.CAN_WRITE: command.connection.state &= ~connection.ConnectionState.CAN_WRITE yield SendQuicStreamData(quic_conn, stream_id, b"", end_stream=True) - if not command.half_close: + # XXX: Use `command.connection.state & connection.ConnectionState.CAN_READ` instead? + only_close_our_half = isinstance(command, commands.CloseTcpConnection) and command.half_close + if not only_close_our_half: if ( stream_is_client_initiated(stream_id) == to_client or not stream_is_unidirectional(stream_id) @@ -786,13 +818,12 @@ def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: # handle post-handshake events while event := self.quic.next_event(): if isinstance(event, quic_events.ConnectionTerminated): - set_connection_error(self.conn, event) if self.debug: reason = event.reason_phrase or error_code_to_str(event.error_code) yield commands.Log( f"{self.debug}[quic] close_notify {self.conn} (reason={reason})", DEBUG ) - yield commands.CloseConnection(self.conn) + yield CloseQuicConnection(self.conn, event.error_code, event.frame_type, event.reason_phrase) return # we don't handle any further events, nor do/can we transmit data, so exit elif isinstance(event, quic_events.DatagramFrameReceived): yield from self.event_to_child(events.DataReceived(self.conn, event.data)) @@ -827,11 +858,10 @@ def send_data(self, data: bytes) -> layer.CommandGenerator[None]: def send_close(self, command: commands.CloseConnection) -> layer.CommandGenerator[None]: # properly close the QUIC connection if self.quic is not None: - close_event = get_connection_error(self.conn) - if close_event is None: - self.quic.close() + if isinstance(command, CloseQuicConnection): + self.quic.close(command.error_code, command.frame_type, command.reason_phrase) else: - self.quic.close(close_event.error_code, close_event.frame_type, close_event.reason_phrase) + self.quic.close() yield from self.tls_interact() yield from super().send_close(command) From 7b1d18857604545b11753cf7c2ed3adee59bd2a2 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 13 Oct 2022 20:07:41 +0200 Subject: [PATCH 4/4] add QUIC metadata to streams --- mitmproxy/proxy/layers/quic.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mitmproxy/proxy/layers/quic.py b/mitmproxy/proxy/layers/quic.py index fd2afd9f40..e48dd32865 100644 --- a/mitmproxy/proxy/layers/quic.py +++ b/mitmproxy/proxy/layers/quic.py @@ -333,7 +333,7 @@ class QuicStreamLayer(layer.Layer): """Virtual client connection for this stream. Use this in QuicRawLayer instead of `context.client`.""" server: connection.Server """Virtual server connection for this stream. Use this in QuicRawLayer instead of `context.server`.""" - child_layer: layer.Layer + child_layer: TCPLayer """The stream's child layer.""" def __init__(self, context: context.Context, ignore: bool, stream_id: int) -> None: @@ -369,11 +369,21 @@ def __init__(self, context: context.Context, ignore: bool, stream_id: int) -> No if ignore else layer.NextLayer(context) ) + if ignore: + self.child_layer = TCPLayer(context, ignore=True) + else: + tcp_layer = TCPLayer(context) + # This can potentially move to a smarter place later on, + # but it's useful debugging info in mitmproxy for now. + tcp_layer.flow.metadata["quic_is_unidirectional"] = stream_is_unidirectional(stream_id) + tcp_layer.flow.metadata["quic_initiator"] = "client" if stream_is_client_initiated(stream_id) else "server" + tcp_layer.flow.metadata["quic_stream_id_client"] = stream_id + self.child_layer = tcp_layer self.handle_event = self.child_layer.handle_event # type: ignore self._handle_event = self.child_layer._handle_event # type: ignore def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: - pass + raise AssertionError def open_server_stream(self, server_stream_id) -> None: assert self._server_stream_id is None @@ -388,6 +398,8 @@ def open_server_stream(self, server_stream_id) -> None: if stream_is_unidirectional(server_stream_id) else connection.ConnectionState.OPEN ) + if self.child_layer.flow: + self.child_layer.flow.metadata["quic_stream_id_server"] = server_stream_id def stream_id(self, client: bool) -> int | None: return self._client_stream_id if client else self._server_stream_id