Skip to content

Instantly share code, notes, and snippets.

@DonnchaC
Last active May 12, 2019 11:54
Show Gist options
  • Save DonnchaC/4a89bf7c52500a1d7e7b to your computer and use it in GitHub Desktop.
Save DonnchaC/4a89bf7c52500a1d7e7b to your computer and use it in GitHub Desktop.
This is a proof-of-concept tool which demonstrates transparent proxying of SSL connections from an entry server to a hidden servce with end-to-end encryption. The SNI extension in the SSL ClientHello is used to determine the destination. Further info in the Tor2web ticket: https://github.com/globaleaks/Tor2web/issues/252
#!/usr/bin/env python
"""
Proxy an SSL connection to a Twisted endpoint based on the SNI extension
Allows for end-to-end encrypted connections from a browser to a Tor hidden
service.
Proxy code based on
http://blog.laplante.io/2013/08/a-basic-man-in-the-middle-proxy-with-twisted/
"""
import logging
from twisted.internet.endpoints import clientFromString
from twisted.internet import protocol, reactor
from OpenSSL import SSL
# Set up logging
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(fmt="%(asctime)s [%(levelname)s]: "
"%(message)s"))
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(level=logging.DEBUG)
class NameResolver(object):
"""
Resolve a domain to a Twisted client endpoint
Takes a domain name and returns an endpoint. This demo uses a
domain -> onion list. In future the resolver could query a TXT
record from the domain to retrieve the destination TCP or onion address.
"""
def __init__(self):
# Testing domain list
self.domain_list = {
'oniontip.com': 'ha2zlm2uo6hnhdha',
'blockchain.info': 'blockchainbdgpzk',
}
def resolve(self, domain):
return self.domain_list.get(domain)
def get_endpoint(self, domain):
resolved_name = self.resolve(domain)
if resolved_name:
logger.info('Resolved domain %s to %s.onion',
domain, resolved_name)
endpoint_string = 'tor:host={}.onion:port=443'.format(
self.resolve(domain))
try:
endpoint = clientFromString(reactor, endpoint_string)
except ValueError:
logger.exception('Error creating client endpoint. Maybe a '
'suitable endpoint parser is not available.')
return None
return endpoint
else:
logger.warn('Could not resolve domain %s.', domain)
return None
class SSLContext(object):
"""
Simple mocked SSL connection to allow parsing of the ClientHello
"""
def __init__(self):
"""
Initialize an SSL connection object
"""
self.server_name = None
context = SSL.Context(SSL.TLSv1_2_METHOD)
context.set_tlsext_servername_callback(self.get_servername)
self.connection = SSL.Connection(context=context)
self.connection.set_accept_state()
def get_servername(self, connection):
"""
Callback to retrieve the parsed SNI extension when it is parsed
"""
self.server_name = connection.get_servername()
def parse_client_hello(self, client_hello):
# Write the SSL handshake into the BIO memory stream.
self.connection.bio_write(client_hello)
try:
# Start parsing the client handshake from the memory stream
self.connection.do_handshake()
except SSL.Error:
# We don't have a complete SSL handshake, only the ClientHello,
# close the connection once we hit an error.
self.connection.shutdown()
# Should have run the get_servername callback already
return self.server_name
class ServerProtocol(protocol.Protocol):
"""
Adapted from http://stackoverflow.com/a/15645169/221061
"""
def __init__(self):
self.buffer = None
self.client = None
self.resolve = NameResolver()
def connectionMade(self):
pass
def parseSNI(self, data):
ssl_context = SSLContext()
return ssl_context.parse_client_hello(data)
def createClient(self, endpoint):
"""
Create an instance of the Client which connects to the
destination server.
"""
factory = protocol.ReconnectingClientFactory()
factory.protocol = ClientProtocol
factory.server = self
# Connect client 'factory' to the destination
logger.debug('Creating connection to endpoint.')
d = endpoint.connect(factory)
reactor.callLater(30, d.cancel)
# Client => Proxy
def dataReceived(self, data):
if self.client:
self.client.write(data)
else:
# Add ClientHello handshake into an outbound buffer
self.buffer = data
# Try to read the SNI header from the SSL ClientHello
sni_domain = self.parseSNI(data)
if sni_domain:
logger.info('Got request with SNI %s.', sni_domain)
# Resolve the domain to a Twisted client endpoint
endpoint = self.resolve.get_endpoint(sni_domain)
if endpoint:
self.createClient(endpoint)
else:
# Could not resolve the SNI domain to an endpoint,
# close the user's connection to the proxy server.
self.transport.loseConnection()
else:
logger.warning("Got a request without SNI field, closing.")
self.transport.loseConnection()
# Proxy => Client
def write(self, data):
self.transport.write(data)
class ClientProtocol(protocol.Protocol):
"""
Protocol for the connection to destination endpoint
"""
def connectionMade(self):
self.factory.server.client = self
# Write the ClientHello from the buffer to the Client connection
# when the connection is created
self.write(self.factory.server.buffer)
self.factory.server.buffer = None
logger.debug('Connection to endpoint successful.')
# Server => Proxy
def dataReceived(self, data):
self.factory.server.write(data)
# Proxy => Server
def write(self, data):
if data:
self.transport.write(data)
def connectionLost(self, reason):
logger.debug('Connection to endpoint lost.')
# Close the proxy server connection when the client connection closes
self.factory.server.transport.loseConnection()
def main():
listening_port = 443
factory = protocol.ServerFactory()
factory.protocol = ServerProtocol
# Start the proxy server listener
logger.info("Starting SNI proxy server on port {}.".format(listening_port))
reactor.listenTCP(listening_port, factory)
reactor.run()
if __name__ == '__main__':
main()
@BookGin
Copy link

BookGin commented Dec 17, 2017

Hi, I encountered a weird error here. Both curl and firefox 56 in the client side will fail to connect to the proxy.

  File "/home/user/python3-sniproxy/lib/python3.5/site-packages/twisted/internet/posixbase.py", line 614, in _doReadOrWrite
    why = selectable.doRead()
  File "/home/user/python3-sniproxy/lib/python3.5/site-packages/twisted/internet/tcp.py", line 205, in doRead                            
    return self._dataReceived(data)
  File "/home/user/python3-sniproxy/lib/python3.5/site-packages/twisted/internet/tcp.py", line 211, in _dataReceived
    rval = self.protocol.dataReceived(data)
  File "./sni-proxy.py", line 161, in dataReceived
    sni_domain = self.parseSNI(data)
  File "./sni-proxy.py", line 136, in parseSNI
    return ssl_context.parse_client_hello(data)
  File "./sni-proxy.py", line 109, in parse_client_hello
    self.connection.do_handshake()
  File "/home/user/python3-sniproxy/lib/python3.5/site-packages/OpenSSL/SSL.py", line 1806, in do_handshake
    self._raise_ssl_error(self._ssl, result)
  File "/home/user/python3-sniproxy/lib/python3.5/site-packages/OpenSSL/SSL.py", line 1546, in _raise_ssl_error
    _raise_current_error()
  File "/home/user/python3-sniproxy/lib/python3.5/site-packages/OpenSSL/_util.py", line 54, in exception_from_error_queue
    raise exception_type(errors)
OpenSSL.SSL.Error: [('SSL routines', 'tls_post_process_client_hello', 'no shared cipher')]

@js42
Copy link

js42 commented May 9, 2019

Hi,
I stumbled upon the same problem. Any idea why this occurs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment