Files

94 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Opt-in, bounded outbound checks; do not infer reputation or universal reachability."""
import ipaddress
import json
import socket
import subprocess
import sys
import time
TARGETS = (("Cloudflare", "1.1.1.1", 443), ("Google", "8.8.8.8", 443))
DNS_NAME = "example.com"
TIMEOUT = 5
def dns_check():
# A subprocess permits a hard timeout even if the system resolver blocks.
program = ("import json,socket; "
"a=socket.getaddrinfo('example.com',443,type=socket.SOCK_STREAM); "
"print(json.dumps(sorted(set(x[0] for x in a))))")
start = time.monotonic()
try:
proc = subprocess.run([sys.executable, '-I', '-B', '-c', program], capture_output=True,
text=True, timeout=TIMEOUT, check=False)
if proc.returncode != 0:
return {'status':'unknown', 'reason':'lookup_failed'}
families = json.loads(proc.stdout)
if not isinstance(families, list) or not families:
return {'status':'unknown', 'reason':'no_answers'}
return {'status':'pass', 'elapsed_ms':round((time.monotonic()-start)*1000),
'ipv4':socket.AF_INET in families, 'ipv6':socket.AF_INET6 in families}
except subprocess.TimeoutExpired:
return {'status':'unknown', 'reason':'timeout'}
except (OSError, ValueError, TypeError):
return {'status':'unknown', 'reason':'lookup_failed'}
def tcp_check(label, address, port):
# Validate a numeric address so this check never triggers DNS.
ipaddress.ip_address(address)
start = time.monotonic()
try:
with socket.create_connection((address, port), timeout=TIMEOUT):
elapsed = round((time.monotonic()-start)*1000)
return {'target':label, 'port':port, 'status':'pass', 'elapsed_ms':elapsed}
except (OSError, TimeoutError):
return {'target':label, 'port':port, 'status':'unknown'}
def run_quality(t, emit=print, compact=False, lang='zh', color='auto', report=None, on_result=None):
"""Run explicit checks, flushing each result as soon as it is available."""
if compact:
from .console import Console
console=Console(lang,color)
dns=dns_check()
if report is not None: report.update(dns=dns,tcp=[])
if on_result is not None: on_result()
if dns['status']=='pass':
versions='/'.join(v for v,ok in [('IPv4',dns['ipv4']),('IPv6',dns['ipv6'])] if ok)
console.row('DNS',f"{versions} / {dns['elapsed_ms']} ms")
else: console.row('DNS',console.text('未成功','Unsuccessful'),'bad')
complete=dns['status']=='pass'
for label,address,port in TARGETS:
result=tcp_check(label,address,port)
if report is not None: report['tcp'].append(result)
if on_result is not None: on_result()
ok=result['status']=='pass'
console.row(f'{label} TCP/{port}',f"{result['elapsed_ms']} ms" if ok else console.text('未连通','Unreachable'),'ok' if ok else 'bad')
complete=complete and ok
return 0 if complete else 1
def show(message):
emit(message, flush=True)
show(t('网络质量检测:会向系统 DNS、Cloudflare 和 Google 发起请求。'))
show(t('正在查询 DNS:example.com ...'))
dns = dns_check()
if report is not None: report.update(dns=dns,tcp=[])
if on_result is not None: on_result()
complete = dns['status'] == 'pass'
if dns['status'] == 'pass':
versions = '/'.join(v for v, ok in [('IPv4', dns['ipv4']), ('IPv6', dns['ipv6'])] if ok) or t('其他')
show(t('DNS 查询成功:{versions},耗时 {ms} ms', versions=versions, ms=dns['elapsed_ms']))
else:
show(t('DNS 查询未成功;原因可能是超时、网络或解析器配置,不能确定唯一原因。'))
for label, address, port in TARGETS:
show(t('正在测试 {target} TCP {port} ...', target=label, port=port))
result = tcp_check(label, address, port)
if report is not None: report['tcp'].append(result)
if on_result is not None: on_result()
complete = complete and result['status'] == 'pass'
if result['status'] == 'pass':
show(t('{target} TCP 连接成功:{ms} ms', target=label, ms=result['elapsed_ms']))
else:
show(t('{target} TCP 连接未成功;原因尚未确定。', target=label))
show(t('以上结果为本次从此服务器到这些测试目标的连接情况。'))
return 0 if complete else 1