ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

VC调用INetFwPolicy2添加Windows防火墙入站规则

VC调用INetFwPolicy2添加Windows防火墙入站规则

给自家服务端程序加一条入站放行规则,这件事听起来一分钟就能干完,但只要真到客户现场走一遭你就知道有多麻烦:打开高级安全防火墙的图形界面,点新建规则,选端口还是选程序,选 TCP 还是 UDP,勾专用还是公用,填完还要确认没把别的规则覆盖掉。客户那边的运维不一定愿意点,点了也可能点错。这就是“Windows 防火墙调用”这个话题的来源——与其教人点鼠标,不如在自己的 VC 程序里把这件事做完。在 VC(后文统一指 Win32 C++)里操作 Windows Defender 防火墙,本质上不是去启动某个命令行工具,而是通过 COM 接口INetFwPolicy2这一组对象,直接向系统的防火墙策略存储读写规则。写进去的东西和你在图形界面里手点出来的完全等价,重启之后依然在,netsh advfirewall firewall show rule也能查到。这篇东西适合三类人看:做安装包、需要在部署阶段静默放行端口的;做工业软件、客户端要和本机服务通信却被防火墙拦住的;还有单纯想搞明白这套 COM 接口怎么用、别再从网上抄一段编译不过的代码的。下面全部是我自己项目里跑过的写法和踩过的坑,代码可以直接拿去改。

1. 先搞清楚“防火墙调用”到底在调什么

1.1 三条技术路线的取舍

在 Windows 上管理防火墙规则,能走的路其实就三条:COM 接口、netsh advfirewall命令行、以及 PowerShell 的New-NetFirewallRule。这三条路最终操作的是同一份数据,区别只在于你从哪个门进去、中间隔了几层。

COM 这条路的主角是一组接口:INetFwPolicy2、INetFwRules、INetFwRule,从 Windows Vista 开始提供,Windows 7 之后基本是标配。你在进程内CoCreateInstance拿到INetFwPolicy2,再从它身上取Rules集合,往集合里Add一条规则对象,整个过程是纯进程内调用,没有控制台窗口闪一下,没有字符串拼接,出问题返回的是 HRESULT,可以精确定位到是哪一步失败了。

netsh这条路的好处是不用写 COM 代码,一行命令搞定,缺点是它是个独立进程,启动开销在几百毫秒量级,而且输出文本是本地化的——中文系统上打印的是中文,英文系统上是英文,你想解析输出判断成功与否,就得面对两套甚至更多套文案。稳妥的做法是只看进程退出码,不看输出。

PowerShell 那条路适合运维批量下发,New-NetFirewallRule语法清晰、可读性好,但它是 Windows 8 / Server 2012 之后才有的 cmdlet,而且每次调用都要拉起一个 PowerShell 宿主,在安装器里高频调用非常不划算。

路线典型耗时是否依赖外部进程返回值可判断性适用场景
COM(INetFwPolicy2)毫秒级否HRESULT,精确客户端程序内嵌、安装器、服务
netsh advfirewall数百毫秒是仅退出码脚本、批处理、兜底方案
PowerShell cmdlet500ms 以上是退出码/异常运维批量、远程下发
组策略下发随策略刷新否不适用域环境统一管控

这张表里我特意把“组策略下发”也列进去了,因为它是很多人排查问题时最容易忽略的一层:域环境里,管理员通过组策略下发的规则优先级高于本地规则,你在本机用 COM 写的规则照样存在、照样能查出来,但它不生效。这一点后面第 5 节会展开讲。

1.2 为什么 VC 项目里我几乎只用INetFwPolicy2

determine 一件事该不该用某个方案,我一般看三个维度:出错时能不能知道错在哪、部署时会不会多带一个依赖、以及调用频率高不高。COM 这套接口在这三点上都占优。

第一,错误信息足够细。Add返回0x80070005就是权限不够,返回0x80070057基本就是参数填错了,返回0x80070002说明按名字找不到规则。这些 HRESULT 直接对应到具体动作,不用去猜。而netsh失败时你只知道“退出码非零”,具体是名字冲突、参数拼错还是权限不足,得靠人肉看输出。

第二,不引入外部依赖。COM 接口的实现在系统自带的组件里,你的 exe 不需要额外带 dll,也不会因为客户机器上 PATH 环境变量被改坏而找不到netsh.exe。

第三,它真的是进程内调用。CLSCTX_INPROC_SERVER拿到的是加载进你自己进程的组件,一次调用就是一次函数调用,批量写十几条规则也就几十毫秒。安装器里经常要在几十秒内做完一堆事,这点开销差异会被放大。

注意:不要用网上流传的#import "netfw.tlb"那种写法。系统里并没有一个叫netfw.tlb的文件,那段代码多数是从别人的示例里抄来的,换台机器就编译不过。Windows SDK 自带netfw.h,接口、常量、coclass 全在里面,#include进来直接用就行。

2. 对象模型拆开看:五个接口撑起全部操作

2.1 从 Policy 到 Rule 的层次关系

这套接口的层次非常浅,一共就三层,理解了这三层,后面所有代码都是填空题。

最上层是INetFwPolicy2,它代表“本机的防火墙策略”,是整个操作的入口。你从它身上能拿到两样关键东西:一是Rules属性,返回INetFwRules集合;二是CurrentProfileTypes,告诉你当前网络位置被系统判定成了哪一类(域、专用、公用)。它还能查某个 profile 的防火墙开关状态,但这个能力一般用不上,别乱动,把用户防火墙关掉是要出事的。

中间层是INetFwRules,规则的容器,提供Add、Remove、Item、get_Count和get__NewEnum五个能力。注意Add的入参是一条完整的规则对象,不是一堆散参数;Remove只接受规则名字符串,没有别的删除方式;Item按名字取单条,取不到返回0x80070002;get__NewEnum是给你遍历用的,返回IEnumVARIANT。

最下层是INetFwRule,一条具体规则。它有一堆属性:名字、描述、方向、动作、协议、本地端口、远程端口、本地地址、远程地址、程序路径、服务名、生效的 profile、是否启用、接口类型、规则分组。这些属性全都是put_/get_形式,跟脚本里写rule.Name = "xxx"是一个意思。

再往下还有两个扩展版本:INetFwRule2在INetFwRule基础上加了EdgeTraversal(边缘遍历,控制是否允许来自 NAT 边界外的流量),INetFwRule3在 2 的基础上又加了LocalAppData、RemoteAppData等属性,用于限制规则只对某个用户生效,是 Windows 7 之后才有的。用QueryInterface往上转就行,转不动说明系统版本太老或者接口不存在,这时候别硬来,把扩展属性跳过。

还有一个INetFwMgr是 XP 时代的遗留接口,只支持单 profile 模型,现在完全没必要碰。你在网上搜到的老代码如果出现INetFwMgr,基本可以判定是十几年前的产物,别参考。

2.2 一条规则里哪些字段是“不填就白干”

这是新手最容易翻车的地方。INetFwRule的属性没有“合理默认值”这回事——你不设,它就是一个空值或者零值,而零值在这个接口里往往对应着“不生效”。所以我的习惯是:新建规则对象之后,除了描述这种纯展示字段,其余全部显式赋值。

属性类型必须显式设置不设的后果
NameBSTR是Add直接返回E_INVALIDARG
Direction枚举是默认可能落到 out,规则方向反了
Action枚举是BLOCK的枚举值是 0,容易变成“禁止”
Protocollong是默认值与预期不符,端口设置失效
LocalPortsBSTR端口类规则必填端口为空,规则匹配不到任何流量
ApplicationNameBSTR程序类规则必填路径为空,规则形同虚设
Profileslong 位掩码是为 0 时规则不属于任何 profile
EnabledVARIANT_BOOL是可能落到 false,规则存在但不生效
InterfaceTypesBSTR建议建议填All,避免只对某种接口生效
GroupingBSTR建议卸载时无法按组批量清理

Action那一行值得单独强调。NET_FW_ACTION_BLOCK的值是 0,NET_FW_ACTION_ALLOW的值是 1。如果你创建了规则对象却没设Action,取到的就是 0,等于你亲手写了一条“禁止”规则。我见过不止一个项目这么干过,结果是把自家程序的端口给封了,然后一群人围着服务器查网络问题查了一下午。

Profiles同理。NET_FW_PROFILE2_DOMAIN是 1,PRIVATE是 2,PUBLIC是 4,三个位或起来是全选,也可以用现成的NET_FW_PROFILE2_ALL(0x7FFFFFFF)。这个字段是位掩码,不是单值,很多人第一次写的时候只填了一个PRIVATE,结果客户笔记本换个网络环境(比如插了手机热点)就变成公用网络,规则立刻不生效,然后收到一个“你们的软件又连不上了”的工单。

提示:LocalPorts支持三种写法——单端口"8080"、端口段"8080-8090"、多端口逗号分隔"80,443,8080"。字符串里不要带空格,不要写中文逗号,端口边界不要超出 1–65535。

还有一条硬规则:非 TCP/UDP 协议(比如 ICMP)不要设LocalPorts。协议和端口是配套的,给 ICMP 设端口大概率返回E_INVALIDARG,因为这个字段对无连接协议没有意义。

2.3 Profile 与当前网络类型:规则“加了不生效”的头号元凶

Windows 的网络位置感知会把每块网卡对应的网络归类成域、专用、公用三种之一,防火墙规则是否生效,取决于规则的Profiles位掩码和当前激活的 profile 有没有交集。这里有两个容易踩的点。

第一个是“当前 profile 可能是多个”。有线连着公司内网(域),无线连着访客网络(公用),这时候get_CurrentProfileTypes返回的是位或的结果,两个位都亮着。如果你的规则只勾了专用,那在某些网段下就是不生效,而且现象非常迷惑——同一台机器,换个办公室就正常了。

第二个是“profile 会随网络变化而变”。笔记本从公司拿回家,网络从域变成专用;插上手机热点,从专用变成公用。这类场景下最稳的做法就是规则直接对全部 profile 生效,除非你有明确的理由要限制。判断逻辑上,你可以先调get_CurrentProfileTypes拿一个long,用&判断具体是哪几个 profile 在生效,把它打日志,出问题时一眼就能看出是不是 profile 对不上。

long profiles = 0; HRESULT hr = pPolicy->get_CurrentProfileTypes(&profiles); if (SUCCEEDED(hr)) { // bit0 域, bit1 专用, bit2 公用,可能同时亮 if (profiles & NET_FW_PROFILE2_DOMAIN) { /* 域网络生效 */ } if (profiles & NET_FW_PROFILE2_PRIVATE) { /* 专用网络生效 */ } if (profiles & NET_FW_PROFILE2_PUBLIC) { /* 公用网络生效 */ } }

这段代码我一般放在程序启动时的自检里,把结果写进日志文件。后来再遇到“规则明明加了却不生效”的反馈,第一件事就是让对方把这个日志发过来,十次里有六七次是 profile 的问题,比远程连过去一顿瞎猜快得多。

3. 从零写出可用的 VC 调用代码

3.1 工程配置:头文件、库、COM 初始化

先把地基打好。头文件顺序有讲究,netfw.h依赖windows.h里的基础定义,必须放在后面;comdef.h提供_bstr_t,能省掉一堆SysFreeString的手工调用。库只需要两个:ole32管CoCreateInstance和 COM 初始化,oleaut32管 BSTR 和 VARIANT。

#define _WIN32_DCOM // 老版本 SDK 兼容用,新 SDK 不加也行,加上没坏处 #include <windows.h> #include <objbase.h> #include <netfw.h> #include <comdef.h> #include <string> #include <cstdio> #pragma comment(lib, "ole32.lib") #pragma comment(lib, "oleaut32.lib")

如果你想用ComPtr来管引用计数(推荐),再加一行#include <wrl/client.h>,然后using Microsoft::WRL::ComPtr;。这个是 SDK 自带的,VS2012 以后都有,不用额外装东西。用了它之后代码里就不需要满屏的Release(),异常路径也不会漏。

COM 初始化这一步,客户端程序用单线程套间(STA)比较自然,和服务里用多线程套间(MTA)都行,因为防火墙接口本身不做线程亲和的要求。关键是初始化一次就够,不要每个函数里都CoInitialize一次然后忘了CoUninitialize,那样子孙线程会拿到RPC_E_CHANGED_MODE。我的做法是在main/WinMain开头初始化一次,结尾反初始化,中间所有函数都假设 COM 已经就绪。

int wmain() { HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) { wprintf(L"COM 初始化失败: 0x%08X\n", hr); return 1; } // ... 业务代码 CoUninitialize(); return 0; }

RPC_E_CHANGED_MODE这个特殊处理是必要的——如果宿主进程(比如某个已经初始化过 MTA 的容器)先初始化了,你这里再初始化 STA 就会拿到这个码,但它其实不是错误,COM 已经可用了,继续往下走就行。不判断这个,你的代码在某些宿主里会莫名其妙“初始化失败”。

3.2 最小可运行:添加一条入站 TCP 端口放行规则

下面这段是整个话题的核心,把它跑通,剩下的都是变体。逻辑顺序是:建 Policy、取 Rules、建 Rule、填属性、Add、清理。每一步都判 HRESULT,失败就把错误码带出去。

// 添加一条入站 TCP 放行规则 // ports 形如 "8080" / "8080-8090" / "80,443" HRESULT AddInboundTcpRule(const wchar_t* ruleName, const wchar_t* ports, const wchar_t* grouping /* 可为 nullptr */) { ComPtr<INetFwPolicy2> policy; ComPtr<INetFwRules> rules; ComPtr<INetFwRule> rule; HRESULT hr = S_OK; hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr, CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2), (void**)policy.GetAddressOf()); if (FAILED(hr)) return hr; hr = policy->get_Rules(rules.GetAddressOf()); if (FAILED(hr)) return hr; hr = CoCreateInstance(__uuidof(NetFwRule), nullptr, CLSCTX_INPROC_SERVER, __uuidof(INetFwRule), (void**)rule.GetAddressOf()); if (FAILED(hr)) return hr; _bstr_t bstrName(ruleName); _bstr_t bstrPorts(ports); rule->put_Name(bstrName); rule->put_Description(_bstr_t(L"由应用程序自动创建,请勿手工修改")); rule->put_Protocol(NET_FW_IP_PROTOCOL_TCP); rule->put_LocalPorts(bstrPorts); rule->put_Direction(NET_FW_RULE_DIR_IN); rule->put_Action(NET_FW_ACTION_ALLOW); rule->put_Profiles(NET_FW_PROFILE2_ALL); rule->put_InterfaceTypes(_bstr_t(L"All")); rule->put_Enabled(VARIANT_TRUE); if (grouping && *grouping) { rule->put_Grouping(_bstr_t(grouping)); } // 先删同名再添加,避免不同系统版本对同名覆盖的处理不一致 rules->Remove(bstrName); hr = rules->Add(rule.Get()); return hr; }

这里有几个刻意的设计。第一,Remove放在Add前面。理论上同名规则再Add会覆盖旧的,但我在不同版本的 Windows 上见过行为差异,有的机器报错,有的机器静默覆盖。先删再加是幂等的,跑多少次结果都一样,值得多花这一行代码。第二,Grouping一定要填,后面卸载清理全靠它。第三,Enabled显式设VARIANT_TRUE,不给默认值留任何机会。

端口那行还有一个细节:LocalPorts是 BSTR,如果传进去的是"8080 "(末尾带空格)或者用了中文逗号,Add会失败,错误码通常落在E_INVALIDARG上。我写过一个入口校验函数专门过滤这类输入,虽然不是必须,但能让错误在最早的环节暴露,而不是等到Add才报个看不懂的码。

3.3 查询、修改、删除:把规则当数据来管理

规则不是写完就不管了。安装器会升级,路径会变,产品卸载的时候得清理干净,所以查询和删除的能力比添加更重要。

按名字查一条规则很简单,Item拿指针,取属性就行:

bool RuleExists(INetFwRules* rules, const wchar_t* name) { ComPtr<INetFwRule> rule; HRESULT hr = rules->Item(_bstr_t(name), rule.GetAddressOf()); return SUCCEEDED(hr) && rule != nullptr; }

Item找不到时返回的是0x80070002(也就是ERROR_FILE_NOT_FOUND的 HRESULT 形式),不是S_FALSE,这一点要记住,不要拿SUCCEEDED之外的逻辑去判断。

如果要按组清理,或者要列出所有自己创建的规则,就得遍历集合。这里get__NewEnum返回的是IUnknown*,还得QueryInterface到IEnumVARIANT,然后逐个Next,每个元素是VT_DISPATCH的 VARIANT,再转成INetFwRule。流程有点绕,但绕一次就熟了:

void ListMyRules(INetFwRules* rules, const wchar_t* grouping) { ComPtr<IUnknown> pUnk; if (FAILED(rules->get__NewEnum(pUnk.GetAddressOf()))) return; ComPtr<IEnumVARIANT> pEnum; if (FAILED(pUnk.As(&pEnum))) return; VARIANT var; VariantInit(&var); ULONG fetched = 0; while (pEnum->Next(1, &var, &fetched) == S_OK && fetched == 1) { if (var.vt == VT_DISPATCH && var.pdispVal) { ComPtr<INetFwRule> rule; if (SUCCEEDED(var.pdispVal->QueryInterface(IID_PPV_ARGS(rule.GetAddressOf())))) { BSTR bstrGroup = nullptr; rule->get_Grouping(&bstrGroup); if (bstrGroup && wcscmp(bstrGroup, grouping) == 0) { BSTR bstrName = nullptr; rule->get_Name(&bstrName); wprintf(L"命中规则: %s\n", bstrName ? bstrName : L""); SysFreeString(bstrName); } SysFreeString(bstrGroup); } } VariantClear(&var); } }

删除就简单了,Remove接受名字。但批量删除的时候有个坑:不要在遍历过程中直接Remove,因为集合正在被修改,枚举器的行为不可预期,轻则漏删,重则返回错误。正确做法是先把命中的名字收集到std::vector<std::wstring>里,遍历结束后再逐个Remove。这个坑我在一个清理工具上踩过,当时的现象是“每次卸载总剩两三条规则”,排查了半天才发现是边遍历边删导致的。

修改规则的话,有两种做法。一种是从Item拿到指针后直接put_改属性,改动确实会落地,但个别属性(比如改名字)改了也不生效。另一种更稳:Remove掉旧的,用新参数重新Add一条。我一般选后者,逻辑统一,不用记哪个属性可以改哪个不可以改。

3.4 封装成可复用的CFirewallHelper

单个函数用着还行,工程一大就需要封装。我的惯例是做一个薄封装类,把 Policy 和 Rules 的指针缓存在对象里,对外只暴露几个业务语义的方法。这样调用方完全不用关心 COM 的细节,也不用反复创建 Policy 对象(虽然开销不大,但没必要)。

方法作用典型调用时机
Init()创建 Policy 并取 Rules程序启动或首次使用
AddPortRule(name, port, proto)加端口放行规则服务端监听端口启动前
AddAppRule(name, exePath)加程序放行规则客户端首次运行
RemoveRule(name)按名字删单条卸载、切换配置
RemoveGroup(group)按组批量删产品卸载
HasRule(name)判断存在性幂等部署前检查
CurrentProfiles()取当前 profile 位掩码自检、打日志

程序集里的ExePath一定要用GetModuleFileNameW现取,不要写死字符串。我见过把路径写成C:\Program Files\XXX\app.exe的代码,客户装在 D 盘就全废了。取到之后建议再过一遍PathCanonicalizeW,把..和多余的斜杠规整掉,因为规则里的ApplicationName是精确字符串匹配,路径写法不一致就匹配不上。

std::wstring GetSelfPath() { wchar_t buf[MAX_PATH * 2] = {0}; DWORD n = GetModuleFileNameW(nullptr, buf, _countof(buf)); if (n == 0 || n >= _countof(buf)) return L""; wchar_t canon[MAX_PATH * 2] = {0}; if (PathCanonicalizeW(canon, buf)) return canon; return buf; }

这段代码里MAX_PATH * 2是故意的。MAX_PATH是 260,但现在的安装路径经常超长,用两倍空间加上GetModuleFileNameW的截断检测,能覆盖绝大多数情况。真要彻底解决长路径问题得开\\?\前缀和清单里的longPathAware,那是另一个话题了,这里不展开。

3.5netsh兜底与静默执行

什么情况下还需要netsh?我总结了两类。一类是排查用——开发阶段想快速验证“规则到底存不存在”,敲一行命令比跑程序快。另一类是极端兼容场景——极老的系统、或者 COM 组件被安全软件拦了(这种情况确实存在,某些安全软件会 hook 防火墙组件的调用),这时候用命令行反而能过去。

netsh advfirewall firewall add rule name="MyApp-TCP-8080" dir=in action=allow protocol=TCP localport=8080 profile=any enable=yes netsh advfirewall firewall delete rule name="MyApp-TCP-8080" netsh advfirewall firewall show rule name="MyApp-TCP-8080"

用CreateProcessW拉起的时候记得加CREATE_NO_WINDOW,否则黑色窗口闪一下,用户会以为中了病毒。等进程结束后看退出码,0 是成功,非 0 就是失败。不要去解析输出文本,中文系统和英文系统打印的内容完全不同,这个坑比想象中普遍。

show rule那条命令有个好用的特性:规则不存在时退出码非 0,所以可以直接用它做存在性判断,比解析输出可靠得多。

bool RunAndWait(const std::wstring& cmdline) { STARTUPINFOW si = { sizeof(si) }; PROCESS_INFORMATION pi = {0}; std::wstring cmd = cmdline; // CreateProcessW 要求可写缓冲区 if (!CreateProcessW(nullptr, cmd.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { return false; } WaitForSingleObject(pi.hProcess, 15000); DWORD code = 1; GetExitCodeProcess(pi.hProcess, &code); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return code == 0; }

15 秒的超时是拍脑袋定的,实际netsh一般几百毫秒就返回了。加超时是为了防止在极端情况下(比如防火墙服务卡死)你的安装程序永久挂在那里,那用户体验比报错还差。

4. 权限、提权与安装部署场景

4.1 为什么你的Add返回0x80070005

这是所有问题里出现频率最高的一个。0x80070005就是E_ACCESSDENIED,直白点说:你这个进程没有权限改动防火墙策略。防火墙规则的写操作需要管理员权限,这是设计如此,不是 bug。

有意思的是,很多人在这一步会怀疑是自己的代码写错了。怎么区分?看是哪一步失败的——如果是CoCreateInstance就失败了,那可能是组件注册问题(极少见);如果是get_Rules失败,通常也是权限;如果是Add失败,基本可以确定是权限。快速验证方法:用管理员身份打开一个命令行,跑一下netsh advfirewall firewall add rule name="test" dir=in action=allow protocol=TCP localport=1,如果能成功,就说明命令本身没问题,纯粹是你的进程权限不够。

还有一个容易被忽略的场景:程序以普通用户启动,代码里靠ShellExecuteEx加runas提权。提权确实成功了,但提权的是那个子进程,父进程还是普通权限。如果你的规则添加逻辑跑在父进程里,照样E_ACCESSDENIED。这种事在调试时特别隐蔽,因为你看日志发现“明明提权了”。

4.2 三种提权落地方式对比

方式实现成本用户可见性适用场景主要缺点
主程序清单声明requireAdministrator极低每次启动都弹 UAC纯管理工具整个程序被迫高权限运行
单独 helper exe +runas中等仅操作时弹一次常规客户端需要拆一个独立进程
安装服务 / 安装器(LocalSystem)较高无感部署阶段一次性操作需要服务框架支撑

第一种最简单,加个 manifest 就完事,但代价是程序每次启动都弹 UAC,用户体验很差,而且高权限运行会放大其他模块的风险。我不推荐在普通业务程序上用这个。

第二种是我最常用的。把防火墙操作拆成一个几十 KB 的小 exe,主程序检测到规则不存在时,用ShellExecuteEx带runas把它拉起来,它干完活就退出。用户只看到一次 UAC,点完就完事了。这个小工具的命令行参数设计得简单点,比如fwhelper.exe --add-port 8080 --name xxx,退出码 0 表示成功,其他表示失败并带错误码。

第三种最干净。如果产品本来就有安装器或者后台服务,直接在 LocalSystem 上下文里调 COM 接口,一步到位、无弹窗、无 UAC。服务里调用 COM 记得用COINIT_MULTITHREADED,因为服务的主线程通常不是 STA,用 STA 会给自己找麻烦。

// 服务中初始化 HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) { // 记录事件日志,别直接退出,服务要能容忍失败 }

服务里还有一个细节:服务启动时网络栈可能还没完全就绪,防火墙策略服务(mpssvc)可能还在初始化。这时候CoCreateInstance有可能失败。稳妥的做法是加重试,失败后等 2 秒再试,最多重试三五次。这个我在一个开机自启的产品上遇到过,冷启动时偶发失败,热重启从来没有,就是时序问题。

4.3 32/64 位、路径与卸载清理

位数问题其实不大。防火墙组件在 32 位和 64 位注册表里都有注册,CoCreateInstance走的是 CLSID 查找,跟你的进程位数无关,32 位程序在 64 位系统上照样能调。真正需要注意的是:你写的规则里如果有程序路径,那个路径必须是目标程序的真实路径。如果 32 位程序装在Program Files下,规则里填的也得是那个完整路径,不能是SysWOW64下的重定向路径——GetModuleFileNameW返回的是真实路径,不会重定向,这点可以放心。

卸载清理是另一个高发区。产品升级时如果安装目录变了(从C:\Program Files\A变成C:\Program Files\A2),旧规则里的ApplicationName就指向了一个不存在的文件,规则会变成“僵尸规则”,界面上还看得见,但永远不会匹配任何流量。所以卸载流程里必须有一句“按 Grouping 清理所有本产品创建的规则”,而且升级流程里也要做,因为升级本质上就是先卸载再安装。

清理的时机也有讲究。我一般放在卸载程序的前半段,趁进程还有权限就做掉。如果放到最后,万一前面某个步骤失败了提前退出,规则就残留了。残留规则的危害是累积的——装十次卸九次,机器上就堆了几十条垃圾规则,用户打开防火墙界面看到一堆鬼画符,观感极差。

5. 常见问题与排查实录

5.1 典型故障速查表

返回列表