Back to Home

Writing an SSL tunnel in python

python · ssl · proxy · tunnel · windows

Writing an SSL tunnel in python

    The problem arose: there is an application for Windows that makes HTTPS requests to the server and receives answers. After updating the server, the application stopped working. It turned out that the version of SSL on the server had changed (switched from SSLv3 to TLSv1), and our application can only work on SSLv3. No one has supported the application for a long time and did not want to change, recompile, test. It was decided to make a layer between the application and the server, which will translate SSLv3 to TLSv1 and vice versa. I searched for a proxy on the Internet, but did not find it right away (I searched poorly). I decided to make a proxy on python. I am not a professional in python, but it seemed to me that he is well suited for this task, and it is interesting to study python in parallel using an example of a real problem.

    Start

    So, install python 3.4. We are writing a script, I used a notepad for this. For ssl sockets, you need the ssl module. For, actually, sockets socket.
    import ssl
    import socket
    

    We create a socket listening to the client, because if it is an SSL server, you will have to create a self-signed certificate for it, which it will provide to the client. To create the certificate, I used the openssl utility. I downloaded the utility from here indy.fulgan.com/SSL . To create a certificate, you need a config for the utility, an example can be taken here web.mit.edu/crypto/openssl.cnf . We put the config in the folder on the computer and set the path to it (hereinafter all the actions on the command line):
    set OPENSSL_CONF=путь_к_файлу\openssl.cnf
    

    We generate a private key
    openssl genrsa -des3 -out server.key 1024
    

    Along the way, you will be prompted to enter the key password and password confirmation, enter. Create a certificate request
    openssl req -new -key server.key -out server.csr
    

    When generating a request, we will need to enter the key password and fill out information about the company, city, country, etc. We fill in. In order to be able to use the key without a password, copy it and parole
    copy server.key server.key.org
    openssl rsa -in server.key.org -out server.key
    

    Finally, create a self-signed certificate
    openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
    

    For convenience, we put our certificate and key next to the python script. We create a socket that will listen to the client and set it to listen to the port on which our application will go (hereinafter the code in python)
    sock = ssl.wrap_socket(socket.socket(), 'server.key', 'server.crt', True)
    sock.bind( ('localhost', 43433) )
    sock.listen(10)
    

    We receive an incoming connection and a request from the client
    conn, addr = sock.accept()
    data = conn.recv(1024)
    

    Next, we need to send the received data to the server to which it was intended. We create for this a socket and a helmet in it data
    serv = ssl.wrap_socket(socket.socket())
    serv.connect( ('server_url', 443) )
    serv.send(data)
    

    So, the request was sent, now we need to get a response and give it to our client
    data = serv.recv(1024)
    conn.send(data)
    

    Well, all our proxies are ready, run, throw a request - it does not work! In order to find out why, add logging.

    Logging

    module is connected logging, configure logging configuration and add logging to places of interest
    import logging
    logging.basicConfig(filename = "proxy.log", level = logging.DEBUG, format = "%(asctime)s - %(message)s")
    logging.info("Ждем входящее соединение");
    conn, addr = sock.accept()
    logging.info("Получаем запрос")
    data = conn.recv(1024)
    logging.info(data)
    logging.info("Отправляем запрос на сервер")
    serv.send(data)
    logging.info("Получаем ответ сервера")
    data = serv.recv(1024)
    logging.info(data)
    logging.info("Отдаем ответ клиенту")
    client.send(resp)
    


    Reading all data

    It turned out that the client transmits data in blocks, i.e. We have not read the full request. Then it turns out that the server also responds in blocks. We will improve our code to read the request and response in blocks. To do this, create a buffer in which we will add the entire request, set the socket to a timeout of 0.1 s, which it will wait for data from the incoming connection and in a cycle we read and put the data into the buffer. If there is no data, we will get an exception and exit the loop
    logging.info("Получаем запрос")
    data = conn.recv(1024)
    req = b''
    conn.settimeout(0.1)
    while data:
        req += data
        try:
            data = conn.recv(1024)
        except socket.error:
            break
    logging.info(req)
    

    The same for reading data from the server
    logging.info("Получаем ответ сервера")
    resp = b''
    serv.settimeout(1)
    data = serv.recv(1024)
    while data:
        resp += data
        try:
            data = serv.recv(1024)
        except socket.error:
            break
    logging.info(resp)
    

    Change the data that we will send to the server and client
    logging.info("Отправляем запрос на сервер")
    serv.send(req)
    logging.info("Отдаем ответ клиенту")
    client.send(resp)
    

    We start. Now it works, however, you have to run the script with every request to the server, which is not very convenient.

    Processing several requests

    Let's improve the script, after processing the request we will again listen to the socket
    while True:
        logging.info("Ждем входящее соединение");
        conn, addr = sock.accept()
        logging.info("Получаем запрос")
        data = conn.recv(1024)
        req = b''
        conn.settimeout(0.1)
        while data:
            req += data
            try:
                data = conn.recv(1024)
            except socket.error:
                break
        logging.info(req)
        logging.info("Отправляем запрос на сервер")
        serv.send(req)
        logging.info("Получаем ответ сервера")
        resp = b''
        serv.settimeout(1)
        data = serv.recv(1024)
        while data:
            resp += data
            try:
                data = serv.recv(1024)
            except socket.error:
                break
        logging.info(resp)
    	logging.info("Отдаем ответ клиенту")
        client.send(resp)
    

    This will work, but there is a problem - we have an infinite loop from which the program cannot exit in the normal way. To exit, you can use the keyboard interrupt Ctrl + C and send a request, after which the program will exit with the exception of KeyboardInterrupt.

    Stopping the service

    To provide a more or less normal exit, I decided to transfer STOP to the socket, this will be the control command for termination. We will write a handler for such a command. To do this, we need to modify the read code from the client socket. We receive the first four bytes and if they will be STOP, we interrupt a cycle.
        logging.info("Получаем запрос")
        data = conn.recv(4)
        if data == b'STOP':
            break
    

    We will write a function to stop our proxy. In it, create a socket (ssl) and send STOP to our proxy
    def stop():
        logging.info("Останов");
        me = ssl.wrap_socket(socket.socket())
        me.connect( ('localhost', 43433) )
        me.send(b'STOP')
        me.close()
    

    To start the STOP command, we will use the command line parameter. If we passed the stop line to the command line, we will call our stop () function (We put this code and the stop function at the beginning, after setting the logging format).
    if len(sys.argv) > 1:
        if sys.argv[1] == "stop":
            stop();
    

    Now we can stop our proxy with the same script. So that after stopping the server’s startup code is not executed, we wrap the main code in the run function, we get
    def run():
        # сюда поместим код прокси-сервера описанный выше
    def stop():
        # код приведен выше
    if len(sys.argv) > 1:
        if sys.argv[1] == "stop":
            stop();
    	else:
    	    print("Неизвестная комманда ", sys.argv[1])
    else:
        run()
    

    At the same time, we handled the case with the wrong command.

    Demonization

    There is a problem, when starting our proxy application will hang on the command line, at first glance it seems to freeze. To solve this problem, we will make a daemon. Because we have Windows, then the daemon is done by starting the process without a window, this code will be non-cross-platform. So let's write the daemonize () function
    import subprocess
    def daemonize():
        logging.info("Запуск демона");
        subprocess.Popen("py proxy.py", creationflags=0x08000000, close_fds=True)
    

    Here creationflags = 0x08000000, setting the flag CREATE_NO_WINDOW for the process. We will start our service in daemon mode if we passed start on the command line
    if len(sys.argv) > 1:
        if sys.argv[1] == "stop":
            stop();
    	elif sys.argv[1] == "start":
    	    daemonize();
    	else:
    	    print("Неизвестная комманда ", sys.argv[1])
    else:
        run()
    

    Now we can start our service in daemon mode and stop it.

    Multitasking

    Another small touch, we’ll add the ability to process several clients, for this we will take out our client work code in a separate function
    def client_run(client, data):
        req = b''
        logging.info("Получаем запрос")
        client.settimeout(0.1)
        while data:
            req += data
            try:
                data = client.recv(1024)
            except socket.error:
                break
        logging.info(req)
        serv = ssl.wrap_socket(socket.socket())
        serv.connect( ('server_name', 443) )
        logging.info("Отправляем запрос на сервер")
        serv.send(req)
        logging.info("Получаем ответ сервера")
        resp = b''
        serv.settimeout(1)
        data = serv.recv(1024)
        while data:
            resp += data
            try:
                data = serv.recv(1024)
            except socket.error:
                break
        logging.info(resp)
        logging.info("Отдаем ответ клиенту")
        client.send(resp)
    

    And in the main function, we will run client_run in a separate thread, because Since we installed socket.listen (10), then at the same time we can have up to 10 threads
    def run():
        logging.info("Старт главного потока");
        sock = ssl.wrap_socket(socket.socket(), 'server.key', 'server.crt', True)
        sock.bind( ('localhost', 43433) )
        sock.listen(10)
        while True:
            logging.info("Ждем входящее соединение");
            conn, addr = sock.accept()
            data = conn.recv(4)
            if data == b'STOP':
                break
            logging.info("Получен запрос")
            t = threading.Thread(target = client_run, args = ( conn, data ) )
            t.run()
        logging.info("Остановка")
    

    Now our proxy service is ready.

    PS: Later, a colleague told me that you can use stunnel for my task, and I decided to put it, and put the script here, if anyone would be interested. The config for stunnel is this:
    [client-in]
    sslVersion = SSLv3
    accept = 127.0.0.1:43433
    connect = 127.0.0.1:8080
    [server-out]
    sslVersion = TLSv1
    client = yes
    accept = 127.0.0.1:8080
    connect = server_name:443
    

    I also had to tinker with stunnel, as the server had incorrect settings and did not pass SNI verification, it worked only with version 4.36, because there is no such verification.

    Sources on github github.com/sesk/py_proxy

    Read Next