watchd: adaptive ssl for secondary checks
Some checks failed
CI / syntax-check (push) Failing after 1s
CI / memory-leak-check (push) Successful in 18s

Use SSL error reason from primary handshake to decide whether
the secondary check should use SSL or plain HTTP. Protocol errors
(proxy can't TLS) fall back to plain HTTP; other failures retry
with SSL sans cert verification.
This commit is contained in:
Username
2026-02-18 09:49:40 +01:00
parent 727ed86692
commit 3e5c486e7e

View File

@@ -800,8 +800,9 @@ class TargetTestJob(object):
protos = [detected] + [p for p in protos if p != detected]
# Phase 1: SSL handshake (if ssl_first enabled or SSL-only mode)
ssl_reason = None
if config.watchd.ssl_first or self.checktype == 'none':
result = self._try_ssl_handshake(protos, pool)
result, ssl_reason = self._try_ssl_handshake(protos, pool)
if result is not None:
return result # SSL succeeded or MITM detected
# SSL failed for all protocols
@@ -811,17 +812,20 @@ class TargetTestJob(object):
_dbg('SSL failed, trying secondary check: %s' % self.checktype, ps.proxy)
# Phase 2: Secondary check (configured checktype)
return self._try_secondary_check(protos, pool)
return self._try_secondary_check(protos, pool, ssl_reason)
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)
(result, ssl_reason) where result is a tuple on success/MITM
or None on failure, and ssl_reason is the last SSL error reason
string (for secondary check SSL/plain decision).
"""
ps = self.proxy_state
ssl_target = random.choice(ssl_targets)
last_error_category = None
last_ssl_reason = None
for proto in protos:
if pool:
@@ -864,13 +868,19 @@ class TargetTestJob(object):
pool.record_success(torhost, elapsed)
sock.disconnect()
_dbg('SSL handshake OK', ps.proxy)
return None, proto, duration, torhost, ssl_target, 0, 1, 'ssl_ok'
return (None, proto, duration, torhost, ssl_target, 0, 1, 'ssl_ok'), None
except rocksock.RocksockException as e:
last_error_category = categorize_error(e)
et = e.get_errortype()
err = e.get_error()
# Track SSL reason for secondary check decision
if et == rocksock.RS_ET_SSL:
reason = e.get_failedproxy()
if isinstance(reason, str):
last_ssl_reason = reason
try:
sock.disconnect()
except:
@@ -883,7 +893,7 @@ class TargetTestJob(object):
if pool:
pool.record_success(torhost, elapsed)
_dbg('SSL MITM detected', ps.proxy)
return None, proto, duration, torhost, ssl_target, 0, 1, 'ssl_mitm'
return (None, proto, duration, torhost, ssl_target, 0, 1, 'ssl_mitm'), None
if config.watchd.debug:
_log('SSL handshake failed: %s://%s:%d: %s' % (
@@ -899,10 +909,16 @@ class TargetTestJob(object):
raise
# All protocols failed SSL
return None
return None, last_ssl_reason
def _try_secondary_check(self, protos, pool):
"""Try the configured secondary checktype (head, judges, irc)."""
def _try_secondary_check(self, protos, pool, ssl_reason=None):
"""Try the configured secondary checktype (head, judges, irc).
ssl_reason: last SSL error reason from _try_ssl_handshake, used to
decide whether to use SSL or plain HTTP for the secondary check.
Protocol errors (proxy doesn't speak TLS) -> plain HTTP.
Other errors (cert, timeout, etc.) -> SSL without cert verification.
"""
ps = self.proxy_state
_sample_dbg('TEST START: proxy=%s target=%s check=%s' % (
ps.proxy, self.target_srv, self.checktype), ps.proxy)
@@ -914,13 +930,26 @@ class TargetTestJob(object):
else:
connect_host = srvname
# Secondary checks: always use plain HTTP
use_ssl = 0
# Decide SSL based on why the primary handshake failed:
# - protocol error (proxy can't TLS) -> plain HTTP
# - other error (cert, timeout) -> SSL without cert verification
# - no ssl_reason (ssl_first off) -> plain HTTP (no prior info)
protocol_error = is_ssl_protocol_error(ssl_reason) if ssl_reason else True
verifycert = False
if self.checktype == 'irc':
server_port = 6667
if protocol_error:
use_ssl = 0
if self.checktype == 'irc':
server_port = 6667
else:
server_port = 80
_dbg('secondary: plain (ssl protocol error)', ps.proxy)
else:
server_port = 80
use_ssl = 1
if self.checktype == 'irc':
server_port = 6697
else:
server_port = 443
_dbg('secondary: ssl/no-verify (non-protocol ssl error)', ps.proxy)
last_error_category = None