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:
@@ -18,12 +18,13 @@ profiling = 0
|
||||
# Database file for proxy storage
|
||||
database = proxies.sqlite
|
||||
|
||||
# Check type(s): judges, ssl, irc, head, tor (comma-separated for random selection)
|
||||
# SSL-first mode: try SSL handshake first, fallback to secondary check on failure
|
||||
ssl_first = 1
|
||||
|
||||
# Secondary check type (used when ssl_first fails, or as primary when ssl_first=0)
|
||||
# judges - HTTP judge servers that echo back request headers
|
||||
# ssl - TLS handshake test (port 443, verifies MITM)
|
||||
# irc - IRC server connection test (port 6667)
|
||||
# head - HTTP HEAD request test (port 80)
|
||||
# tor - Tor exit check via check.torproject.org (port 80)
|
||||
checktype = head
|
||||
|
||||
# Thread configuration
|
||||
|
||||
@@ -51,8 +51,8 @@ class Config(ComboParser):
|
||||
if self.ppf.max_fail < 1:
|
||||
errors.append('ppf.max_fail must be >= 1')
|
||||
|
||||
# Validate checktypes (comma-separated list)
|
||||
valid_checktypes = {'irc', 'head', 'judges', 'ssl'}
|
||||
# Validate checktypes (secondary check types, ssl is handled by ssl_first)
|
||||
valid_checktypes = {'irc', 'head', 'judges'}
|
||||
for ct in self.watchd.checktypes:
|
||||
if ct not in valid_checktypes:
|
||||
errors.append('watchd.checktype "%s" invalid, must be one of: %s' % (ct, ', '.join(sorted(valid_checktypes))))
|
||||
@@ -111,7 +111,8 @@ class Config(ComboParser):
|
||||
self.add_item(section, 'stale_days', int, 30, 'days after which dead proxies are removed (default: 30)', False)
|
||||
self.add_item(section, 'stats_interval', int, 300, 'seconds between status reports (default: 300)', False)
|
||||
self.add_item(section, 'tor_safeguard', bool, True, 'enable tor safeguard (default: True)', False)
|
||||
self.add_item(section, 'checktype', str, 'ssl', 'check type(s): irc, head, judges, ssl (comma-separated for random)', False)
|
||||
self.add_item(section, 'checktype', str, 'head', 'secondary check type: irc, head, judges (used when ssl_first fails)', False)
|
||||
self.add_item(section, 'ssl_first', bool, True, 'try SSL handshake first, fallback to checktype on failure (default: True)', False)
|
||||
self.add_item(section, 'scale_cooldown', int, 10, 'seconds between thread scaling decisions (default: 10)', False)
|
||||
self.add_item(section, 'scale_threshold', float, 10.0, 'min success rate % to scale up threads (default: 10.0)', False)
|
||||
|
||||
|
||||
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