# -*- coding: utf-8 -*- """防火墙检测与修复脚本测试。 背景(真机踩到的坑): Windows 防火墙的「阻止」规则**优先级高于「放行」规则**。 当系统弹「是否允许此应用通过防火墙」而用户点了取消时, 系统会自动建一条 Block 规则,之后加多少 Allow 都不管用。 而且规则是按**程序完整路径**建的,换个目录解压就多一条。 又因为防火墙不过滤回环(127.0.0.1),本地自测全绿、真机永远超时。 这里验证: 1. netsh 输出解析(中英文两种语言都要认) 2. 阻止规则识别 3. 修复脚本内容正确(先删阻止、再建放行) 不需要管理员权限:不实际执行提权,只验证解析与脚本生成。 """ from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "windows")) from lanremote import firewall # noqa: E402 PASSED = 0 FAILED = 0 def check(name: str, ok: bool, detail: str = "") -> None: global PASSED, FAILED if ok: PASSED += 1 print(f" [PASS] {name}") else: FAILED += 1 print(f" [FAIL] {name} {detail}") # 中文系统的 netsh 输出样例(字段名是中文的) ZH_RULE = """规则名称: ykxmz.exe ---------------------------------------------------------------------- 已启用: 是 方向: 入 配置文件: 公用 分组: 本地 IP: 任何 远程 IP: 任何 协议: TCP 本地端口: 任何 远程端口: 任何 边缘遍历: 否 程序: C:\\Users\\shu\\Desktop\\LanRemote\\dist_exe\\ykxmz\\ykxmz.exe 操作: 阻止 """ EN_RULE = """Rule Name: ykxmz.exe ---------------------------------------------------------------------- Enabled: Yes Direction: In Profiles: Public Grouping: LocalIP: Any RemoteIP: Any Protocol: UDP LocalPort: Any RemotePort: Any Edge traversal: No Program: C:\\test\\ykxmz.exe Action: Block """ ALLOW_RULE = """规则名称: LAN Remote TCP ---------------------------------------------------------------------- 已启用: 是 方向: 入 配置文件: 域,专用,公用 协议: TCP 本地端口: 8765 程序: 操作: 允许 """ def main() -> int: print("=" * 68) print("一、netsh 输出解析(中英文都要认)") print("=" * 68) zh = firewall._parse_fields(ZH_RULE) check("中文输出: 规则名解析正确", zh.get("RuleName") == "ykxmz.exe", str(zh.get("RuleName"))) check("中文输出: 操作解析为「阻止」", zh.get("Action") == "阻止", str(zh.get("Action"))) check("中文输出: 协议解析正确", zh.get("Protocol") == "TCP", str(zh.get("Protocol"))) check("中文输出: 程序路径解析正确", (zh.get("Program") or "").lower().endswith("ykxmz.exe"), str(zh.get("Program"))) en = firewall._parse_fields(EN_RULE) check("英文输出: 规则名解析正确", en.get("RuleName") == "ykxmz.exe", str(en.get("RuleName"))) check("英文输出: 操作解析为 Block", en.get("Action") == "Block", str(en.get("Action"))) check("英文输出: 协议解析正确", en.get("Protocol") == "UDP", str(en.get("Protocol"))) allow = firewall._parse_fields(ALLOW_RULE) check("放行规则不会被误判为阻止", (allow.get("Action") or "").lower() not in firewall._BLOCK_VALUES, str(allow.get("Action"))) print() print("=" * 68) print("二、规则切分与阻止规则识别(模拟完整 netsh 输出)") print("=" * 68) combined = ZH_RULE + "\n" + ALLOW_RULE + "\n" + EN_RULE rules = firewall._split_rules(combined) check("切分出 3 条规则", len(rules) == 3, f"得到 {len(rules)}") blocks = [r for r in rules if (r.get("Action") or "").lower() in firewall._BLOCK_VALUES] check("其中 2 条是阻止规则", len(blocks) == 2, f"得到 {len(blocks)}") names = {firewall.os.path.basename((r.get("Program") or "").strip('"')).lower() for r in blocks} check("阻止规则都指向本程序", names == {"ykxmz.exe"}, str(names)) print() print("=" * 68) print("三、修复脚本内容(先删阻止,再建放行)") print("=" * 68) script = firewall.build_script(8765, 8766) check("脚本删除了 ykxmz.exe 的规则", 'delete rule name="ykxmz.exe" dir=in' in script) check("脚本删除了 python.exe 的规则(源码模式遗留)", 'delete rule name="python.exe" dir=in' in script) check("脚本重建 TCP 放行规则", 'add rule name="LAN Remote TCP"' in script and "protocol=TCP localport=8765" in script) check("脚本重建 UDP 放行规则", 'add rule name="LAN Remote UDP"' in script and "protocol=UDP localport=8766" in script) # 端口放行 2 条 + 程序级放行 2 条 = 4 条,全部覆盖域/专用/公用 check("脚本使用 profile=any(公用/专用/域都放行)", script.count("profile=any") == 4, f"出现 {script.count('profile=any')} 次") # 源码模式下不能把打包版的程序级规则冲掉(真机事故) target = firewall.target_executable() check("修复目标是一个真实的可执行文件", bool(target) and target.lower().endswith(".exe"), str(target)) if not getattr(sys, "frozen", False): existing = firewall.app_rule_program() # 注意还要看那个路径**现在还在不在** —— # 规则指向一个已经删掉的旧副本时,target_executable 会(正确地) # 退回 python.exe。测试条件漏了这一点就会假失败(改名后就踩了)。 if (existing and "python" not in existing.lower() and __import__("os").path.isfile(existing)): check("源码模式下沿用已有的打包版目标,不写 python.exe", "python" not in target.lower(), f"target={target}") elif existing: print(f" (已有规则指向 {existing},但那个文件不在了," f"所以退回 python.exe —— 这是对的)") print(f" 修复将放行: {target}") check("删除动作在添加动作之前", script.index("delete rule") < script.index("add rule")) # 真机上踩到的坑:裸 LF 换行会让 cmd 把整个脚本解析成一堆无效命令 lone_lf = script.count("\n") - script.count("\r\n") check("脚本全部使用 CRLF 换行(裸 LF 会让 cmd 解析错乱)", lone_lf == 0, f"发现 {lone_lf} 处裸 LF") check("脚本以 chcp 936 开头(配合 GBK 存盘)", script.splitlines()[1].strip() == "chcp 936 >nul", script.splitlines()[1].strip() if len(script.splitlines()) > 1 else "?") check("脚本编码声明可用 GBK 编码", isinstance(script.encode("gbk", errors="replace"), bytes)) # 只删阻止规则治标不治本:必须补程序级放行,否则系统会再生成阻止规则 check("脚本添加了程序级放行规则(治本)", "LAN Remote App TCP" in script and "LAN Remote App UDP" in script) check("程序级放行规则指向真实可执行文件", 'program="' in script and ".exe" in script.lower(), "未找到 program= 指向") check("程序级规则 enable=yes 且 profile=any", script.count("enable=yes protocol=") == 2 and script.count("profile=any") >= 4, f"enable=yes 出现 {script.count('enable=yes protocol=')} 次") print() print("=" * 68) print("四、真实状态查询(本机)") print("=" * 68) status = firewall.check(8765, 8766) for key in ("supported", "ok", "tcp", "udp", "blocked", "blocked_count", "elevated", "message", "tcp_port", "udp_port"): check(f"状态包含字段 {key}", key in status) check("字段类型正确", isinstance(status["blocked"], list) and isinstance(status["blocked_count"], int) and isinstance(status["ok"], bool)) check("状态包含 app_rule(程序级放行)字段", "app_rule" in status) check("状态包含 blocked_others(其他副本的阻止规则数)", "blocked_others" in status) # 阻止规则按程序路径生效:别的副本被阻止不该影响本安装的判定 print() print(" —— 作用域检查:只统计当前程序 ——") live = r"C:\Users\shu\Desktop\deepseekagent\ykxmz\ykxmz.exe" original = firewall.executable_path try: firewall.executable_path = lambda: live scoped = firewall.check(8765, 8766) print(f" 以「实际安装」视角: 本程序阻止规则={scoped['blocked_count']} 条, " f"其他副本={scoped['blocked_others']} 条, ok={scoped['ok']}") check("其他副本的阻止规则不影响本安装判定", scoped["ok"] is True or scoped["blocked_count"] > 0, f"blocked={scoped['blocked_count']} others={scoped['blocked_others']}") check("blocked 只包含当前程序的规则", all((b.get("Program") or "").lower() == live.lower() for b in scoped["blocked"]), str([b.get("Program") for b in scoped["blocked"]])) check("blocked_count + blocked_others 等于全部阻止规则数", scoped["blocked_count"] + scoped["blocked_others"] == len(firewall.list_block_rules()), f"{scoped['blocked_count']}+{scoped['blocked_others']} " f"vs {len(firewall.list_block_rules())}") finally: firewall.executable_path = original check("有阻止规则时 ok 必为 False", status["blocked_count"] == 0 or status["ok"] is False) print(f" 本机结果: ok={status['ok']} 阻止规则={status['blocked_count']} 条") print(f" 说明: {status['message']}") for item in status["blocked"][:3]: print(f" - {item.get('ProgramBase')} {item.get('Protocol')} " f"{(item.get('Program') or '')[-48:]}") if status["blocked_count"] > 0: check("有阻止规则时 ok 必须为 False(阻止压过放行)", status["ok"] is False) check("阻止规则条数与列表长度一致", status["blocked_count"] == len(status["blocked"])) print() print("=" * 68) print(f"通过 {PASSED} 项,失败 {FAILED} 项") print("=" * 68) return 1 if FAILED else 0 if __name__ == "__main__": sys.exit(main())