How to create TOR proxy with Python (Cheat sheet 101)

Yicong
6 min readNov 24, 2021

Introduction

Back in the 1990s, the internet is an insecure place where network traffic is largely unencrypted and traceable, which allow hackers to perform man-in-the-middle attacks and collect personal data easily. To tackle this issue, a group of researchers from the U.S. naval research lab started The Onion Routing (TOR) project with the intent to anonymize and secure communications on the internet.

Methodology

TOR relies on a distributed trust model to secure your network traffic over the internet. It is a methodology where your data is encrypted by multiple parties to provide a layered data protection (like an Onion). So your data will be safe, unless someone is able to hijack all the parties involved in the encryption.

In practice, TOR selects 3 unique relays operated by different entities to encrypt and route your network traffic through before reaching the final destination.

Here is a simple diagram to illustrate the methodology.

Principle of onion routing (https://medium.com/swlh/tor-how-does-it-work-d0be02ddb539)

To learn more about TOR, Velentin Quelquejay has a great write-up for it.

For this article, I will be focusing on the practical aspect of configuring TOR proxy with Python as I felt that there is a lack of online materials and discussion in this area.

To help you learn more effectively, I have split this tutorial into 3 levels of difficulty, covering the useful features of TOR, which allows you harness its full potential.

Prerequisites

Before continuing this tutorial, do make sure you have the following installed.

  1. Python3
  2. Pip install stem
  3. Git clone https://github.com/ohyicong/Tor

Basic Setup: Default configuration

Here is a simple and effective code to create a TOR proxy server using its default configuration:

  1. Establish a proxy on local port 9050
  2. Establish a TOR connection over 3 unique relays (random)
  3. Changes IP address every 10 minutes
# complete code found in create_basic_tor_proxy.py
import io
import os
import stem.process
import re
SOCKS_PORT = 9050
TOR_PATH = os.path.normpath(os.getcwd()+"\\tor\\tor.exe")
tor_process = stem.process.launch_tor_with_config(
config = {
'SocksPort': str(SOCKS_PORT),
},
init_msg_handler = lambda line: print(line) if re.search('Bootstrapped', line) else False,
tor_cmd = TOR_PATH
)

To check if your setup has been successful, you can make a GET request to http://ip-api.com/json/. If the IP address is from a different country, you have successfully setup your first TOR proxy!

import requests
import json
from datetime import datetime
PROXIES = {
'http': 'socks5://127.0.0.1:9050',
'https': 'socks5://127.0.0.1:9050'
}
response = requests.get("http://ip-api.com/json/", proxies=PROXIES)
result = json.loads(response.content)
print('TOR IP [%s]: %s %s'%(datetime.now().strftime("%d-%m-%Y %H:%M:%S"), result["query"], result["country"]))

Do remember to stop your proxy before starting the next exercise. Here is the command to stop it.

tor_process.kill()

Intermediate Setup: Select relays from specific countries

There may be some instances where you need more control over TOR connection. Here are some possible reasons:

  1. Avoiding relays from certain countries due to cybersecurity/law/political reasons.
  2. Changing TOR IP address refresh rate.
  3. Using a different authentication method for TOR connection

The code below creates a TOR proxy server on local port 9050 with the following configuration:

  1. EntryNodes: TOR shall use a relay from France as its entry node.
  2. ExitNodes: TOR shall use a relay from Japan as its exit node.
  3. StrictNodes: TOR connection shall strictly follow user configuration.
  4. CookieAuthentication: Use cookie authentication.
  5. MaxCircuitDirtiness: Refresh IP every 60 seconds.
  6. GeoIPFile: Use the specified geographical ipv4 file. Downloadable from https://raw.githubusercontent.com/torproject/tor/main/src/config/geoip
# complete code found in create_intermediate_tor_proxy.py
import io
import os
import stem.process
import re
import urllib.request
SOCKS_PORT = 9050
TOR_PATH = os.path.normpath(os.getcwd()+"\\tor\\tor.exe")
GEOIPFILE_PATH = os.path.normpath(os.getcwd()+"\\data\\tor\\geoip")
try:
urllib.request.urlretrieve('https://raw.githubusercontent.com/torproject/tor/main/src/config/geoip', GEOIPFILE_PATH)
except:
print ('[INFO] Unable to update geoip file. Using local copy.')
tor_process = stem.process.launch_tor_with_config(
config = {
'SocksPort' : str(SOCKS_PORT),
'EntryNodes' : '{FR}',
'ExitNodes' : '{JP}',
'StrictNodes' : '1',
'CookieAuthentication' : '1',
'MaxCircuitDirtiness' : '60',
'GeoIPFile' : 'https://raw.githubusercontent.com/torproject/tor/main/src/config/geoip',

},
init_msg_handler = lambda line: print(line) if re.search('Bootstrapped', line) else False,
tor_cmd = TOR_PATH
)

To check if your setup has been successful, you can make a GET request to http://ip-api.com/json/. If the IP address is from Japan and changes every 60 secs, you have successfully configured your TOR connection.

import requests
import time
from datetime import datetime
PROXIES = {
'http': 'socks5://127.0.0.1:9050',
'https': 'socks5://127.0.0.1:9050'
}
for i in range(10):
response = requests.get("http://ip-api.com/json/", proxies=PROXIES)
result = json.loads(response.content)
print('TOR IP [%s]: %s %s'%(datetime.now().strftime("%d-%m-%Y %H:%M:%S"), result["query"], result["country"]))
time.sleep(60)

Advanced Setup: Selecting a reliable relay for your TOR connection.

In the previous exercises, TOR is responsible for selecting the relays for creating its connection. However, there may be instances where you want to select a specific relay to enhance your anonymity and security over the internet.

Here are some consideration in selecting a good relay:

  1. Network speed and reliability
  2. Provider’s credibility/reputation
  3. Service uptime

To find a good TOR relay, we can use https://metrics.torproject.org/rs.html

  1. Search for public relay. Personally I like to use relay hosted by “Hetzner”, as they are a data center provider with credible background.

2. Copy the fingerprint of the relay

3. This code will allow you to use the selected relay as your entry node (1st relay).

# complete code found in create_advanced_tor_proxy.py
import io
import os
import stem.process
from stem.control import Controller
from stem import Signal
import re
import requests
import json
from datetime import datetime
import time
SOCKS_PORT = 9050
CONTROL_PORT = 9051
TOR_PATH = os.path.normpath(os.getcwd()+"\\tor\\tor.exe")
tor_process = stem.process.launch_tor_with_config(
config = {
'SocksPort' : str(SOCKS_PORT),
'ControlPort' : str(CONTROL_PORT),
'CookieAuthentication' : '1' ,
'EntryNodes' : '9695DFC35FFEB861329B9F1AB04C46397020CE31',
'StrictNodes' : '1'
},
init_msg_handler = lambda line: print(line) if re.search('Bootstrapped', line) else False,
tor_cmd = TOR_PATH
)

4. To check whether your setup has been successful, you can list out your TOR circuits. Verify that your 1st node has the same fingerprint as your configuration.

from stem import CircStatus
from stem.control import Controller
with Controller.from_port(port = 9051) as controller:
controller.authenticate()
for circ in sorted(controller.get_circuits()):
if circ.status == CircStatus.BUILT:
print("Circuit %s (%s)" % (circ.id, circ.purpose))
for i, entry in enumerate(circ.path):
div = '+' if (i == len(circ.path) - 1) else '|'
fingerprint, nickname = entry
desc = controller.get_network_status(fingerprint, None)
address = desc.address if desc else 'unknown'
print(" %s- %s (%s, %s)" % (div, fingerprint, nickname, address))

Don’t worry if you see a few circuits, TOR automatically creates additional circuits as backup.

Final words

I hope that you have enjoyed this practical tutorial as much as I do. It has been fun researching on TOR and stem library. There are actually more interesting stuff we can do with TOR, I will be writing it in the coming weeks! Stay tuned :)

--

--