watchd: add tor checktype, use Tor API for secondary check
All checks were successful
CI / syntax-check (push) Successful in 3s
CI / memory-leak-check (push) Successful in 11s

This commit is contained in:
Username
2025-12-26 21:20:16 +01:00
parent d2bd7d4f34
commit f7a762331a

View File

@@ -273,6 +273,12 @@ ssl_targets = [
'www.letsencrypt.org',
]
# Tor check targets - verify proxy exits through Tor network
# Response contains JSON with IsTor: true/false
tor_targets = [
'check.torproject.org/api/ip',
]
class Stats():
"""Track and report comprehensive runtime statistics."""
@@ -1340,11 +1346,16 @@ class TargetTestJob():
try:
recv = sock.recv(-1)
# Select regex based on check type
if self.checktype == 'irc':
# Select regex based on check type (or fallback target)
if 'check.torproject.org' in srv:
# Tor API fallback or tor checktype
regex = r'"IsTor"\s*:\s*true'
elif self.checktype == 'irc':
regex = '^(:|NOTICE|ERROR)'
elif self.checktype == 'judges':
regex = judges[srv]
elif self.checktype == 'tor':
regex = r'"IsTor"\s*:\s*true'
elif self.checktype == 'ssl':
# Should not reach here - ssl returns before recv
self.proxy_state.record_result(True, proto=proto, srv=srv, ssl=is_ssl)
@@ -1356,19 +1367,20 @@ class TargetTestJob():
if re.search(regex, recv, re.IGNORECASE):
elapsed = time.time() - duration
_dbg('regex MATCH, elapsed=%.2fs' % elapsed, self.proxy_state.proxy)
# Extract exit IP from judge response
# Extract exit IP from judge/tor response
exit_ip = None
reveals_headers = None
if self.checktype == 'judges':
if self.checktype == 'judges' or self.checktype == 'tor' or 'check.torproject.org' in srv:
ip_match = re.search(IP_PATTERN, recv)
if ip_match:
exit_ip = ip_match.group(0)
# Check for header echo judge (elite detection)
if 'headers' in srv:
# If X-Forwarded-For/Via/etc present, proxy reveals chain
reveals_headers = bool(re.search(HEADER_REVEAL_PATTERN, recv, re.IGNORECASE))
# Record successful judge
judge_stats.record_success(srv)
if self.checktype == 'judges' and 'check.torproject.org' not in srv:
# Check for header echo judge (elite detection)
if 'headers' in srv:
# If X-Forwarded-For/Via/etc present, proxy reveals chain
reveals_headers = bool(re.search(HEADER_REVEAL_PATTERN, recv, re.IGNORECASE))
# Record successful judge
judge_stats.record_success(srv)
self.proxy_state.record_result(
True, proto=proto, duration=elapsed,
srv=srv, tor=tor, ssl=is_ssl, exit_ip=exit_ip,
@@ -1410,8 +1422,8 @@ class TargetTestJob():
ps = self.proxy_state
_dbg('_connect_and_test: target=%s checktype=%s' % (self.target_srv, self.checktype), ps.proxy)
srvname = self.target_srv.strip() if self.checktype == 'irc' else self.target_srv
# For judges, extract host from 'host/path' format
if self.checktype == 'judges' and '/' in srvname:
# For judges/tor, extract host from 'host/path' format
if (self.checktype == 'judges' or self.checktype == 'tor') and '/' in srvname:
connect_host = srvname.split('/')[0]
else:
connect_host = srvname
@@ -1422,6 +1434,12 @@ class TargetTestJob():
ssl_only_check = True # handshake only, no HTTP request
server_port = 443
verifycert = True
elif self.checktype == 'tor':
# Tor check uses HTTP by default (like judges/head)
use_ssl = random.choice([0, 1]) if config.watchd.use_ssl == 2 else config.watchd.use_ssl
ssl_only_check = False
server_port = 443 if use_ssl else 80
verifycert = True if use_ssl else False
else:
use_ssl = random.choice([0, 1]) if config.watchd.use_ssl == 2 else config.watchd.use_ssl
ssl_only_check = False # minimal SSL test (handshake only, no request)
@@ -1488,8 +1506,8 @@ class TargetTestJob():
if self.checktype == 'irc':
sock.send('NICK\n')
elif self.checktype == 'judges':
# GET request to receive body with IP address
elif self.checktype == 'judges' or self.checktype == 'tor':
# GET request to receive body (IP for judges, JSON for tor)
sock.send('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (
srvname.split('/', 1)[1] if '/' in srvname else '',
srvname.split('/')[0]
@@ -1552,7 +1570,7 @@ class TargetTestJob():
_log('failed to extract MITM cert: %s' % str(e), 'debug')
return None, proto, duration, torhost, srvname, 0, use_ssl, 'ssl_mitm'
elif et == rocksock.RS_ET_SSL and not ssl_only_check:
# SSL failed but proxy protocol worked - fallback to HTTP
# SSL failed but proxy protocol worked - fallback to Tor API check (HTTP)
# sock already disconnected above, but ensure cleanup
try:
sock.disconnect()
@@ -1561,28 +1579,28 @@ class TargetTestJob():
# Delay before secondary check (allows different Tor circuit)
time.sleep(0.3)
if config.watchd.debug:
_log('SSL failed, fallback to HTTP: %s://%s:%d' % (proto, ps.ip, ps.port), 'debug')
_log('SSL failed, fallback to Tor API: %s://%s:%d' % (proto, ps.ip, ps.port), 'debug')
try:
http_port = 80
# New Tor credentials = new circuit
# Secondary check via Tor Project API (plain HTTP)
tor_check_host = 'check.torproject.org'
if ps.auth:
fallback_proxy_url = '%s://%s@%s:%s' % (proto, ps.auth, ps.ip, ps.port)
else:
fallback_proxy_url = '%s://%s:%s' % (proto, ps.ip, ps.port)
http_proxies = [
fallback_proxies = [
rocksock.RocksockProxyFromURL(tor_proxy_url(torhost)),
rocksock.RocksockProxyFromURL(fallback_proxy_url),
]
http_sock = rocksock.Rocksock(host=connect_host, port=http_port, ssl=0,
proxies=http_proxies, timeout=adaptive_timeout)
http_sock.connect()
http_sock.send('HEAD / HTTP/1.0\r\nHost: %s\r\n\r\n' % srvname)
fallback_sock = rocksock.Rocksock(host=tor_check_host, port=80, ssl=0,
proxies=fallback_proxies, timeout=adaptive_timeout)
fallback_sock.connect()
fallback_sock.send('GET /api/ip HTTP/1.0\r\nHost: %s\r\n\r\n' % tor_check_host)
elapsed = time.time() - duration
if pool:
pool.record_success(torhost, elapsed)
return http_sock, proto, duration, torhost, srvname, 0, 0, 'ssl_fallback_http'
return fallback_sock, proto, duration, torhost, tor_check_host + '/api/ip', 0, 0, 'ssl_fallback_tor'
except rocksock.RocksockException:
pass # HTTP fallback failed, continue to next protocol
pass # Fallback failed, continue to next protocol
except KeyboardInterrupt as e:
raise e
@@ -1845,6 +1863,9 @@ class Proxywatchd():
elif ct == 'ssl':
target_pools[ct] = ssl_targets
_dbg('target_pool[ssl]: %d targets' % len(ssl_targets))
elif ct == 'tor':
target_pools[ct] = tor_targets
_dbg('target_pool[tor]: %d targets' % len(tor_targets))
else: # http/head
target_pools[ct] = list(regexes.keys())
_dbg('target_pool[%s]: %d targets' % (ct, len(regexes)))