#!/usr/bin/env python3 """Read-only VM diagnostics. Never prints subscription URLs or node credentials.""" import argparse import json import os from pathlib import Path import sqlite3 import subprocess import sys from urllib.parse import urlsplit parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--host', required=True, help='Hostname of a saved subscription') parser.add_argument('--data-dir', default='/var/lib/clashweb') parser.add_argument('--proxy', help='Override the saved VM Mihomo HTTP proxy address') args = parser.parse_args() if os.geteuid() != 0: raise SystemExit('Run with sudo to read the protected VM database; all network tests run as user clashweb.') database = (Path(args.data_dir) / 'clashweb.sqlite').resolve() with sqlite3.connect(database.as_uri() + '?mode=ro', uri=True) as db: row = db.execute("SELECT value FROM kv WHERE key='state'").fetchone() if not row: raise SystemExit('No saved state found.') state = json.loads(row[0]) settings = state.get('settings', {}) proxy = args.proxy or f"http://{settings.get('lanIP') or '127.0.0.1'}:{settings.get('mixedPort', 7890)}" subscriptions = [s for s in state.get('subscriptions', []) if urlsplit(s.get('url', '')).hostname == args.host] if not subscriptions: raise SystemExit('No saved subscription matches this hostname.') code = r''' import hashlib,json,os,pathlib,socket,ssl,time,urllib.error,urllib.request from urllib.parse import urlsplit payload=json.loads(__import__('sys').stdin.read()) url=payload['url'];host=urlsplit(url).hostname class SafeRedirect(urllib.request.HTTPRedirectHandler): max_redirections=5 def redirect_request(self,request,fp,code,msg,headers,newurl): target=urlsplit(newurl) if target.scheme not in ('http','https') or target.username is not None: raise urllib.error.URLError('Unsupported redirect') return super().redirect_request(request,fp,code,msg,headers,newurl) verify=ssl.get_default_verify_paths() result={'host':host,'runAsUID':os.getuid(),'caBundleExists':bool(verify.cafile and pathlib.Path(verify.cafile).exists()),'utcTime':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'tests':[]} try: addresses=socket.getaddrinfo(host,443,type=socket.SOCK_STREAM) result['systemDNSAddresses']=sorted({a[4][0] for a in addresses}) except Exception as error:result['systemDNSError']=type(error).__name__ for mode,proxy in [('direct',{}),('vm-proxy',{'http':payload['proxy'],'https':payload['proxy']})]: started=time.monotonic();entry={'mode':mode} try: opener=urllib.request.build_opener(urllib.request.ProxyHandler(proxy),SafeRedirect()) request=urllib.request.Request(url,headers={'User-Agent':'clash.meta/ClashWeb'}) try:response=opener.open(request,timeout=20) except urllib.error.HTTPError as error:response=error with response: body=response.read(8*1024*1024+1) entry.update({'httpStatus':response.status,'contentType':response.headers.get('Content-Type'),'bytes':len(body),'sha256':hashlib.sha256(body).hexdigest(),'hasClashProxies':b'proxies:' in body,'cfChallenge':response.headers.get('cf-mitigated')=='challenge'}) except Exception as error: reason=getattr(error,'reason',error) entry['errorType']=type(reason).__name__ if isinstance(reason,ssl.SSLCertVerificationError): entry['certificateErrorCode']=reason.verify_code;entry['detail']=reason.verify_message elif isinstance(reason,socket.gaierror):entry['detail']='System DNS lookup failed' elif isinstance(reason,(socket.timeout,TimeoutError)):entry['detail']='Connection or read timed out' else: entry['detail']='Network connection failed' if getattr(reason,'errno',None) is not None:entry['errno']=reason.errno entry['seconds']=round(time.monotonic()-started,2);result['tests'].append(entry) print(json.dumps(result,ensure_ascii=False,indent=2)) ''' for subscription in subscriptions: try: result = subprocess.run( ['/usr/bin/sudo', '-n', '-u', 'clashweb', 'python3', '-c', code], input=json.dumps({'url': subscription['url'], 'proxy': proxy}), text=True, capture_output=True, timeout=60, ) except subprocess.TimeoutExpired: raise SystemExit('Diagnostics timed out; inspect VM system DNS and connectivity.') if result.returncode: print('Diagnostics subprocess failed; no stored URL or subscription body has been printed.', file=sys.stderr) raise SystemExit(result.returncode) print(result.stdout.strip())