From ce0a500885619444b7fd386419c5c556d5196e18 Mon Sep 17 00:00:00 2001 From: David Weinstein Date: Sat, 23 Jan 2016 21:29:14 -0500 Subject: [PATCH] Add ignore based on TLS ClientHello SNI - also add some documentation about ignoring based on SNI --- libmproxy/protocol/__init__.py | 4 +- libmproxy/protocol/tls.py | 126 +++++++++++++++++++------------- libmproxy/proxy/root_context.py | 40 ++++++++-- 3 files changed, 113 insertions(+), 57 deletions(-) diff --git a/libmproxy/protocol/__init__.py b/libmproxy/protocol/__init__.py index 0d624fd7c..d8ebd4f08 100644 --- a/libmproxy/protocol/__init__.py +++ b/libmproxy/protocol/__init__.py @@ -28,12 +28,12 @@ as late as possible; this makes server replay without any outgoing connections p from __future__ import (absolute_import, print_function, division) from .base import Layer, ServerConnectionMixin, Kill from .http import Http1Layer, UpstreamConnectLayer, Http2Layer -from .tls import TlsLayer, is_tls_record_magic +from .tls import TlsLayer, is_tls_record_magic, TlsClientHello from .rawtcp import RawTCPLayer __all__ = [ "Layer", "ServerConnectionMixin", "Kill", "Http1Layer", "UpstreamConnectLayer", "Http2Layer", - "TlsLayer", "is_tls_record_magic", + "TlsLayer", "is_tls_record_magic", "TlsClientHello" "RawTCPLayer" ] diff --git a/libmproxy/protocol/tls.py b/libmproxy/protocol/tls.py index ed7476432..0955166fe 100644 --- a/libmproxy/protocol/tls.py +++ b/libmproxy/protocol/tls.py @@ -221,6 +221,75 @@ def is_tls_record_magic(d): d[2] in ('\x00', '\x01', '\x02', '\x03') ) +def get_client_hello(client_conn): + """ + Peek into the socket and read all records that contain the initial client hello message. + + client_conn: + The :py:class:`client connection `. + + Returns: + The raw handshake packet bytes, without TLS record header(s). + """ + client_hello = "" + client_hello_size = 1 + offset = 0 + while len(client_hello) < client_hello_size: + record_header = client_conn.rfile.peek(offset + 5)[offset:] + if not is_tls_record_magic(record_header) or len(record_header) != 5: + raise TlsProtocolException('Expected TLS record, got "%s" instead.' % record_header) + record_size = struct.unpack("!H", record_header[3:])[0] + 5 + record_body = client_conn.rfile.peek(offset + record_size)[offset + 5:] + if len(record_body) != record_size - 5: + raise TlsProtocolException("Unexpected EOF in TLS handshake: %s" % record_body) + client_hello += record_body + offset += record_size + client_hello_size = struct.unpack("!I", '\x00' + client_hello[1:4])[0] + 4 + return client_hello + +class TlsClientHello(object): + def __init__(self, raw_client_hello): + self._client_hello = ClientHello.parse(raw_client_hello) + + def raw(self): + return self._client_hello + + @property + def client_cipher_suites(self): + return self._client_hello.cipher_suites.cipher_suites + + @property + def client_sni(self): + for extension in self._client_hello.extensions: + if (extension.type == 0x00 and len(extension.server_names) == 1 + and extension.server_names[0].type == 0): + return extension.server_names[0].name + + @property + def client_alpn_protocols(self): + for extension in self._client_hello.extensions: + if extension.type == 0x10: + return list(extension.alpn_protocols) + + @classmethod + def from_client_conn(cls, client_conn): + """ + Peek into the connection, read the initial client hello and parse it to obtain ALPN values. + client_conn: + The :py:class:`client connection `. + Returns: + :py:class:`client hello `. + """ + try: + raw_client_hello = get_client_hello(client_conn)[4:] # exclude handshake header. + except ProtocolException as e: + raise TlsProtocolException('Cannot parse Client Hello: %s' % repr(e)) + + try: + return cls(raw_client_hello) + except ConstructError as e: + #self.log("Raw Client Hello: %s" % raw_client_hello.encode("hex"), "debug") + raise TlsProtocolException('Cannot parse Client Hello: %s' % repr(e)) class TlsLayer(Layer): def __init__(self, ctx, client_tls, server_tls): @@ -281,60 +350,15 @@ class TlsLayer(Layer): else: return "TlsLayer(inactive)" - def _get_client_hello(self): - """ - Peek into the socket and read all records that contain the initial client hello message. - - Returns: - The raw handshake packet bytes, without TLS record header(s). - """ - client_hello = "" - client_hello_size = 1 - offset = 0 - while len(client_hello) < client_hello_size: - record_header = self.client_conn.rfile.peek(offset + 5)[offset:] - if not is_tls_record_magic(record_header) or len(record_header) != 5: - raise TlsProtocolException('Expected TLS record, got "%s" instead.' % record_header) - record_size = struct.unpack("!H", record_header[3:])[0] + 5 - record_body = self.client_conn.rfile.peek(offset + record_size)[offset + 5:] - if len(record_body) != record_size - 5: - raise TlsProtocolException("Unexpected EOF in TLS handshake: %s" % record_body) - client_hello += record_body - offset += record_size - client_hello_size = struct.unpack("!I", '\x00' + client_hello[1:4])[0] + 4 - return client_hello def _parse_client_hello(self): """ Peek into the connection, read the initial client hello and parse it to obtain ALPN values. """ - try: - raw_client_hello = self._get_client_hello()[4:] # exclude handshake header. - except ProtocolException as e: - self.log("Cannot parse Client Hello: %s" % repr(e), "error") - return - - try: - client_hello = ClientHello.parse(raw_client_hello) - except ConstructError as e: - self.log("Cannot parse Client Hello: %s" % repr(e), "error") - self.log("Raw Client Hello: %s" % raw_client_hello.encode("hex"), "debug") - return - - self.client_ciphers = client_hello.cipher_suites.cipher_suites - - for extension in client_hello.extensions: - if extension.type == 0x00: - if len(extension.server_names) != 1 or extension.server_names[0].type != 0: - self.log("Unknown Server Name Indication: %s" % extension.server_names, "error") - self.client_sni = extension.server_names[0].name - elif extension.type == 0x10: - self.client_alpn_protocols = list(extension.alpn_protocols) - - self.log( - "Parsed Client Hello: sni=%s, alpn=%s" % (self.client_sni, self.client_alpn_protocols), - "debug" - ) + parsed = TlsClientHello.from_client_conn(self.client_conn) + self.client_sni = parsed.client_sni + self.client_alpn_protocols = parsed.client_alpn_protocols + self.client_ciphers = parsed.client_cipher_suites def connect(self): if not self.server_conn: @@ -359,6 +383,10 @@ class TlsLayer(Layer): def alpn_for_client_connection(self): return self.server_conn.get_alpn_proto_negotiated() + @property + def client_tls(self): + return self._client_tls + def __alpn_select_callback(self, conn_, options): """ Once the client signals the alternate protocols it supports, diff --git a/libmproxy/proxy/root_context.py b/libmproxy/proxy/root_context.py index f62b0c8ec..ab10c47d5 100644 --- a/libmproxy/proxy/root_context.py +++ b/libmproxy/proxy/root_context.py @@ -4,14 +4,40 @@ import sys import six -from libmproxy.exceptions import ProtocolException +from libmproxy.exceptions import ProtocolException, TlsProtocolException from netlib.exceptions import TcpException from ..protocol import ( RawTCPLayer, TlsLayer, Http1Layer, Http2Layer, is_tls_record_magic, ServerConnectionMixin, - UpstreamConnectLayer + UpstreamConnectLayer, TlsClientHello ) from .modes import HttpProxy, HttpUpstreamProxy, ReverseProxy +def tls_sni_check_ignore(fun): + """ + A decorator to wrap the process of getting the next layer. + If it's a TlsLayer and the client uses SNI, see if the user asked us to + ignore the host. + Returns: + A function that returns the next layer. + """ + def inner(self, top_layer): + """ + Arguments: + top_layer: the current innermost layer. + Returns: + The next layer + """ + layer = fun(self, top_layer) + if not isinstance(layer, TlsLayer) or not layer.client_tls: + return layer + try: + parsed_client_hello = TlsClientHello.from_client_conn(self.client_conn) + if parsed_client_hello and self.config.check_ignore((parsed_client_hello.client_sni, 443)): + return RawTCPLayer(top_layer, logging=False) + except TlsProtocolException as e: + six.reraise(ProtocolException, ProtocolException(str(e)), sys.exc_info()[2]) + return layer + return inner class RootContext(object): """ @@ -33,6 +59,7 @@ class RootContext(object): self.client_conn = client_conn self.channel = channel self.config = config + self._client_tls = False def next_layer(self, top_layer): """ @@ -47,6 +74,7 @@ class RootContext(object): layer = self._next_layer(top_layer) return self.channel.ask("next_layer", layer) + @tls_sni_check_ignore def _next_layer(self, top_layer): # 1. Check for --ignore. if self.config.check_ignore(top_layer.server_conn.address): @@ -56,15 +84,15 @@ class RootContext(object): d = top_layer.client_conn.rfile.peek(3) except TcpException as e: six.reraise(ProtocolException, ProtocolException(str(e)), sys.exc_info()[2]) - client_tls = is_tls_record_magic(d) + self._client_tls = is_tls_record_magic(d) # 2. Always insert a TLS layer, even if there's neither client nor server tls. # An inline script may upgrade from http to https, # in which case we need some form of TLS layer. if isinstance(top_layer, ReverseProxy): - return TlsLayer(top_layer, client_tls, top_layer.server_tls) + return TlsLayer(top_layer, self._client_tls, top_layer.server_tls) if isinstance(top_layer, ServerConnectionMixin) or isinstance(top_layer, UpstreamConnectLayer): - return TlsLayer(top_layer, client_tls, client_tls) + return TlsLayer(top_layer, self._client_tls, self._client_tls) # 3. In Http Proxy mode and Upstream Proxy mode, the next layer is fixed. if isinstance(top_layer, TlsLayer): @@ -74,7 +102,7 @@ class RootContext(object): return Http1Layer(top_layer, "upstream") # 4. Check for other TLS cases (e.g. after CONNECT). - if client_tls: + if self._client_tls: return TlsLayer(top_layer, True, True) # 4. Check for --tcp