Skip to content

Instantly share code, notes, and snippets.

@mrphs
Last active January 16, 2023 18:12
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mrphs/a63c6b9134080265bab5 to your computer and use it in GitHub Desktop.
Save mrphs/a63c6b9134080265bab5 to your computer and use it in GitHub Desktop.
testing obfs4 performance
#!/usr/bin/env python
import csv
import datetime
import os
import os.path
import re
import sys
import time
import urllib2
import stem
import stem.control
import stem.process
import torsocks
OBFS4PROXY = "/usr/bin/obfs4proxy"
# 10 MB file
URL = "http://speedtest.wdc01.softlayer.com/downloads/test10.zip"
# https://globe.torproject.org/#/relay/B204DE75B37064EF6A4C6BAF955C5724578D0B32
# cry, NL
MIDDLE_NODE = "B204DE75B37064EF6A4C6BAF955C5724578D0B32"
# https://globe.torproject.org/#/relay/E1E922A20AF608728824A620BADC6EFC8CB8C2B8
# TorLand1, GB.
EXIT_NODE = "E1E922A20AF608728824A620BADC6EFC8CB8C2B8"
BASE_CONFIG = {
"SOCKSPort": "auto",
"ControlPort": "auto",
"DataDirectory": os.path.join(os.getcwd(), "datadir"),
"CookieAuthentication": "1",
"LearnCircuitBuildTimeout": "0",
"CircuitBuildTimeout": "300",
"__DisablePredictedCircuits": "1",
"__LeaveStreamsUnattached": "1",
"FetchHidServDescriptors": "0",
"UseBridges": "1",
"ClientTransportPlugin": "obfs4 exec " + OBFS4PROXY,
}
class Bridge(object):
def __init__(self, transport, addr, fingerprint, *rest):
self.transport = transport
self.addr = addr
self.fingerprint = fingerprint
self.rest = rest
def __str__(self):
parts = []
if self.transport is not None:
parts.append(self.transport)
parts.append(self.addr)
parts.append(self.fingerprint)
parts.extend(self.rest)
return " ".join(parts)
BRIDGES = (
Bridge("obfs4", "154.35.22.10:41835", "8FB9F4319E89E5C6223052AA525A192AFBC85D55", "cert=GGGS1TX4R81m3r0HBl79wKy1OtPPNR2CZUIrHjkRg65Vc2VR8fOyo64f9kmT1UAFG7j0HQ", "iat-mode=0"),
Bridge("obfs4", "154.35.22.11:49868", "A832D176ECD5C7C6B58825AE22FC4C90FA249637", "cert=YPbQqXPiqTUBfjGFLpm9JYEFTBvnzEJDKJxXG5Sxzrr/v2qrhGU4Jls9lHjLAhqpXaEfZw", "iat-mode=0"),
Bridge("obfs4", "154.35.22.12:80", "00DC6C4FA49A65BD1472993CF6730D54F11E0DBB", "cert=N86E9hKXXXVz6G7w2z8wFfhIDztDAzZ/3poxVePHEYjbKDWzjkRDccFMAnhK75fc65pYSg", "iat-mode=0"),
Bridge("obfs4", "154.35.22.13:443", "FE7840FE1E21FE0A0639ED176EDA00A3ECA1E34D", "cert=fKnzxr+m+jWXXQGCaXe4f2gGoPXMzbL+bTBbXMYXuK0tMotd+nXyS33y2mONZWU29l81CA", "iat-mode=0"),
)
def start_tor(config):
ports = {}
# Parse status messages and set socks_port and control_port.
def parse_msg(s):
print >> sys.stderr, s
m = re.search(r'Socks listener listening on port ([0-9]+)\.', s)
if m is not None:
ports["socks"] = int(m.group(1))
m = re.search(r'Control listener listening on port ([0-9]+)\.', s)
if m is not None:
ports["control"] = int(m.group(1))
tor = stem.process.launch_tor_with_config(config=config, timeout=300, take_ownership=True, completion_percent=80, init_msg_handler=parse_msg)
return tor, ports["socks"], ports["control"]
def download(url, csvw, run_id, fingerprint):
start_time = time.time()
count = 0
def logline(now=None, msg=None):
if now is None:
now = time.time()
if msg is None:
msg = str(count)
csvw.writerow([run_id, fingerprint, url, "%.2f" % (now-start_time), msg])
logline(start_time)
try:
f = urllib2.urlopen(url)
while True:
data = f.read(4096)
if not data:
break
count += len(data)
logline()
except Exception as e:
logline(msg=str(e))
else:
logline()
f.close()
csvw = csv.writer(sys.stdout)
run_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
for bridge in BRIDGES:
config = BASE_CONFIG.copy()
config["Bridge"] = str(bridge)
tor, socks_port, control_port = start_tor(config)
controller = stem.control.Controller.from_port(port=control_port)
controller.authenticate()
circ = controller.new_circuit([bridge.fingerprint, MIDDLE_NODE, EXIT_NODE], await_build=True)
def stream_event(event):
if event.status == stem.StreamStatus.NEW and event.purpose == stem.StreamPurpose.USER:
controller.attach_stream(event.id, circ)
controller.add_event_listener(stream_event, stem.control.EventType.STREAM)
torsocks.run_python_over_tor(socks_port)(download, URL, csvw, run_id, bridge.fingerprint)
tor.terminate()
tor.wait()
# Copyright 2015 Philipp Winter <phw@nymity.ch>
#
# This file is part of exitmap.
#
# exitmap is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# exitmap is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with exitmap. If not, see <http://www.gnu.org/licenses/>.
"""
Provide a Tor-specific SOCKSv5 interface.
"""
import struct
import socket
proxy_addr = None
proxy_port = None
orig_socket = socket.socket
class SOCKSv5Error(Exception):
pass
def set_default_proxy(ip_addr, port):
"""
Set the SOCKS proxy address and its port.
"""
global proxy_addr, proxy_port
proxy_addr, proxy_port = ip_addr, port
class torsocket(socket.socket):
"""
Provides a minimal, Tor-specific SOCKSv5 interface.
"""
def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM,
proto=0, _sock=None):
self.sockfamily = family
self.socktype = type
super(torsocket, self).__init__(family, type, proto, _sock)
def _authenticate(self):
"""
Authenticate to our SOCKSv5 server.
"""
assert (proxy_addr is not None) and (proxy_port is not None)
# Connect to SOCKSv5 server. We use version 5 and one authentication
# method, which is "no authentication".
orig_socket.connect(self, (proxy_addr, proxy_port))
self.sendall("\x05\x01\x00")
resp = self.recv(2)
if resp != "\x05\x00":
raise SOCKSv5Error("Invalid server response: 0x%s" %
resp.encode("hex"))
def resolve(self, domain):
"""
Resolve the given domain using Tor's SOCKS resolution extension.
"""
domain_len = len(domain)
if domain_len > 255:
raise SOCKSv5Error("Domain must not be longer than 255 "
"characters, but %d given." % domain_len)
# Tor defines a new command value, \x0f, that is used for domain
# resolution.
self._authenticate()
self.sendall("\x05\xf0\x00\x03%s%s%s" %
(chr(domain_len), domain, "\x00\x00"))
resp = self.recv(10)
if resp[:2] != "\x05\x00":
raise SOCKSv5Error("Invalid server response: 0x%s" %
resp[1].encode("hex"))
return socket.inet_ntoa(resp[4:8])
def connect(self, addr_tuple):
"""
Tell SOCKS server to connect to our destination.
"""
dst_addr, dst_port = addr_tuple[0], int(addr_tuple[1])
self._authenticate()
# Tell SOCKS server to connect to destination.
self.sendall("\x05\x01\x00\x01%s%s" %
(socket.inet_aton(dst_addr), struct.pack(">H", dst_port)))
resp = self.recv(4)
if resp[1] != "\x00":
raise SOCKSv5Error("Connection failed. Server responded "
"with 0x%s." % resp[0].encode("hex"))
# Depending on address type, get address.
if resp[3] == "\x01":
self.recv(4)
elif resp[3] == "\x03":
length = self.recv(1)
self.recv(length)
else:
self.recv(16)
# Get port.
self.recv(2)
def run_python_over_tor(socks_port):
"""
Returns a closure to route a Python function's network traffic over Tor.
"""
def closure(func, *args):
"""
Route the given Python function's network traffic over Tor.
We temporarily monkey-patch socket.socket using our torsocks module and
reset it, once the function returns.
"""
set_default_proxy("127.0.0.1", socks_port)
orig_socket = socket.socket
socket.socket = torsocket
func(*args)
socket.socket = orig_socket
return closure
@xanoni
Copy link

xanoni commented Nov 1, 2021

Sir — haven't read the script but seems this is something to benchmark performance of obfs4 Tor bridges ... curious, what results did you get when you ran this (compared to vanilla bridges)? Might run myself but curious. Thank you.

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