Writing an SSL tunnel in python
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