watchd: add tor checktype, use Tor API for secondary check
This commit is contained in:
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user