diff --git a/proxywatchd.py b/proxywatchd.py index 7dc387d..4ed860f 100644 --- a/proxywatchd.py +++ b/proxywatchd.py @@ -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