Files
vpn-loopback-firewall/scripts/test_dns_fallback.py
T
lou efa634b574 fix(套壳): 云服 OOM 根因修复 + DNS 降级 + watchdog 判据升级
真凶(两次"阿里云黑洞"实为系统卡死): server 每连接新建 SSLContext
(每次重读证书, 内存+盘 IO 双爆点) + ThreadPoolExecutor 无界队列
(4MB 帧在内存堆积) -> 1.6G 云服 OOM -> swap 抖动 -> 系统盘
IOPS/BPS 打满 -> 整机假死(ping 丢+应用全断, 形似黑洞)。

- server: SSLContext 启动建一次复用; TLS 握手移入连接线程(不再
  阻塞 accept 主循环); 在飞请求/并发连接双上限(64); 响应上限
  8MB; 内存自愈(RSS 达阈值主动退出, 交由 systemd 重启释放)
- proxy_dns: 通道不可用 -> 直连 fallback_dns 降级(全屋 DNS 不断),
  再失败回 SERVFAIL(客户端立即切备用 DNS, 不再干等超时); 重连改
  后台限频, 不再阻塞查询路径
- client: DNS 请求超时 15s -> 3.5s(让降级快速触发)
- watchdog: 判据由端口/进程探测改为直连云端完成 TLS+AUTH(端口在听、
  本地 DNS 探针都会给假健康信号 - 降级路径照样能答); 通道不健康即
  撤 L2 劫持规则, DNS 回落 smartdns 直连; HTTP 规则排除内网段
- 配置外置: fallback_dns + 服务端防护参数
- 新增 scripts/test_dns_fallback.py(降级链单测, 2/2 PASS)

QEMU OpenWrt lab 全链路验证: 降级 / 判据 / 故障演练(云挂->自动撤
规则、云回->自动重挂) 全过。
2026-09-10 15:56:02 +08:00

100 lines
3.0 KiB
Python

"""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())