In contemplating traffic analysis, it occurred to me that having something generating extra legitimate (but irrelevant) traffic might help. To that end, I wrote a short Python script that:
- Takes one or more "seed" URLs
- Downloads the page(s) at random from the list
- Fetches the URLs mentioned on the page, adding them to the list
- Pause for a random length of time, then repeat
The script uses the multiprocessing module to run multiple instances of the function that does the above. Each instance roughly simulates a user randomly clicking on links.
The idea is that if there are enough instances running, a traffic analysis attack will become very difficult for the adversary. The downside is that it wastes bandwidth. It can be run with the HTTP_PROXY and HTTPS_PROXY environment variables set to cause the traffic to go through TOR (to obfuscate relay / exit node analysis) or without (to obfuscate ISP level traffic analysis).
I would like to get other opinions on the merits and problems with this idea. The code is as follows:
#!/usr/bin/env python3
import re
import requests
import sys
import random
import argparse
import multiprocessing
import signal
import time
import os
links = list()
link_search = re.compile(r"(?i)(https?://[^><\"\'\s]+)")
def pid_print(msg):
print(str(os.getpid()) + ": " + msg)
def find_urls(page_text):
global link_search
urls = link_search.findall(page_text)
if urls:
return urls
else:
return []
def run_link():
global links
global args
link = links[random.randint(0, len(links) - 1)]
if args.verbose:
pid_print(str(len(links)) + " links: navigating to " + link)
try:
resp = requests.get(link)
if 200 <= resp.status_code <= 299:
start_len = len(links)
links.extend(find_urls(resp.text))
links.sort()
links = list(set(links))
end_len = len(links)
if end_len > start_len and args.verbose:
pid_print("Found " + str(end_len - start_len) + " new links")
while len(links) > 200000:
del links[random.randint(0, len(links) - 1)]
else:
if args.verbose:
pid_print(link + " - HTTP " + str(resp.status_code))
if link in links:
links.remove(link)
except requests.exceptions.MissingSchema:
if args.verbose:
pid_print(" - failed, missing schema")
if link in links:
links.remove(link)
links.append("https://" + link)
except Exception as ee:
if args.verbose:
pid_print(" - failed, " + str(ee))
if link in links:
links.remove(link)
def run_loop():
while True:
global args
run_link()
time.sleep(random.randint(int(args.min_linger), int(args.max_linger)))
def signal_handler(signum, frame):
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
parser.add_argument('-t', '--threads', help='Number of threads (default 4)', default=4)
parser.add_argument('-m', '--min_linger', help='Minimum number of seconds to wait after page pull', default=2)
parser.add_argument('-x', '--max_linger', help='Maximum number of seconds to wait after page pull', default=10)
parser.add_argument('links', nargs='*', help='Seed links')
args = parser.parse_args()
if len(args.links) < 1:
sys.stderr.write("Please supply one or more seed URLs as arguments\n")
sys.exit(1)
links.extend(args.links)
print("Starting with a seed of " + str(len(links)) + " URLs")
child_list = list()
for i in range(1, int(args.threads)):
child_list.append(multiprocessing.Process(
target=run_loop
))
child_list[-1].start()
print("Started child " + str(child_list[-1].pid))
for t in child_list:
t.join()