Add ignore based on TLS ClientHello SNI

- also add some documentation about ignoring based on SNI
This commit is contained in:
David Weinstein 2016-01-23 21:29:14 -05:00
parent a60810cc2c
commit ce0a500885
3 changed files with 113 additions and 57 deletions

View File

@ -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 __future__ import (absolute_import, print_function, division)
from .base import Layer, ServerConnectionMixin, Kill from .base import Layer, ServerConnectionMixin, Kill
from .http import Http1Layer, UpstreamConnectLayer, Http2Layer 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 from .rawtcp import RawTCPLayer
__all__ = [ __all__ = [
"Layer", "ServerConnectionMixin", "Kill", "Layer", "ServerConnectionMixin", "Kill",
"Http1Layer", "UpstreamConnectLayer", "Http2Layer", "Http1Layer", "UpstreamConnectLayer", "Http2Layer",
"TlsLayer", "is_tls_record_magic", "TlsLayer", "is_tls_record_magic", "TlsClientHello"
"RawTCPLayer" "RawTCPLayer"
] ]

View File

@ -221,6 +221,75 @@ def is_tls_record_magic(d):
d[2] in ('\x00', '\x01', '\x02', '\x03') 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 <libmproxy.models.ClientConnection>`.
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 <libmproxy.models.ClientConnection>`.
Returns:
:py:class:`client hello <libmproxy.protocol.tls.TlsClientHello>`.
"""
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): class TlsLayer(Layer):
def __init__(self, ctx, client_tls, server_tls): def __init__(self, ctx, client_tls, server_tls):
@ -281,60 +350,15 @@ class TlsLayer(Layer):
else: else:
return "TlsLayer(inactive)" 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): def _parse_client_hello(self):
""" """
Peek into the connection, read the initial client hello and parse it to obtain ALPN values. Peek into the connection, read the initial client hello and parse it to obtain ALPN values.
""" """
try: parsed = TlsClientHello.from_client_conn(self.client_conn)
raw_client_hello = self._get_client_hello()[4:] # exclude handshake header. self.client_sni = parsed.client_sni
except ProtocolException as e: self.client_alpn_protocols = parsed.client_alpn_protocols
self.log("Cannot parse Client Hello: %s" % repr(e), "error") self.client_ciphers = parsed.client_cipher_suites
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"
)
def connect(self): def connect(self):
if not self.server_conn: if not self.server_conn:
@ -359,6 +383,10 @@ class TlsLayer(Layer):
def alpn_for_client_connection(self): def alpn_for_client_connection(self):
return self.server_conn.get_alpn_proto_negotiated() return self.server_conn.get_alpn_proto_negotiated()
@property
def client_tls(self):
return self._client_tls
def __alpn_select_callback(self, conn_, options): def __alpn_select_callback(self, conn_, options):
""" """
Once the client signals the alternate protocols it supports, Once the client signals the alternate protocols it supports,

View File

@ -4,14 +4,40 @@ import sys
import six import six
from libmproxy.exceptions import ProtocolException from libmproxy.exceptions import ProtocolException, TlsProtocolException
from netlib.exceptions import TcpException from netlib.exceptions import TcpException
from ..protocol import ( from ..protocol import (
RawTCPLayer, TlsLayer, Http1Layer, Http2Layer, is_tls_record_magic, ServerConnectionMixin, RawTCPLayer, TlsLayer, Http1Layer, Http2Layer, is_tls_record_magic, ServerConnectionMixin,
UpstreamConnectLayer UpstreamConnectLayer, TlsClientHello
) )
from .modes import HttpProxy, HttpUpstreamProxy, ReverseProxy 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): class RootContext(object):
""" """
@ -33,6 +59,7 @@ class RootContext(object):
self.client_conn = client_conn self.client_conn = client_conn
self.channel = channel self.channel = channel
self.config = config self.config = config
self._client_tls = False
def next_layer(self, top_layer): def next_layer(self, top_layer):
""" """
@ -47,6 +74,7 @@ class RootContext(object):
layer = self._next_layer(top_layer) layer = self._next_layer(top_layer)
return self.channel.ask("next_layer", layer) return self.channel.ask("next_layer", layer)
@tls_sni_check_ignore
def _next_layer(self, top_layer): def _next_layer(self, top_layer):
# 1. Check for --ignore. # 1. Check for --ignore.
if self.config.check_ignore(top_layer.server_conn.address): 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) d = top_layer.client_conn.rfile.peek(3)
except TcpException as e: except TcpException as e:
six.reraise(ProtocolException, ProtocolException(str(e)), sys.exc_info()[2]) 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. # 2. Always insert a TLS layer, even if there's neither client nor server tls.
# An inline script may upgrade from http to https, # An inline script may upgrade from http to https,
# in which case we need some form of TLS layer. # in which case we need some form of TLS layer.
if isinstance(top_layer, ReverseProxy): 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): 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. # 3. In Http Proxy mode and Upstream Proxy mode, the next layer is fixed.
if isinstance(top_layer, TlsLayer): if isinstance(top_layer, TlsLayer):
@ -74,7 +102,7 @@ class RootContext(object):
return Http1Layer(top_layer, "upstream") return Http1Layer(top_layer, "upstream")
# 4. Check for other TLS cases (e.g. after CONNECT). # 4. Check for other TLS cases (e.g. after CONNECT).
if client_tls: if self._client_tls:
return TlsLayer(top_layer, True, True) return TlsLayer(top_layer, True, True)
# 4. Check for --tcp # 4. Check for --tcp