#!/usr/bin/env python3 """Validate private subscriptions; network execution requires explicit --run. For a formal multi-node subscription use a private --expectations JSON array: [{"tag": "exact outbound tag", "host": "relay.example", "port": 12345, "sni": "node.example", "uuid": "private password", "expected_exit": "192.0.2.1", "expected_country": "JP"}]. Never print credentials or the subscription. """ import argparse import copy import ipaddress import json import os from pathlib import Path import shutil import socket import subprocess import tempfile import time class VerificationError(Exception): pass def require(condition, message): if not condition: raise VerificationError(message) def read_private(path): p = Path(path) require(p.is_file() and not p.is_symlink(), 'Private input must be a regular file') require(p.stat().st_mode & 0o077 == 0, 'Private input must have no group/other permissions') return json.loads(p.read_text()) def validate(config, expectations, exact=True): require(isinstance(config, dict) and isinstance(config.get('outbounds'), list), 'Missing outbounds') all_tags = [o.get('tag') for o in config['outbounds'] if isinstance(o, dict)] require(len(all_tags) == len(config['outbounds']) and all(isinstance(t, str) and t for t in all_tags), 'Each outbound needs a tag') require(len(set(all_tags)) == len(all_tags), 'Duplicate outbound tags') nodes = {o['tag']: o for o in config['outbounds'] if o.get('type') == 'anytls'} require(isinstance(expectations, list) and expectations, 'Nonempty expectations required') selected = [] for spec in expectations: require(isinstance(spec, dict), 'Invalid expectation') for key in ['tag', 'host', 'sni', 'uuid', 'expected_exit', 'expected_country']: require(isinstance(spec.get(key), str) and bool(spec[key]), 'Expectation missing required field') require(isinstance(spec.get('port'), int) and not isinstance(spec['port'], bool) and 1 <= spec['port'] <= 65535, 'Invalid relay port') try: ipaddress.ip_address(spec['expected_exit']) except ValueError as exc: raise VerificationError('Invalid expected exit IP') from exc require(len(spec['expected_country']) == 2 and spec['expected_country'].isalpha() and spec['expected_country'].isupper(), 'Expected country must be two uppercase letters') tag = spec['tag'] require(tag in nodes and tag not in selected, 'Missing/duplicate selected node') node = nodes[tag] tls = node.get('tls') or {} require(tls.get('enabled') is True and tls.get('insecure') is False, 'Strict TLS must be explicit') require(node.get('server') == spec['host'] and node.get('server_port') == spec['port'], 'Relay endpoint mismatch') require(tls.get('server_name') == spec['sni'], 'SNI mismatch') require(node.get('password') == spec['uuid'], 'Subscription password mismatch') selected.append(tag) if exact: require(set(selected) == set(nodes), 'Expectations must cover every AnyTLS node exactly') return selected def probe_config(config, spec, port): result = copy.deepcopy(config) result['inbounds'] = [{'type': 'mixed', 'listen': '127.0.0.1', 'listen_port': port}] result['log'] = {'level': 'error'} # Preserve DNS, remote rulesets and all original outbounds. Only the two # diagnostic destinations are pinned; ordinary subscription routing remains. route = result.setdefault('route', {}) rules = route.setdefault('rules', []) require(isinstance(rules, list), 'Invalid route rules') rules.insert(0, {'domain': ['api.ipify.org', 'www.cloudflare.com'], 'action': 'route', 'outbound': spec['tag']}) return result def stop(proc): if proc is None or proc.poll() is not None: return proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=5) def run_node(config, spec, binary, port, diagnostic_dir): require(1024 <= port <= 65535, 'Invalid local SOCKS port') with socket.socket() as free: try: free.bind(('127.0.0.1', port)) except OSError as exc: raise VerificationError('Local SOCKS port is occupied') from exc with tempfile.TemporaryDirectory(prefix='dpl-client-', dir=diagnostic_dir) as tmp: temporary = Path(tmp) path = temporary / 'client.json' path.write_text(json.dumps(probe_config(config, spec, port))) path.chmod(0o600) env = dict(os.environ, GOMEMLIMIT='64MiB', GOGC='75') proc = None log_path = temporary / 'client.log' try: with log_path.open('wb') as log: subprocess.run([binary, 'check', '-c', str(path)], env=env, cwd=tmp, stdout=log, stderr=log, check=True, timeout=30) proc = subprocess.Popen([binary, 'run', '-c', str(path)], env=env, cwd=tmp, stdout=log, stderr=log) ready = False for _ in range(100): require(proc.poll() is None, 'Client exited during startup') try: with socket.create_connection(('127.0.0.1', port), 0.1): ready = True break except OSError: time.sleep(0.1) require(ready, 'Client startup timed out') command = ['curl', '-q', '--fail', '--silent', '--show-error', '--max-time', '30', '--socks5-hostname', f'127.0.0.1:{port}', '--noproxy', ''] ip = subprocess.run(command + ['https://api.ipify.org'], capture_output=True, text=True, check=True, timeout=35).stdout.strip() require(ipaddress.ip_address(ip) == ipaddress.ip_address(spec['expected_exit']), 'Unexpected exit IP') response = subprocess.run(command + ['https://www.cloudflare.com/cdn-cgi/trace'], capture_output=True, text=True, check=True, timeout=35) trace = dict(line.split('=', 1) for line in response.stdout.splitlines() if '=' in line) require(trace.get('ip') == ip, 'Second exit check mismatch') require(trace.get('loc') == spec['expected_country'], 'Exit country mismatch') require(trace.get('tls', '').startswith('TLSv'), 'Target HTTPS TLS not observed') return {'tag': spec['tag'], 'strict_tls': True, 'exit_ip': ip, 'country': trace['loc'], 'https_tls': trace['tls'], 'live': True, 'relay': spec['host'], 'relay_port': spec['port'], 'sni': spec['sni']} except Exception: # Logs may contain private config context; retain only in private task directory. stop(proc) if log_path.exists(): fd, name = tempfile.mkstemp(prefix='client-failure-', suffix='.log', dir=diagnostic_dir) os.close(fd) failure = Path(name) shutil.copyfile(log_path, failure) failure.chmod(0o600) raise finally: stop(proc) def write_report(path, report): destination = Path(path) require(destination.parent.is_dir() and destination.parent.stat().st_mode & 0o077 == 0, 'Report directory must be private') require(not destination.is_symlink(), 'Refusing report symlink') with tempfile.NamedTemporaryFile(mode='w', dir=destination.parent, delete=False) as stream: temporary = Path(stream.name) try: os.chmod(temporary, 0o600) json.dump(report, stream, ensure_ascii=False) stream.flush() os.fsync(stream.fileno()) os.replace(temporary, destination) finally: if temporary.exists(): temporary.unlink() def main(): parser = argparse.ArgumentParser(description=__doc__) sources = parser.add_mutually_exclusive_group(required=True) sources.add_argument('--qa') sources.add_argument('--expectations') parser.add_argument('--subscription', required=True) parser.add_argument('--binary') parser.add_argument('--expected-exit') parser.add_argument('--expected-country') parser.add_argument('--target-tag') parser.add_argument('--report', help='Private JSON result file; required for --run') parser.add_argument('--port', type=int, default=19080) execution = parser.add_mutually_exclusive_group() execution.add_argument('--run', action='store_true') execution.add_argument('--validate-only', action='store_true') args = parser.parse_args() os.umask(0o077) try: require(not args.run or args.report, '--run requires a private --report destination') if args.report: destination = Path(args.report) inputs = [args.subscription, args.qa, args.expectations] require(all(destination.resolve() != Path(item).resolve() for item in inputs if item), 'Report must not overwrite a private input') require(destination.parent.is_dir() and destination.parent.stat().st_mode & 0o077 == 0 and not destination.is_symlink(), 'Private regular report destination required') if args.report: write_report(args.report, {'state': 'verification_pending', 'verified_at': None}) config = read_private(args.subscription) qa_user_id = None source = args.expectations or args.qa if args.expectations: require(not any([args.expected_exit, args.expected_country, args.target_tag]), 'Put each node expectation in the private expectations file') specs = read_private(args.expectations) else: qa = read_private(args.qa) qa_user_id = qa.get('user_id') require(args.expected_exit and args.expected_country, 'QA requires explicit expected-exit and expected-country') candidates = [n for n in config.get('outbounds', []) if n.get('type') == 'anytls' and n.get('server') == qa.get('host') and n.get('server_port') == qa.get('port') and (not args.target_tag or n.get('tag') == args.target_tag)] require(len(candidates) == 1, 'QA target ambiguous/missing; supply exact target-tag') specs = [{'tag': candidates[0]['tag'], 'host': qa.get('host'), 'port': qa.get('port'), 'sni': qa.get('sni'), 'uuid': qa.get('uuid'), 'expected_exit': args.expected_exit, 'expected_country': args.expected_country}] tags = validate(config, specs, exact=bool(args.expectations)) results = [] if args.run: require(args.binary and Path(args.binary).is_file(), 'Explicit existing client binary required') binary = str(Path(args.binary).resolve()) folder = Path(source).resolve().parent require(folder.stat().st_mode & 0o077 == 0, 'Live diagnostics directory must be private') for spec in specs: results.append(run_node(config, spec, binary, args.port, str(folder))) report = {'state': 'live_verified' if args.run else 'structure_validated', 'verified_at': int(time.time()) if args.run else None, 'qa_user_id': qa_user_id, 'selected_nodes': tags, 'results': results, 'scope': 'Full subscription retained; only diagnostic destination routes pinned. No traffic/account/GUI acceptance implied.'} if args.report: write_report(args.report, report) print(json.dumps(report, ensure_ascii=False)) return 0 except (Exception, KeyboardInterrupt) as exc: # No traceback or subprocess stderr: either can contain credentials. message = str(exc) if isinstance(exc, VerificationError) else 'Verification failed; inspect protected diagnostics' print(json.dumps({'state': 'failed', 'reason': message}), file=__import__('sys').stderr) return 1 if __name__ == '__main__': raise SystemExit(main())