"""proxy_dns 降级路径实测(模拟云通道挂掉)。 验证两条降级链: 1. 通道失败 -> 直连 fallback_dns 解析成功(全屋 DNS 不断) 2. 通道失败 + 直连也失败 -> 回 SERVFAIL(客户端立即切备用 DNS) 用法: python3 scripts/test_dns_fallback.py """ from __future__ import annotations import socket import sys import threading import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from channel.client import ChannelClient, ChannelError # noqa: E402 from channel.proxy_dns import DnsSplitProxy # noqa: E402 QUERY = bytes.fromhex( "abcd01000001000000000000037777770674616f62616f03636f6d0000010001" ) class DownClient(ChannelClient): """模拟云通道不可用(所有请求抛 ChannelError)。""" def __init__(self) -> None: super().__init__(host="127.0.0.1", port=1, token="test-stub") def send_dns_query(self, raw_query: bytes) -> bytes | None: raise ChannelError("simulated channel down") def ensure_connected(self) -> None: raise ChannelError("simulated channel down") def ask(port: int, timeout: float = 6.0) -> bytes: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(timeout) try: s.sendto(QUERY, ("127.0.0.1", port)) resp, _ = s.recvfrom(4096) return resp finally: s.close() def rcode(resp: bytes) -> int: return resp[3] & 0x0F if len(resp) >= 12 else -1 def case_fallback_ok() -> bool: """通道挂 + 直连上游可用 -> 应返回正常解析(RCODE=0)。""" proxy = DnsSplitProxy( DownClient(), listen_port=18053, school_dns="223.5.5.5", internal_suffixes=[], fallback_dns="223.5.5.5", ) threading.Thread(target=proxy.serve, daemon=True).start() time.sleep(0.3) resp = ask(18053) ok = len(resp) > 12 and rcode(resp) == 0 and proxy.fallback_resolved >= 1 print( f" 用例1 通道挂->直连降级: 响应 {len(resp)}B RCODE={rcode(resp)} " f"fallback_resolved={proxy.fallback_resolved} -> " f"{'PASS' if ok else 'FAIL'}" ) return ok def case_servfail() -> bool: """通道挂 + 直连上游也不可达 -> 应回 SERVFAIL(RCODE=2), 不静默丢弃。""" proxy = DnsSplitProxy( DownClient(), listen_port=18054, school_dns="192.0.2.1", internal_suffixes=[], fallback_dns="192.0.2.1", ) threading.Thread(target=proxy.serve, daemon=True).start() time.sleep(0.3) resp = ask(18054) ok = len(resp) >= 12 and rcode(resp) == 2 print( f" 用例2 通道+直连全挂->SERVFAIL: 响应 {len(resp)}B " f"RCODE={rcode(resp)} -> {'PASS' if ok else 'FAIL'}" ) return ok def main() -> int: print("[DNS 降级测试] 模拟云通道不可用") r1 = case_fallback_ok() r2 = case_servfail() print(f"结果: {'全部 PASS' if r1 and r2 else '有 FAIL'}") return 0 if (r1 and r2) else 1 if __name__ == "__main__": raise SystemExit(main())