diff --git a/proxywatchd.py b/proxywatchd.py index da14f1c..5c5f73c 100644 --- a/proxywatchd.py +++ b/proxywatchd.py @@ -142,6 +142,20 @@ def is_valid_ip(ip_str): except (ValueError, AttributeError): return False +def is_public_ip(ip_str): + """Validate IP is a public, globally routable address.""" + if not is_valid_ip(ip_str): + return False + parts = [int(p) for p in ip_str.split('.')] + if parts[0] == 0: return False # 0.0.0.0/8 + if parts[0] == 10: return False # 10.0.0.0/8 + if parts[0] == 127: return False # 127.0.0.0/8 + if parts[0] == 169 and parts[1] == 254: return False # link-local + if parts[0] == 172 and 16 <= parts[1] <= 31: return False # 172.16/12 + if parts[0] == 192 and parts[1] == 168: return False # 192.168/16 + if parts[0] >= 224: return False # multicast + reserved + return True + # Pattern for header echo - if X-Forwarded-For or Via present, proxy reveals chain HEADER_REVEAL_PATTERN = r'(X-Forwarded-For|Via|X-Real-Ip|Forwarded)' @@ -393,10 +407,20 @@ class ProxyTestState(object): self.evaluated = True self.checktime = int(time.time()) - successes = [r for r in self.results if r['success']] - failures = [r for r in self.results if not r['success']] + # Filter out judge_block results (inconclusive, neither pass nor fail) + real_results = [r for r in self.results if r.get('category') != 'judge_block'] + successes = [r for r in real_results if r['success']] + failures = [r for r in real_results if not r['success']] num_success = len(successes) - _dbg('evaluate: %d success, %d fail, results=%d' % (num_success, len(failures), len(self.results)), self.proxy) + judge_blocks = len(self.results) - len(real_results) + _dbg('evaluate: %d success, %d fail, %d judge_block, results=%d' % ( + num_success, len(failures), judge_blocks, len(self.results)), self.proxy) + + # All results were judge blocks: inconclusive, preserve current state + if not real_results and self.results: + _dbg('all results inconclusive (judge_block), no state change', self.proxy) + self.failcount = self.original_failcount + return (self.original_failcount == 0, None) # Determine dominant failure category fail_category = None @@ -547,6 +571,12 @@ class TargetTestJob(object): recv = sock.recv(-1) _sample_dbg('RECV: %d bytes from %s, first 80: %r' % (len(recv), srv, recv[:80]), self.proxy_state.proxy) + # Validate HTTP response for non-IRC checks + if self.checktype != 'irc' and not recv.startswith('HTTP/'): + _dbg('not an HTTP response, failing (first 40: %r)' % recv[:40], self.proxy_state.proxy) + self.proxy_state.record_result(False, category='bad_response') + return + # Select regex based on check type (or fallback target) if 'check.torproject.org' in srv: # Tor API fallback (judge using torproject.org) @@ -571,7 +601,7 @@ class TargetTestJob(object): reveals_headers = None if self.checktype == 'judges' or 'check.torproject.org' in srv: ip_match = re.search(IP_PATTERN, recv) - if ip_match and is_valid_ip(ip_match.group(0)): + if ip_match and is_public_ip(ip_match.group(0)): exit_ip = ip_match.group(0) if self.checktype == 'judges' and 'check.torproject.org' not in srv: # Check for header echo judge (elite detection) @@ -590,17 +620,14 @@ class TargetTestJob(object): # Check if judge is blocking us (not a proxy failure) if self.checktype == 'judges' and JUDGE_BLOCK_RE.search(recv): judge_stats.record_block(srv) - # Judge block = proxy worked, we got HTTP response, just no IP - # Count as success without exit_ip - block_elapsed = time.time() - duration - _dbg('judge BLOCK detected, counting as success', self.proxy_state.proxy) + # Judge block = inconclusive, not a pass or fail + _dbg('judge BLOCK detected, skipping (neutral)', self.proxy_state.proxy) self.proxy_state.record_result( - True, proto=proto, duration=block_elapsed, - srv=srv, tor=tor, ssl=is_ssl, exit_ip=None, - reveals_headers=None + False, category='judge_block', proto=proto, + srv=srv, tor=tor, ssl=is_ssl ) if config.watchd.debug: - _log('judge %s challenged proxy %s (counted as success)' % ( + _log('judge %s challenged proxy %s (neutral, skipped)' % ( srv, self.proxy_state.proxy), 'debug') else: _dbg('FAIL: no match, no block', self.proxy_state.proxy) @@ -675,15 +702,14 @@ class TargetTestJob(object): protos = self._build_proto_order() pool = connection_pool.get_pool() - # Phase 1: SSL handshake (if ssl_first enabled) - if config.watchd.ssl_first: + # Phase 1: SSL handshake (if ssl_first enabled or SSL-only mode) + if config.watchd.ssl_first or self.checktype == 'none': result = self._try_ssl_handshake(protos, pool) if result is not None: return result # SSL succeeded or MITM detected # SSL failed for all protocols - if config.watchd.ssl_only: - # ssl_only mode: skip secondary check, mark as failed - _dbg('SSL failed, ssl_only mode, skipping secondary check', ps.proxy) + if config.watchd.ssl_only or self.checktype == 'none': + _dbg('SSL failed, no secondary check', ps.proxy) return (None, None, 0, None, None, 1, 0, 'ssl_only') _dbg('SSL failed, trying secondary check: %s' % self.checktype, ps.proxy) @@ -1357,7 +1383,11 @@ class Proxywatchd(): # Build target pools for each checktype target_pools = {} for ct in checktypes: - if ct == 'irc': + if ct == 'none': + # SSL-only mode: use ssl_targets as placeholder + target_pools[ct] = ssl_targets + _dbg('target_pool[none]: SSL-only mode, %d ssl targets' % len(ssl_targets)) + elif ct == 'irc': target_pools[ct] = config.servers _dbg('target_pool[irc]: %d servers' % len(config.servers)) elif ct == 'judges': diff --git a/stats.py b/stats.py index a24396c..1b6dc95 100644 --- a/stats.py +++ b/stats.py @@ -107,11 +107,9 @@ regexes = { 'www.twitter.com': 'x-connection-hash', 't.co': 'x-connection-hash', 'www.msn.com': 'x-aspnetmvc-version', - 'www.bing.com': 'p3p', 'www.ask.com': 'x-served-by', 'www.hotmail.com': 'x-msedge-ref', 'www.bbc.co.uk': 'x-bbc-edge-cache-status', - 'www.skype.com': 'X-XSS-Protection', 'www.alibaba.com': 'object-status', 'www.mozilla.org': 'cf-ray', 'www.cloudflare.com': 'cf-ray', @@ -121,7 +119,6 @@ regexes = { 'www.netflix.com': 'X-Netflix.proxy.execution-time', 'www.amazon.de': 'x-amz-cf-id', 'www.reuters.com': 'x-amz-cf-id', - 'www.ikea.com': 'x-frame-options', 'www.twitpic.com': 'timing-allow-origin', 'www.digg.com': 'cf-request-id', 'www.wikia.com': 'x-served-by', @@ -133,8 +130,6 @@ regexes = { 'www.yelp.com': 'x-timer', 'www.ebay.com': 'x-envoy-upstream-service-time', 'www.wikihow.com': 'x-c', - 'www.archive.org': 'referrer-policy', - 'www.pandora.tv': 'X-UA-Compatible', 'www.w3.org': 'x-backend', 'www.time.com': 'x-amz-cf-pop' }