Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
next_layer.py475 linesDownload Raw Back to addons
1"""2This addon determines the next protocol layer in our proxy stack.3Whenever a protocol layer in the proxy wants to pass a connection to a child layer and isn't sure which protocol comes4next, it calls the `next_layer` hook, which ends up here.5For example, if mitmproxy runs as a regular proxy, we first need to determine if6new clients start with a TLS handshake right away (Secure Web Proxy) or send a plaintext HTTP CONNECT request.7This addon here peeks at the incoming bytes and then makes a decision based on proxy mode, mitmproxy options, etc.8 9For a typical HTTPS request, this addon is called a couple of times: First to determine that we start with an HTTP layer10which processes the `CONNECT` request, a second time to determine that the client then starts negotiating TLS, and a11third time when we check if the protocol within that TLS stream is actually HTTP or something else.12 13Sometimes it's useful to hardcode specific logic in next_layer when one wants to do fancy things.14In that case it's not necessary to modify mitmproxy's source, adding a custom addon with a next_layer event hook15that sets nextlayer.layer works just as well.16"""17 18from __future__ import annotations19 20import logging21import re22import sys23from collections.abc import Iterable24from collections.abc import Sequence25from typing import Any26from typing import cast27 28from mitmproxy import ctx29from mitmproxy.connection import Address30from mitmproxy.net.tls import starts_like_dtls_record31from mitmproxy.net.tls import starts_like_tls_record32from mitmproxy.proxy import layer33from mitmproxy.proxy import layers34from mitmproxy.proxy import mode_specs35from mitmproxy.proxy import tunnel36from mitmproxy.proxy.context import Context37from mitmproxy.proxy.layer import Layer38from mitmproxy.proxy.layers import ClientQuicLayer39from mitmproxy.proxy.layers import ClientTLSLayer40from mitmproxy.proxy.layers import DNSLayer41from mitmproxy.proxy.layers import HttpLayer42from mitmproxy.proxy.layers import modes43from mitmproxy.proxy.layers import RawQuicLayer44from mitmproxy.proxy.layers import ServerQuicLayer45from mitmproxy.proxy.layers import ServerTLSLayer46from mitmproxy.proxy.layers import TCPLayer47from mitmproxy.proxy.layers import UDPLayer48from mitmproxy.proxy.layers.http import HTTPMode49from mitmproxy.proxy.layers.quic import quic_parse_client_hello_from_datagrams50from mitmproxy.proxy.layers.tls import dtls_parse_client_hello51from mitmproxy.proxy.layers.tls import HTTP_ALPNS52from mitmproxy.proxy.layers.tls import parse_client_hello53from mitmproxy.tls import ClientHello54 55if sys.version_info < (3, 11):56    from typing_extensions import assert_never57else:58    from typing import assert_never59 60logger = logging.getLogger(__name__)61 62 63def stack_match(64    context: Context, layers: Sequence[type[Layer] | tuple[type[Layer], ...]]65) -> bool:66    if len(context.layers) != len(layers):67        return False68    return all(69        expected is Any or isinstance(actual, expected)70        for actual, expected in zip(context.layers, layers)71    )72 73 74class NeedsMoreData(Exception):75    """Signal that the decision on which layer to put next needs to be deferred within the NextLayer addon."""76 77 78class NextLayer:79    ignore_hosts: Sequence[re.Pattern] = ()80    allow_hosts: Sequence[re.Pattern] = ()81    tcp_hosts: Sequence[re.Pattern] = ()82    udp_hosts: Sequence[re.Pattern] = ()83 84    def configure(self, updated):85        if "tcp_hosts" in updated:86            self.tcp_hosts = [87                re.compile(x, re.IGNORECASE) for x in ctx.options.tcp_hosts88            ]89        if "udp_hosts" in updated:90            self.udp_hosts = [91                re.compile(x, re.IGNORECASE) for x in ctx.options.udp_hosts92            ]93        if "allow_hosts" in updated or "ignore_hosts" in updated:94            self.ignore_hosts = [95                re.compile(x, re.IGNORECASE) for x in ctx.options.ignore_hosts96            ]97            self.allow_hosts = [98                re.compile(x, re.IGNORECASE) for x in ctx.options.allow_hosts99            ]100 101    def next_layer(self, nextlayer: layer.NextLayer):102        if nextlayer.layer:103            return  # do not override something another addon has set.104        try:105            nextlayer.layer = self._next_layer(106                nextlayer.context,107                nextlayer.data_client(),108                nextlayer.data_server(),109            )110        except NeedsMoreData:111            logger.debug(112                f"Deferring layer decision, not enough data: {nextlayer.data_client().hex()!r}"113            )114 115    def _next_layer(116        self, context: Context, data_client: bytes, data_server: bytes117    ) -> Layer | None:118        assert context.layers119 120        def s(*layers):121            return stack_match(context, layers)122 123        tcp_based = context.client.transport_protocol == "tcp"124        udp_based = context.client.transport_protocol == "udp"125 126        # 1)  check for --ignore/--allow127        if self._ignore_connection(context, data_client, data_server):128            return (129                layers.TCPLayer(context, ignore=not ctx.options.show_ignored_hosts)130                if tcp_based131                else layers.UDPLayer(context, ignore=not ctx.options.show_ignored_hosts)132            )133 134        # 2)  Handle proxy modes with well-defined next protocol135        # 2a) Reverse proxy: derive from spec136        if s(modes.ReverseProxy):137            return self._setup_reverse_proxy(context, data_client)138        # 2b) Explicit HTTP proxies139        if s((modes.HttpProxy, modes.HttpUpstreamProxy)):140            return self._setup_explicit_http_proxy(context, data_client)141 142        # 3)  Handle security protocols143        # 3a) TLS/DTLS144        is_tls_or_dtls = (145            tcp_based146            and starts_like_tls_record(data_client)147            or udp_based148            and starts_like_dtls_record(data_client)149        )150        if is_tls_or_dtls:151            server_tls = ServerTLSLayer(context)152            server_tls.child_layer = ClientTLSLayer(context)153            return server_tls154        # 3b) QUIC155        if udp_based and _starts_like_quic(data_client, context.server.address):156            server_quic = ServerQuicLayer(context)157            server_quic.child_layer = ClientQuicLayer(context)158            return server_quic159 160        # 4)  Check for --tcp/--udp161        if tcp_based and self._is_destination_in_hosts(context, self.tcp_hosts):162            return layers.TCPLayer(context)163        if udp_based and self._is_destination_in_hosts(context, self.udp_hosts):164            return layers.UDPLayer(context)165 166        # 5)  Handle application protocol167        # 5a) Do we have a known ALPN negotiation?168        if context.client.alpn:169            if context.client.alpn in HTTP_ALPNS:170                return layers.HttpLayer(context, HTTPMode.transparent)171            elif context.client.tls_version == "QUICv1":172                # TODO: Once we support more QUIC-based protocols, relax force_raw here.173                return layers.RawQuicLayer(context, force_raw=True)174        # 5b) Is it DNS?175        if context.server.address and context.server.address[1] in (53, 5353):176            return layers.DNSLayer(context)177        # 5c) We have no other specialized layers for UDP, so we fall back to raw forwarding.178        if udp_based:179            return layers.UDPLayer(context)180        # 5d) Check for raw tcp mode.181        probably_no_http = (182            # the first three bytes should be the HTTP verb, so A-Za-z is expected.183            len(data_client) < 3184            # HTTP would require whitespace...185            or b" " not in data_client186            # ...and that whitespace needs to be in the first line.187            or (data_client.find(b" ") > data_client.find(b"\n"))188            or not data_client[:3].isalpha()189            # a server greeting would be uncharacteristic.190            or data_server191            or data_client.startswith(b"SSH")192        )193        if ctx.options.rawtcp and probably_no_http:194            return layers.TCPLayer(context)195        # 5c) Assume HTTP by default.196        return layers.HttpLayer(context, HTTPMode.transparent)197 198    def _ignore_connection(199        self,200        context: Context,201        data_client: bytes,202        data_server: bytes,203    ) -> bool | None:204        """205        Returns:206            True, if the connection should be ignored.207            False, if it should not be ignored.208 209        Raises:210            NeedsMoreData, if we need to wait for more input data.211        """212        if not ctx.options.ignore_hosts and not ctx.options.allow_hosts:213            return False214        # Special handling for wireguard mode: if the hostname is "10.0.0.53", do not ignore the connection215        if isinstance(216            context.client.proxy_mode, mode_specs.WireGuardMode217        ) and context.server.address == ("10.0.0.53", 53):218            return False219        hostnames: list[str] = []220        if context.server.peername:221            host, port, *_ = context.server.peername222            hostnames.append(f"{host}:{port}")223        if context.server.address:224            host, port, *_ = context.server.address225            hostnames.append(f"{host}:{port}")226 227            # We also want to check for TLS SNI and HTTP host headers, but in order to ignore connections based on that228            # they must have a destination address. If they don't, we don't know how to establish an upstream connection229            # if we ignore.230            if host_header := self._get_host_header(context, data_client, data_server):231                if not re.search(r":\d+$", host_header):232                    host_header = f"{host_header}:{port}"233                hostnames.append(host_header)234            if (235                client_hello := self._get_client_hello(context, data_client)236            ) and client_hello.sni:237                hostnames.append(f"{client_hello.sni}:{port}")238            if context.client.sni:239                # Hostname may be allowed, TLS is already established, and we have another next layer decision.240                hostnames.append(f"{context.client.sni}:{port}")241 242        if not hostnames:243            return False244 245        if ctx.options.allow_hosts:246            not_allowed = not any(247                re.search(rex, host, re.IGNORECASE)248                for host in hostnames249                for rex in ctx.options.allow_hosts250            )251            if not_allowed:252                return True253 254        if ctx.options.ignore_hosts:255            ignored = any(256                re.search(rex, host, re.IGNORECASE)257                for host in hostnames258                for rex in ctx.options.ignore_hosts259            )260            if ignored:261                return True262 263        return False264 265    @staticmethod266    def _get_host_header(267        context: Context,268        data_client: bytes,269        data_server: bytes,270    ) -> str | None:271        """272        Try to read a host header from data_client.273 274        Returns:275            The host header value, or None, if no host header was found.276 277        Raises:278            NeedsMoreData, if the HTTP request is incomplete.279        """280        if context.client.transport_protocol != "tcp" or data_server:281            return None282 283        host_header_expected = re.match(284            rb"[A-Z]{3,}.+HTTP/", data_client, re.IGNORECASE285        )286        if host_header_expected:287            if m := re.search(288                rb"\r\n(?:Host:\s+(.+?)\s*)?\r\n", data_client, re.IGNORECASE289            ):290                if host := m.group(1):291                    return host.decode("utf-8", "surrogateescape")292                else:293                    return None  # \r\n\r\n - header end came first.294            else:295                raise NeedsMoreData296        else:297            return None298 299    @staticmethod300    def _get_client_hello(context: Context, data_client: bytes) -> ClientHello | None:301        """302        Try to read a TLS/DTLS/QUIC ClientHello from data_client.303 304        Returns:305            A complete ClientHello, or None, if no ClientHello was found.306 307        Raises:308            NeedsMoreData, if the ClientHello is incomplete.309        """310        match context.client.transport_protocol:311            case "tcp":312                if starts_like_tls_record(data_client):313                    try:314                        ch = parse_client_hello(data_client)315                    except ValueError:316                        pass317                    else:318                        if ch is None:319                            raise NeedsMoreData320                        return ch321                return None322            case "udp":323                try:324                    return quic_parse_client_hello_from_datagrams([data_client])325                except ValueError:326                    pass327 328                try:329                    ch = dtls_parse_client_hello(data_client)330                except ValueError:331                    pass332                else:333                    if ch is None:334                        raise NeedsMoreData335                    return ch336                return None337            case _:  # pragma: no cover338                assert_never(context.client.transport_protocol)339 340    @staticmethod341    def _setup_reverse_proxy(context: Context, data_client: bytes) -> Layer:342        spec = cast(mode_specs.ReverseMode, context.client.proxy_mode)343        stack = tunnel.LayerStack()344 345        match spec.scheme:346            case "http":347                if starts_like_tls_record(data_client):348                    stack /= ClientTLSLayer(context)349                stack /= HttpLayer(context, HTTPMode.transparent)350            case "https":351                if context.client.transport_protocol == "udp":352                    stack /= ServerQuicLayer(context)353                    stack /= ClientQuicLayer(context)354                    stack /= HttpLayer(context, HTTPMode.transparent)355                else:356                    stack /= ServerTLSLayer(context)357                    if starts_like_tls_record(data_client):358                        stack /= ClientTLSLayer(context)359                    stack /= HttpLayer(context, HTTPMode.transparent)360 361            case "tcp":362                if starts_like_tls_record(data_client):363                    stack /= ClientTLSLayer(context)364                stack /= TCPLayer(context)365            case "tls":366                stack /= ServerTLSLayer(context)367                if starts_like_tls_record(data_client):368                    stack /= ClientTLSLayer(context)369                stack /= TCPLayer(context)370 371            case "udp":372                if starts_like_dtls_record(data_client):373                    stack /= ClientTLSLayer(context)374                stack /= UDPLayer(context)375            case "dtls":376                stack /= ServerTLSLayer(context)377                if starts_like_dtls_record(data_client):378                    stack /= ClientTLSLayer(context)379                stack /= UDPLayer(context)380 381            case "dns":382                # TODO: DNS-over-TLS / DNS-over-DTLS383                # is_tls_or_dtls = (384                #     context.client.transport_protocol == "tcp" and starts_like_tls_record(data_client)385                #     or386                #     context.client.transport_protocol == "udp" and starts_like_dtls_record(data_client)387                # )388                # if is_tls_or_dtls:389                #     stack /= ClientTLSLayer(context)390                stack /= DNSLayer(context)391 392            case "http3":393                stack /= ServerQuicLayer(context)394                stack /= ClientQuicLayer(context)395                stack /= HttpLayer(context, HTTPMode.transparent)396            case "quic":397                stack /= ServerQuicLayer(context)398                stack /= ClientQuicLayer(context)399                stack /= RawQuicLayer(context, force_raw=True)400 401            case _:  # pragma: no cover402                assert_never(spec.scheme)403 404        return stack[0]405 406    @staticmethod407    def _setup_explicit_http_proxy(context: Context, data_client: bytes) -> Layer:408        stack = tunnel.LayerStack()409 410        if context.client.transport_protocol == "udp":411            stack /= layers.ClientQuicLayer(context)412        elif starts_like_tls_record(data_client):413            stack /= layers.ClientTLSLayer(context)414 415        if isinstance(context.layers[0], modes.HttpUpstreamProxy):416            stack /= layers.HttpLayer(context, HTTPMode.upstream)417        else:418            stack /= layers.HttpLayer(context, HTTPMode.regular)419 420        return stack[0]421 422    @staticmethod423    def _is_destination_in_hosts(context: Context, hosts: Iterable[re.Pattern]) -> bool:424        return any(425            (context.server.address and rex.search(context.server.address[0]))426            or (context.client.sni and rex.search(context.client.sni))427            for rex in hosts428        )429 430 431# https://www.iana.org/assignments/quic/quic.xhtml432KNOWN_QUIC_VERSIONS = {433    0x00000001,  # QUIC v1434    0x51303433,  # Google QUIC Q043435    0x51303436,  # Google QUIC Q046436    0x51303530,  # Google QUIC Q050437    0x6B3343CF,  # QUIC v2438    0x709A50C4,  # QUIC v2 draft codepoint439}440 441TYPICAL_QUIC_PORTS = {80, 443, 8443}442 443 444def _starts_like_quic(data_client: bytes, server_address: Address | None) -> bool:445    """446    Make an educated guess on whether this could be QUIC.447    This turns out to be quite hard in practice as 1-RTT packets are hardly distinguishable from noise.448 449    Returns:450        True, if the passed bytes could be the start of a QUIC packet.451        False, otherwise.452    """453    # Minimum size: 1 flag byte + 1+ packet number bytes + 16+ bytes encrypted payload454    if len(data_client) < 18:455        return False456    if starts_like_dtls_record(data_client):457        return False458    # TODO: Add more checks here to detect true negatives.459 460    # Long Header Packets461    if data_client[0] & 0x80:462        version = int.from_bytes(data_client[1:5], "big")463        if version in KNOWN_QUIC_VERSIONS:464            return True465        # https://www.rfc-editor.org/rfc/rfc9000.html#name-versions466        # Versions that follow the pattern 0x?a?a?a?a are reserved for use in forcing version negotiation467        if version & 0x0F0F0F0F == 0x0A0A0A0A:468            return True469    else:470        # ¯\_(ツ)_/¯471        # We can't even rely on the QUIC bit, see https://datatracker.ietf.org/doc/rfc9287/.472        pass473 474    return bool(server_address and server_address[1] in TYPICAL_QUIC_PORTS)475 
codekingpro/portable-devtools · Team Ai