#!/usr/bin/env python3 """ Fake Tor control port for the fd-bug harness. dragonxd's torcontrol.cpp opens a libevent bufferevent to -torcontrol=, speaks the control protocol (PROTOCOLINFO / AUTHCHALLENGE / AUTHENTICATE / ADD_ONION), and on EOF/ERROR reconnects with exponential backoff. We give it a server on a loopback port that behaves badly in several ways so the reconnect/teardown loop (bufferevent_free with BEV_OPT_CLOSE_ON_FREE, the event_base, the reconnect timer) runs constantly. Each accepted connection picks a behaviour at random: close : accept, then immediately close (EOF right away) rst : accept, set SO_LINGER 0, close -> sends RST stall : accept, read nothing, hold the socket open then drop it garbage : send non-control-protocol bytes then close halfok : answer PROTOCOLINFO plausibly, then drop before AUTHENTICATE completes Loopback only. Run with --port. Ctrl-C / SIGTERM stops it. """ import argparse, random, socket, struct, threading, time, sys, signal STOP = threading.Event() def handle(conn, mode): try: conn.settimeout(5.0) if mode == "close": conn.close(); return if mode == "rst": conn.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) conn.close(); return if mode == "stall": # read whatever the controller sends but never reply; hold 2-4s try: conn.recv(4096) except Exception: pass time.sleep(random.uniform(2.0, 4.0)) conn.close(); return if mode == "garbage": try: conn.recv(4096) except Exception: pass conn.sendall(b"\x00\xff\x01garbage not a tor reply\r\n" * 4) conn.close(); return if mode == "halfok": # read the PROTOCOLINFO line, give a plausible 250 reply advertising # NULL auth, then drop the connection before AUTHENTICATE finishes. try: conn.recv(4096) except Exception: pass conn.sendall(b"250-PROTOCOLINFO 1\r\n" b"250-AUTH METHODS=NULL\r\n" b'250-VERSION Tor="0.4.7.13"\r\n' b"250 OK\r\n") time.sleep(random.uniform(0.2, 1.0)) conn.close(); return except Exception: try: conn.close() except Exception: pass def main(): ap = argparse.ArgumentParser() ap.add_argument("--port", type=int, required=True) ap.add_argument("--host", default="127.0.0.1") args = ap.parse_args() modes = ["close", "rst", "stall", "garbage", "halfok"] srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((args.host, args.port)) srv.listen(16) srv.settimeout(1.0) print(f"faketor: listening on {args.host}:{args.port}", flush=True) def onsig(*_): STOP.set() signal.signal(signal.SIGTERM, onsig) signal.signal(signal.SIGINT, onsig) n = 0 while not STOP.is_set(): try: conn, _ = srv.accept() except socket.timeout: continue except Exception: break mode = modes[n % len(modes)] n += 1 threading.Thread(target=handle, args=(conn, mode), daemon=True).start() srv.close() print(f"faketor: stopped after {n} connections", flush=True) if __name__ == "__main__": main()