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