add ssl_first: try SSL handshake before secondary check
When ssl_first=1 (default), proxy validation first attempts an SSL handshake. If it fails, falls back to the configured secondary check (head, judges, or irc). This separates SSL capability detection from basic connectivity testing. New config options: - ssl_first: enable SSL-first pattern (default: 1) - checktype: secondary check type (head, judges, irc)
This commit is contained in:
199
proxywatchd.py
199
proxywatchd.py
@@ -1451,41 +1451,146 @@ class TargetTestJob():
|
||||
sock.disconnect()
|
||||
|
||||
def _connect_and_test(self):
|
||||
"""Connect to target through the proxy and send test packet."""
|
||||
"""Connect to target through the proxy and send test packet.
|
||||
|
||||
If ssl_first is enabled:
|
||||
1. Try SSL handshake first
|
||||
2. If SSL succeeds -> return success
|
||||
3. If SSL fails -> try secondary check (configured checktype)
|
||||
"""
|
||||
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 ssl_first=%s' % (
|
||||
self.target_srv, self.checktype, config.watchd.ssl_first), ps.proxy)
|
||||
# Always log first test to verify code path
|
||||
global _sample_debug_counter
|
||||
if _sample_debug_counter == 0:
|
||||
_log('FIRST TEST: proxy=%s target=%s check=%s' % (ps.proxy, self.target_srv, self.checktype), 'info')
|
||||
_sample_dbg('TEST START: proxy=%s target=%s check=%s' % (ps.proxy, self.target_srv, self.checktype), ps.proxy)
|
||||
_log('FIRST TEST: proxy=%s target=%s check=%s ssl_first=%s' % (
|
||||
ps.proxy, self.target_srv, self.checktype, config.watchd.ssl_first), 'info')
|
||||
|
||||
protos = ['http', 'socks5', 'socks4'] if ps.proto is None else [ps.proto]
|
||||
pool = connection_pool.get_pool()
|
||||
|
||||
# Phase 1: SSL handshake (if ssl_first enabled)
|
||||
if config.watchd.ssl_first:
|
||||
result = self._try_ssl_handshake(protos, pool)
|
||||
if result is not None:
|
||||
return result # SSL succeeded or MITM detected
|
||||
# SSL failed for all protocols, continue to secondary check
|
||||
_dbg('SSL failed, trying secondary check: %s' % self.checktype, ps.proxy)
|
||||
|
||||
# Phase 2: Secondary check (configured checktype)
|
||||
return self._try_secondary_check(protos, pool)
|
||||
|
||||
def _try_ssl_handshake(self, protos, pool):
|
||||
"""Attempt SSL handshake to verify proxy works with TLS.
|
||||
|
||||
Returns:
|
||||
Tuple on success/MITM, None on failure (should try secondary check)
|
||||
"""
|
||||
ps = self.proxy_state
|
||||
ssl_target = random.choice(ssl_targets)
|
||||
last_error_category = None
|
||||
|
||||
for proto in protos:
|
||||
if pool:
|
||||
torhost = pool.get_tor_host(self.worker_id)
|
||||
else:
|
||||
torhost = random.choice(config.torhosts)
|
||||
network_stats.set_tor_node(torhost)
|
||||
|
||||
if proto == 'socks4':
|
||||
srv = socks4_resolve(ssl_target, 443)
|
||||
else:
|
||||
srv = ssl_target
|
||||
if not srv:
|
||||
continue
|
||||
|
||||
duration = time.time()
|
||||
if ps.auth:
|
||||
proxy_url = '%s://%s@%s:%s' % (proto, ps.auth, ps.ip, ps.port)
|
||||
else:
|
||||
proxy_url = '%s://%s:%s' % (proto, ps.ip, ps.port)
|
||||
proxies = [
|
||||
rocksock.RocksockProxyFromURL(tor_proxy_url(torhost)),
|
||||
rocksock.RocksockProxyFromURL(proxy_url),
|
||||
]
|
||||
|
||||
adaptive_timeout = config.watchd.timeout + min(
|
||||
ps.failcount * config.watchd.timeout_fail_inc,
|
||||
config.watchd.timeout_fail_max)
|
||||
|
||||
try:
|
||||
sock = rocksock.Rocksock(host=srv, port=443, ssl=1,
|
||||
proxies=proxies, timeout=adaptive_timeout,
|
||||
verifycert=True)
|
||||
_dbg('SSL handshake: proto=%s tor=%s target=%s' % (proto, torhost, ssl_target), ps.proxy)
|
||||
sock.connect()
|
||||
|
||||
# SSL handshake succeeded
|
||||
elapsed = time.time() - duration
|
||||
if pool:
|
||||
pool.record_success(torhost, elapsed)
|
||||
sock.disconnect()
|
||||
_dbg('SSL handshake OK', ps.proxy)
|
||||
return None, proto, duration, torhost, ssl_target, 0, 1, 'ssl_ok'
|
||||
|
||||
except rocksock.RocksockException as e:
|
||||
last_error_category = categorize_error(e)
|
||||
et = e.get_errortype()
|
||||
err = e.get_error()
|
||||
|
||||
try:
|
||||
sock.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
if et == rocksock.RS_ET_SSL and err == rocksock.RS_E_SSL_CERTIFICATE_ERROR:
|
||||
# MITM detected - proxy works but intercepts TLS
|
||||
ps.mitm = 1
|
||||
elapsed = time.time() - duration
|
||||
if pool:
|
||||
pool.record_success(torhost, elapsed)
|
||||
_dbg('SSL MITM detected', ps.proxy)
|
||||
return None, proto, duration, torhost, ssl_target, 0, 1, 'ssl_mitm'
|
||||
|
||||
if config.watchd.debug:
|
||||
_log('SSL handshake failed: %s://%s:%d: %s' % (
|
||||
proto, ps.ip, ps.port, e.get_errormessage()), 'debug')
|
||||
|
||||
# Check for Tor connection issues
|
||||
if et == rocksock.RS_ET_OWN:
|
||||
if e.get_failedproxy() == 0 and err == rocksock.RS_E_TARGET_CONN_REFUSED:
|
||||
if pool:
|
||||
pool.record_failure(torhost)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
|
||||
# All protocols failed SSL
|
||||
return None
|
||||
|
||||
def _try_secondary_check(self, protos, pool):
|
||||
"""Try the configured secondary checktype (head, judges, irc)."""
|
||||
ps = self.proxy_state
|
||||
_sample_dbg('TEST START: proxy=%s target=%s check=%s' % (
|
||||
ps.proxy, 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:
|
||||
connect_host = srvname.split('/')[0]
|
||||
else:
|
||||
connect_host = srvname
|
||||
|
||||
# SSL checktype: always use SSL with certificate verification
|
||||
if self.checktype == 'ssl':
|
||||
use_ssl = 1
|
||||
ssl_only_check = True # handshake only, no HTTP request
|
||||
server_port = 443
|
||||
verifycert = True
|
||||
# Secondary checks: always use plain HTTP
|
||||
use_ssl = 0
|
||||
verifycert = False
|
||||
if self.checktype == 'irc':
|
||||
server_port = 6667
|
||||
else:
|
||||
# head, judges, irc: always use plain HTTP
|
||||
use_ssl = 0
|
||||
ssl_only_check = False
|
||||
verifycert = False
|
||||
if self.checktype == 'irc':
|
||||
server_port = 6667
|
||||
else:
|
||||
server_port = 80
|
||||
protos = ['http', 'socks5', 'socks4'] if ps.proto is None else [ps.proto]
|
||||
last_error_category = None
|
||||
server_port = 80
|
||||
|
||||
# Get Tor host from pool (with worker affinity)
|
||||
pool = connection_pool.get_pool()
|
||||
last_error_category = None
|
||||
|
||||
for proto in protos:
|
||||
if pool:
|
||||
@@ -1527,14 +1632,6 @@ class TargetTestJob():
|
||||
_dbg('connected OK', ps.proxy)
|
||||
_sample_dbg('CONNECTED OK: %s via %s' % (ps.proxy, proto), ps.proxy)
|
||||
|
||||
# SSL-only check: handshake passed, no request needed
|
||||
if ssl_only_check:
|
||||
elapsed = time.time() - duration
|
||||
if pool:
|
||||
pool.record_success(torhost, elapsed)
|
||||
sock.disconnect()
|
||||
return None, proto, duration, torhost, srvname, 0, use_ssl, 'ssl_ok'
|
||||
|
||||
if self.checktype == 'irc':
|
||||
sock.send('NICK\n')
|
||||
elif self.checktype == 'judges':
|
||||
@@ -1602,48 +1699,6 @@ class TargetTestJob():
|
||||
if config.watchd.debug:
|
||||
_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 ssl_only_check:
|
||||
# SSL handshake failed - check if protocol error vs other error
|
||||
# fp contains the SSL error reason string
|
||||
if is_ssl_protocol_error(fp):
|
||||
# Protocol error (WRONG_VERSION_NUMBER, etc.) - proxy doesn't support SSL
|
||||
# No fallback needed, just fail this proxy for SSL
|
||||
if config.watchd.debug:
|
||||
_log('SSL protocol error, no fallback: %s://%s:%d (%s)' % (proto, ps.ip, ps.port, fp), 'debug')
|
||||
# Continue to try next protocol
|
||||
else:
|
||||
# Other SSL error - verify with HTTP HEAD fallback
|
||||
try:
|
||||
sock.disconnect()
|
||||
except Exception as e:
|
||||
if config.watchd.debug:
|
||||
_log('socket disconnect failed: %s' % e, 'debug')
|
||||
# Delay before secondary check (allows different Tor circuit)
|
||||
time.sleep(0.3)
|
||||
if config.watchd.debug:
|
||||
_log('SSL error, fallback to HTTP HEAD: %s://%s:%d (%s)' % (proto, ps.ip, ps.port, fp), 'debug')
|
||||
try:
|
||||
# Secondary check via HTTP HEAD (same as 'head' checktype)
|
||||
fallback_host = random.choice(list(regexes.keys()))
|
||||
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)
|
||||
fallback_proxies = [
|
||||
rocksock.RocksockProxyFromURL(tor_proxy_url(torhost)),
|
||||
rocksock.RocksockProxyFromURL(fallback_proxy_url),
|
||||
]
|
||||
fallback_sock = rocksock.Rocksock(host=fallback_host, port=80, ssl=0,
|
||||
proxies=fallback_proxies, timeout=adaptive_timeout)
|
||||
fallback_sock.connect()
|
||||
fallback_sock.send('HEAD / HTTP/1.0\r\nHost: %s\r\n\r\n' % fallback_host)
|
||||
elapsed = time.time() - duration
|
||||
if pool:
|
||||
pool.record_success(torhost, elapsed)
|
||||
return fallback_sock, proto, duration, torhost, fallback_host, 0, 0, 'ssl_fallback_head'
|
||||
except rocksock.RocksockException:
|
||||
pass # Fallback failed, continue to next protocol
|
||||
|
||||
except KeyboardInterrupt as e:
|
||||
raise e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user