efa634b574
真凶(两次"阿里云黑洞"实为系统卡死): 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 全链路验证: 降级 / 判据 / 故障演练(云挂->自动撤 规则、云回->自动重挂) 全过。
100 lines
3.0 KiB
Python
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())
|