简介:面向局域网运维与网络安全管理场景,这套大势至网络准入控制系统 v8.0 主要用于隔离内外网设备、规范终端接入行为并降低局域网安全风险,适合企业网管、系统集成人员及需要开展终端准入与行为管控实验的技术人员参考。压缩包共 9 个文件,约 13.06MB,以 exe 可执行程序、doc 说明文档为主,并辅以 htm、txt、pdf 等格式,涵盖试用功能说明、部署使用指引、常见问题解答和产品详细介绍,便于快速了解软件能力边界与配置思路。已有 730 人学习或下载。读者可从中获取准入控制、IP/MAC 变更限制、共享访问隔离、ARP 攻击防御及终端在线状态探测等功能的说明材料,结合试用程序与文档梳理测试流程;配套 FAQ 和详细说明也有助于排查部署中的典型问题,适合作为局域网安全加固方案的参考资料。
1. 一根网线插进空闲口的那一秒,网络准入控制系统在干什么
机房里最容易被忽略的一幕:一根网线插进交换机空闲口,一台补丁落后半年、没装杀毒软件的笔记本就这么进了内网,等运维发现时它已经在广播扫描了。网络准入控制系统要解决的就是这一秒钟的事——在终端拿到业务网 IP 之前先判断它合不合规,不合规就拦在门外,只放行到修复区。这个 v8.0 的包给的是一整套:管理端、准入引擎、终端 Agent,外加一块统计监测看板,能看到谁在线、谁被拦、流量跑去了哪几个网段。
它更适合中小规模内网的自建运维团队,尤其是那种交换机型号杂、不敢一次性全网上 802.1X 的环境;也适合想搞明白「准入判定这条链路到底怎么串起来」的开发者,代码结构不复杂,改起来门槛不高。下面按我拆包的顺序讲:架构怎么分、包怎么跑起来、核心判定链长什么样、统计监测的数据从哪来,最后说我改这块代码时踩过的坑。
2. 系统骨架拆解:管理端、准入引擎、终端 Agent 的三段式分工
2.1 三个角色各管一段,谁都不越界
这套系统的职责切得很干净,三块各干各的,出问题时定位范围能立刻缩小一半。
管理端是给人看的,Web 或桌面客户端形态,负责终端台账、策略配置、事件查询和统计监测的图表渲染。它不直接碰网络设备,所有动作都落成数据库里的策略记录,等准入引擎来取。
准入引擎是真正的执行者,常驻在服务器上,做三件事:接收 Agent 心跳并维护在线状态表、按策略算出每个终端的准入结论、把结论翻译成具体动作(下发 ACL、放行、踢下线)。它是唯一有权限改网络设备配置的组件,所以通常和交换机管理网放在同一个隔离区。
终端 Agent装在每台 PC 上,负责采集本机信息——操作系统版本、补丁号、杀毒软件状态、USB 使用情况、当前 IP 和 MAC——然后周期上报。Agent 不做判定,只上报事实,判定权全在服务端。这个设计很关键:规则改一次全网立即生效,不用重新推客户端。
三者之间的通信走一条长连接加 HTTP 上报的混合模式:Agent 启动后先向引擎注册拿一个 token,之后每 30 秒心跳一次携带最新采集结果,引擎回了allow就正常上网,回了deny就自己跳到修复页面。心跳超时三分钟的终端会被标成离线,这是后面统计监测里在线率的计算口径。
2.2 目录结构:解压后先认这几个文件夹
包解开之后的目录大致长这样,认准几个关键位置,后面改代码、改配置都在这几处:
NacSystem/ ├── Server/ # 管理端 + 准入引擎,同一台机器部署 │ ├── bin/ │ │ ├── nac_server.exe # 主服务,注册成 Windows 服务 │ │ └── nac_engine.exe # 准入引擎,可单独起停 │ ├── conf/ │ │ ├── server.json # 监听端口、数据库连接、日志级别 │ │ └── segments.json # 纳管的网段与交换机清单 │ ├── web/ # 管理端前端静态资源 │ ├── plugins/ # 厂商适配插件,按交换机品牌分目录 │ └── logs/ ├── Agent/ │ ├── bin/nac_agent.exe # 终端 Agent 主程序 │ ├── conf/agent.json # 服务端地址、心跳间隔、采集项开关 │ └── install.bat ├── Tools/ │ ├── db_init.sql # 建库建表脚本 │ └── agent_packager.exe # 把配置打进安装包的打包器 └── Docs/ ├── 部署手册.pdf └── 策略配置说明.pdfplugins/是最值得先翻的地方。准入引擎对接不同品牌交换机靠的就是这一层适配器,每个插件导出一组统一接口(查询端口、下发 ACL、踢 MAC),加新品牌只需要加目录,不用动主程序。我第一次改的时候直接去改nac_engine.exe的配置,绕了一大圈才发现该动的是插件。
segments.json决定系统管哪些网段。新手最容易犯的错是把整个内网都写进去,包括服务器区,结果引擎把服务器也当终端来判定,直接把自己管的服务给拦了。
2.3 数据模型:五张表撑起台账、策略和统计
数据库是这套系统的黑匣子,理解了表结构基本就理解了它的思路。核心五张表,建表脚本在Tools/db_init.sql里:
-- 终端台账:一台机器一行,用 MAC 做主键最稳 CREATE TABLE t_terminal ( mac VARCHAR(17) PRIMARY KEY, -- 00-11-22-33-44-55 ip VARCHAR(15), hostname NVARCHAR(64), os_version NVARCHAR(64), dept NVARCHAR(64), -- 所属部门,统计监测按这个维度出图 agent_ver VARCHAR(16), last_beat DATETIME, -- 最后心跳时间,判在线用 state TINYINT DEFAULT 0, -- 0未注册 1在线 2离线 3被阻断 first_seen DATETIME ); -- 策略:一个策略挂多条规则,按部门或网段下发 CREATE TABLE t_policy ( policy_id INT PRIMARY KEY IDENTITY, policy_name NVARCHAR(64), match_type TINYINT, -- 1按网段 2按部门 3按终端组 match_value NVARCHAR(128), mode TINYINT, -- 1监控模式 2强制模式 enabled TINYINT DEFAULT 1 ); -- 规则项:策略的具体条款,判定引擎逐条执行 CREATE TABLE t_rule ( rule_id INT PRIMARY KEY IDENTITY, policy_id INT, rule_key VARCHAR(32), -- antivirus / patch / usb / registry operator VARCHAR(8), -- eq / ge / in / exists rule_value NVARCHAR(256), action TINYINT, -- 1允许 2阻断 3仅告警 weight INT DEFAULT 0 -- 同策略内多规则的优先级 ); -- 准入事件:每次判定落一条,排查误拦全靠它 CREATE TABLE t_event ( event_id BIGINT PRIMARY KEY IDENTITY, mac VARCHAR(17), ip VARCHAR(15), policy_id INT, result TINYINT, -- 1放行 2阻断 hit_rule INT, -- 命中的规则 ID,-1 表示无命中 detail NVARCHAR(512), -- 规则表达式 + 实际值,JSON 串 happen_at DATETIME ); -- 流量统计:五分钟粒度的聚合,别在这个表上做实时查询 CREATE TABLE t_traffic_stat ( id BIGINT PRIMARY KEY IDENTITY, stat_time DATETIME, segment VARCHAR(32), mac VARCHAR(17), bytes_in BIGINT, bytes_out BIGINT, conn_count INT );几个字段值得单独说。t_terminal.mac用 MAC 做主键而不是自增 ID,是因为准入这件事天然以 MAC 为身份依据,IP 会变、主机名会重名,MAC 相对唯一。代价是 MAC 被改(虚拟网卡、随机 MAC)时会多出一条新记录,统计监测里的终端总数就会虚高,这个问题在后面的避坑章还会碰到。
t_event.detail存的是判定时的现场快照,比如{"rule_key":"antivirus","expect":"exists","actual":"none"}。运维报「我没违规却被拦了」,直接把这一条取出来就能复现当时的判定条件,比翻日志快得多。
t_traffic_stat单独拆出来是有意的。终端台账是写少读多,事件表是只写不读(除了排查),流量表是高频写、按时间范围聚合读,三类负载混在一张表里迟早锁表。
3. 从解压到第一台终端上线:一份能照着走的部署流程
3.1 环境准备:Windows Server、数据库和那条容易被忽略的依赖
服务端只跑 Windows,这是硬约束,nac_server.exe依赖 Windows 的服务注册机制和 WMI 本地调用。准备一台 Windows Server 2016 以上的机器,2 核 4G 起步,如果纳管终端超过 500 台建议 4 核 8G 并把数据库单独放。
数据库支持 SQL Server 和内置的 SQLite 便携模式。小于 200 台终端直接用 SQLite,配置里改个路径就行,省掉装数据库和配账号的一堆事;超过这个量级换 SQL Server,主要原因是事件表写入太频繁,SQLite 写锁会卡住心跳响应。
最容易被忽略的是抓包驱动。统计监测模块要靠它统计真实流量,包里自带的是 WinPcap 兼容层,装之前先确认系统里没装过其他版本——装重了wpcap.dll冲突,服务能起来但流量统计永远是零,这个坑后面还会讲。安装顺序建议是:驱动 → 数据库 → 主服务。
# 以管理员身份执行,装抓包驱动 .\Tools\winpcap-setup.exe /S # 建库(SQLite 模式可跳过,服务首次启动自动建) sqlcmd -S .\SQLEXPRESS -U sa -P <你的密码> -i .\Tools\db_init.sql # 注册主服务,-install 参数会把 nac_server.exe 挂到 Windows 服务里 cd .\Server\bin .\nac_server.exe -install .\nac_server.exe -start # 确认服务起来了,端口默认 8080(管理端)和 8443(引擎通信) Get-Service NacServer | Format-List Status, StartType Test-NetConnection -ComputerName 127.0.0.1 -Port 8080-install和-start是这套程序自带的命令行参数,不要用sc create手工注册,因为它注册时会顺带写注册表里的工作目录,手工注册的服务启动时找不到conf/就会秒退,日志里只有一句「配置文件缺失」。
3.2 服务端初始化:把网段、模式和数据库三件事配对
服务起来后先别急着装 Agent,打开conf/server.json把基础参数改掉。这份配置是引擎启动时读一次,改完必须重启服务:
{ "server": { "admin_port": 8080, "engine_port": 8443, "session_timeout_sec": 180 }, "database": { "type": "sqlite", "dsn": "C:/NacData/nac.db", "pool_size": 8 }, "engine": { "default_mode": 1, "heartbeat_interval_sec": 30, "offline_threshold_sec": 180, "traffic_sample_interval_sec": 300 }, "log": { "level": "info", "keep_days": 30 } }default_mode是整个部署里最重要的一个开关:1是监控模式,只记录判定结果不动网络;2是强制模式,判定为阻断就真下手。上线第一周一律用 1,先让系统跑一周,去t_event里看有多少条会命中阻断规则,把这个数量核对一遍再切 2,否则第一天就可能把半个部门拦在门外,这是我的血泪经验,不是吓唬人。
offline_threshold_sec别设得比心跳间隔的 3 倍还小。心跳 30 秒、阈值 60 秒的配置会导致网络稍有抖动就大面积显示离线,统计监测的在线率曲线跟心电图似的。
网段配置在conf/segments.json:
{ "segments": [ { "name": "办公网A", "cidr": "10.20.10.0/24", "gateway": "10.20.10.1", "switch_vendor": "vendor_a", "switch_ip": "10.20.10.2", "username": "nac", "password": "******" }, { "name": "修复区", "cidr": "10.20.99.0/24", "gateway": "10.20.99.1", "switch_vendor": "vendor_a", "switch_ip": "10.20.10.2", "username": "nac", "password": "******" } ] }修复区必须单独列一个网段,这是设计上的强制约定:被阻断的终端不是彻底断网,而是被 ACL 重定向到修复区,那里放一个补丁服务器和杀毒安装包,修好了下次心跳自动放行。不配修复区,阻断就等于断网,运维电话会被打爆。
3.3 终端 Agent 安装与首次上线验证
Agent 的安装包里需要预置服务端地址,用Tools/agent_packager.exe把agent.json打进去再分发:
{ "server_url": "https://10.20.0.10:8443", "heartbeat_interval_sec": 30, "collect": { "antivirus": true, "patch": true, "usb_storage": true, "process": false }, "repair_portal": "http://10.20.99.1/" }collect.process默认关掉是有道理的,采集进程列表开销不小,而且容易碰到用户的隐私顾虑。真要做违规软件检测再打开,并且只上报进程名的哈希而不是完整路径。
装完一台,回到服务端验证三件事,顺序别乱:
# 1. 终端有没有注册进来 sqlite3 /data/nac.db "select mac, ip, hostname, state from t_terminal order by first_seen desc limit 5;" # 2. 心跳有没有持续更新 last_beat sqlite3 /data/nac.db "select mac, last_beat from t_terminal where mac='00-11-22-33-44-55';" # 3. 判定事件有没有落库 sqlite3 /data/nac.db "select mac, result, hit_rule, happen_at from t_event order by event_id desc limit 3;"第一条有记录、第二条时间在往前滚、第三条出现result=1的放行事件,说明这条链路通了。如果第一步就没数据,别去翻前台页面,直接看Server/logs/engine.log,注册失败的原因九成写在那里,最常见的是 Agent 那边的server_url用了 IP 但服务端证书是自签的,Agent 校验证书直接放弃了连接。
4. 核心模块逐段读:一条终端信息是怎样变成准入结论的
4.1 采集层:WMI 查补丁,注册表看杀毒
Agent 的采集逻辑是整套系统的数据源头,采集错了后面全错。它走两条路:系统类信息(补丁号、系统版本、驱动)走 WMI,软件类信息(杀毒是否安装、是否在运行)走注册表和进程双查。
下面这段是我按它的逻辑用 Python 复刻的采集器,用来在服务端侧调试规则时对照真实数据,逻辑和 Agent 里的一致:
import wmi import winreg def collect_patch(c): """取最近安装的补丁号,返回 KB 编号列表。 用 Win32_QuickFixEngineering 而不是读注册表, 因为它会过滤掉已卸载但残留记录的补丁。""" patches = [] try: for item in c.Win32_QuickFixEngineering(): if item.HotFixID and item.HotFixID.startswith("KB"): patches.append(item.HotFixID) except Exception as e: # WMI 查询本身也会失败(服务被禁用、权限不足), # 这里不能吞掉,要上报 unknown 让服务端走兜底策略 return None return patches def collect_antivirus(): """读注册表里安全中心登记的杀毒产品。 路径固定,64 位系统要读 Wow6432Node 下的镜像。""" key_path = (r"SOFTWARE\Microsoft\Windows\CurrentVersion" r"\Uninstall") result = [] for hive in (winreg.HKEY_LOCAL_MACHINE,): try: with winreg.OpenKey(hive, r"SOFTWARE\Microsoft\Security Center" r"\Provider\Av") as k: i = 0 while True: try: name = winreg.EnumKey(k, i) with winreg.OpenKey(k, name) as sub: state = winreg.QueryValueEx(sub, "State")[0] result.append({"name": name, "state": state}) i += 1 except OSError: break except FileNotFoundError: # 老系统没有这个键,退回扫进程判断 return collect_av_by_process() return result def collect_av_by_process(): """兜底方案:看常见杀毒进程在不在。 缺点是不认品牌,只认「有没有防护在跑」。""" c = wmi.WMI() guards = ("msmpeng.exe", "qqpctray.exe", "360tray.exe") running = {p.Name.lower() for p in c.Win32_Process()} return [{"name": g, "state": 1} for g in guards if g in running]Win32_QuickFixEngineering这个类有个特性值得知道:它返回的是「已安装且未卸载」的补丁,比直接扫C:\Windows\SoftwareDistribution目录准确得多。但也有例外,某些通过 MSI 单独装的补丁不会出现在这个列表里,所以规则里判断补丁不能只判断「是否包含 KBxxxxxx」,还得允许按安装日期判断——InstalledOn字段取出来比对一个基线日期,鲁棒性更好。
采集函数返回None和返回空列表是两件完全不同的事:None表示「我没查出来」,空列表表示「我查了,确实没有」。前者应该触发兜底策略(一般是放行并告警),后者应该正常按规则判定。Agent 里如果不区分这两种情况,WMI 服务偶尔抽风一次就会导致大批机器被判成不合规,这是很典型的翻车方式。
4.2 判定引擎:把策略表翻译成布尔表达式
判定引擎的输入是「终端上报的采集结果」和「该终端匹配到的策略规则集」,输出是一个准入结论加命中的规则 ID。它的核心就是一次规则遍历,但要处理好优先级和短路:
def judge(terminal, rules): """ terminal: dict,Agent 上报的采集结果 rules: list,按 weight 降序排好的规则 返回 (result, hit_rule_id, detail) result: 1 放行 / 2 阻断 """ # 没有任何规则命中时,默认放行。这是刻意的选择: # 策略没覆盖到的终端不应该被误伤 hit = None for rule in sorted(rules, key=lambda r: -r["weight"]): actual = terminal.get(rule["rule_key"]) if actual is None: # 采集缺失,记一条 detail 但不判阻断 continue ok = eval_rule(rule["operator"], actual, rule["rule_value"]) # action=1 表示这条规则是白名单性质,命中即放行,直接短路 if ok and rule["action"] == 1: return 1, rule["rule_id"], {"rule": rule["rule_key"], "actual": actual, "pass": True} # action=2 命中即阻断,但要先把同优先级里更严格的看一遍 if not ok and rule["action"] == 2: hit = (rule, actual) if hit: rule, actual = hit return 2, rule["rule_id"], {"rule": rule["rule_key"], "expect": rule["rule_value"], "actual": actual} return 1, -1, {} def eval_rule(op, actual, expect): if op == "exists": return bool(actual) if op == "eq": return str(actual) == str(expect) if op == "ge": return int(actual) >= int(expect) if op == "in": return str(actual) in str(expect).split(",") raise ValueError(f"未支持的操作符: {op}")这里有个我改过的点。原版是「遇到第一条不通过的阻断规则就立即返回」,改成「遍历完之后再返回第一个命中」,原因是规则集里常有一条兜底白名单(比如dept=IT直接放行),如果阻断规则排在前面先短路了,IT 部门的机器一样被拦。weight就是用来控制这个顺序的,权重高的先算,白名单规则给最高权重。
eval_rule只支持四种操作符看着简陋,但覆盖了绝大多数场景:exists判断装没装,eq判断版本号相等,ge判断补丁数量够不够,in判断系统版本在不在白名单里。真要做复杂表达式(比如「杀毒要装 且 补丁数 ≥ 5」),靠的是同一个策略下挂多条规则,而不是把表达式塞进一个字段——这个设计省掉了写表达式解析器的活,二次开发时直接加一条规则就行。
策略匹配那一层在t_policy.match_type上分三种:按网段、按部门、按终端组。优先级是终端组 > 部门 > 网段,取最高优先级里第一个命中的策略。这个优先级定义在文档里写得很模糊,我是从nac_engine.exe的日志里反推出来的——它每次判定会打一行policy matched: id=3, by=dept,多试几次就能确认。
4.3 阻断动作:ACL 下发为主,其他手法做兜底
判定出result=2之后,引擎要把它变成网络上的实际效果。包里支持三种方式,按可靠性从高到低排:
第一种是交换机 ACL 下发,走plugins/里的厂商适配器,把终端 MAC 加进隔离 ACL。这是首选,因为动作明确、可回滚、审计日志清楚。缺点是需要交换机管理权限,而且各品牌 CLI 语法不一样。
第二种是 802.1X 联动,向 RADIUS 服务器下发一个拒绝属性,让认证失败。这种方式最标准,但要求交换机端口事先配好 802.1X,改造量大,一般只有新建网络才用。
第三种是 ARP 方式,靠持续发送 ARP 响应把终端的网关指向一个不存在的地址,属于兼容老交换机的兜底手段。它的问题是粗暴——终端自己配了静态 ARP 就失效,而且会对同网段产生额外流量,所以默认关闭,只在segments.json里显式打开才生效。
下面是一个走 ACL 接口的调用示例,逻辑和插件里的deny()一致:
import requests def deny_by_acl(switch, mac, vlan, timeout=5): """把 MAC 加进交换机的隔离 ACL 组。 switch: segments.json 里的一段配置 返回 True 表示下发成功,False 表示要降级到兜底方案 """ payload = { "vlan": vlan, "acl_group": 3999, # 隔离组编号,各厂商约定不同 "action": "add", "mac": mac, "remark": "nac-block" } try: r = requests.post( f"http://{switch['switch_ip']}/api/acl", json=payload, auth=(switch["username"], switch["password"]), timeout=timeout ) return r.status_code == 200 and r.json().get("code") == 0 except requests.RequestException: # 交换机没响应不能反复重试,会拖垮引擎线程池 return False注意那个timeout=5。交换机管理口响应慢是常态,但不设超时会让引擎的判定线程一直挂着,几百台终端同时触发阻断时线程池直接耗尽,表现出来就是「系统卡死」。我给这个值调过三次,最后定在 5 秒,配合一个异步补偿队列:下发失败的进队列,30 秒后重试一次,还失败就发告警让人工介入。
放行动作比阻断简单,因为「不拦」是默认状态,只需要把之前加的 ACL 条目删掉。但这里有个隐藏的坑:如果终端在阻断期间换了 IP,ACL 里按 MAC 删是能删掉的,按 IP 删就漏了,残留的 ACL 条目会一直挂着,直到交换机重启。所以插件接口统一按 MAC 操作,不提供按 IP 的删除入口。
4.4 统计监测:在线状态和流量数据是怎么攒出来的
统计监测这块是很多同类产品做得最糊的地方——页面上画几条曲线,点进去发现数据延迟半小时,或者终端明明在线却显示 0。这套系统的采集口径相对清楚,分两条线。
在线状态不用单独采集,直接用心跳。t_terminal.last_beat每次心跳更新,查询时用now() - last_beat < offline_threshold判断在线,所以这个指标天然是准的,最多延迟一个心跳周期。
流量数据靠两路来源:Agent 上报本机网卡的收发字节数增量,以及服务端抓包统计的网关侧总流量。两路数据分开存,页面上可以对比,一旦差距过大说明 Agent 采集出了问题。
五分钟粒度的聚合是定时任务做的,SQL 大概是这个意思:
-- 每 5 分钟把心跳上报的增量累加进统计表 -- 用整 5 分钟对齐,避免一条记录跨两个时间片 INSERT INTO t_traffic_stat (stat_time, segment, mac, bytes_in, bytes_out, conn_count) SELECT datetime(strftime('%s', beat_time) - strftime('%s', beat_time) % 300, 'unixepoch') AS slot, t.segment, t.mac, SUM(t.bytes_in_delta), SUM(t.bytes_out_delta), COUNT(DISTINCT t.remote_ip) FROM t_beat_raw t WHERE t.beat_time >= datetime('now', '-10 minutes') AND t.beat_time < datetime('now', '-5 minutes') GROUP BY slot, t.segment, t.mac;strftime('%s', beat_time) % 300这一步是把时间戳对齐到 5 分钟边界。看着多余,但不做的话同一台终端在 10:00:01 和 10:00:04 的两条心跳会进两个不同的时间片,聚合表里就出现一堆只有一两条记录的碎片行,图表上表现为锯齿。
WHERE条件里的时间窗是[-10分钟, -5分钟),比当前时间滞后一个窗口。这是故意的:心跳可能延迟到达(网络抖动、Agent 重连),如果统计最近 5 分钟的数据,那些晚到的心跳就丢了,总量会偏小。滞后一个窗口等于给数据留了 5 分钟的缓冲期。
前端看板上的三个核心指标是:在线率(在线终端数 / 台账总数)、合规率(最近一次判定为放行的终端数 / 在线终端数)、阻断趋势(t_event按小时分组统计result=2的数量)。合规率这个指标我建议天天看,它突然掉下去往往不是有人违规,而是某条规则写错了或者在批量推补丁。
5. 避坑与排查:上线头一周最容易翻车的五个地方
5.1 部署期:服务起来了但终端进不来
现象一:服务显示 Running,管理页面打不开,端口检测也通。
原因是nac_server.exe注册成服务时的工作目录被设成了C:\Windows\System32,它去那里找conf/server.json找不到,进程退出了但服务状态还没刷新。解决办法是用自带的-install参数注册,或者手工在服务属性的「起始参数」里补绝对路径。判断方法很简单:Server/logs/下没有当天的日志文件,就是根本没读到配置。
现象二:Agent 装完显示「已连接」,但服务端台账里一直没有这台机器。
八成是证书问题。Agent 用 HTTPS 连8443,服务端首次初始化时生成的是自签证书,Agent 侧默认做完整校验,自签直接拒。这个错误的坑在于它不报错,只是重连,界面上显示「连接中」。解决办法是把服务端证书导出来,装进终端的信任根,或者先在agent.json里把server_url改成http://验证链路是否通——通了再回头修证书,别一上来就怀疑代码。
现象三:纳管了全网段之后,补丁服务器自己也连不上了。
服务器被引擎当成了终端来判定。修改segments.json,把服务器区排除掉,或者给服务器做一个单独的终端组,策略里直接放行。这个问题在监控模式下不会暴露,切成强制模式那一刻才会集体爆发,所以切模式前一定先做一次全量判定核对。
5.2 运行期:误拦、漏拦和对不上的数
现象四:同事说「我什么都没改,早上突然被拦了」。
先别辩解,去t_event里按 MAC 查最新几条:
SELECT event_id, hit_rule, detail, happen_at FROM t_event WHERE mac = '00-11-22-33-44-55' ORDER BY event_id DESC LIMIT 5;detail里会写着命中的是哪条规则、期望值和实际值。我遇到过的真实原因有三类:杀毒软件升级过程中短暂退出,被瞬时采集到「未运行」;WMI 服务被系统更新重启,采集返回None被当成空;还有一类是规则本身写得太严,比如要求补丁数ge 5,结果新装的系统本来就只有 3 个。
正确的处理不是放宽规则,而是给规则加「连续 N 次命中才阻断」的确认机制。包里t_rule表没有这个字段,我是自己加了一列confirm_times,判定引擎里维护一个内存计数器,连续 3 次判阻断才真正下发 ACL。这一个改动把误拦率降了一大截。
现象五:统计监测的在线率一直比实际情况低 10% 左右。
终端总数虚高。原因在 4.1 提到的 MAC 问题——开了随机 MAC 的笔记本、装了虚拟网卡的开发机,每次重连都产生一条新台账记录。解决办法是在t_terminal上加一个「按主机名 + 部门归并」的视图,统计在线率时用视图而不是原表,同时在 Agent 里优先上报物理网卡的 MAC。
还有个小概率情况值得单独提:t_traffic_stat里某几个网段的数据连续几个小时全是零。去看Server/logs/traffic.log,如果里面反复出现pcap open failed,就是抓包驱动装重了。卸载掉两个版本的驱动,只留包里自带的那一份,重启服务即可。这个错特别隐蔽,因为服务端一切正常,只是流量统计静默失败。
6. 二次开发:把策略引擎换成你自己的规则
这套代码最值得动的地方是判定引擎,因为它的规则模型是「一行一条规则」的扁平结构,加复杂逻辑就得改表结构。我一般的做法是保留t_rule不动,新增一张t_expr表存表达式 DSL,判定时把两边的规则合并执行——这样原厂升级时不会冲突。
改造的关键是给judge()换一个规则来源。上面 4.2 那段代码里rules是从数据库读的,你只需要在读的地方多做一步:
def load_rules(policy_id): """把扁平规则和表达式规则合并成统一的 list, 表达式规则先编译成可调用对象塞进 extra 字段。""" rules = query_flat_rules(policy_id) for expr in query_expr_rules(policy_id): rules.append({ "rule_id": expr["id"], "rule_key": "__expr__", "operator": "call", "rule_value": compile_expr(expr["dsl"]), "action": expr["action"], "weight": expr["weight"] }) return rulescompile_expr里用eval是有风险的,别直接执行用户输入。更稳的做法是限定一个白名单语法:只允许字段名、比较符、and/or和数字常量,用正则先校验一遍再交给ast.literal_eval和手写的解释器。我吃过一次亏——测试环境里随手写了个表达式,把__import__带进去了,好在是内网测试库,但也足够让人后背发凉。
验证改造是否生效,别只看页面。写一个离线回归脚本,把历史t_event里的detail快照拿出来,用新引擎重算一遍,对比result有多少差异:
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 规则解析 | 用 50 条历史事件回放 | 命中规则 ID 与原记录一致 |
| 优先级 | 构造同权重的白名单+黑名单冲突 | 白名单优先放行 |
| 阻断下发 | 单台终端手工触发,看交换机 ACL | 5 秒内出现nac-block条目 |
| 统计口径 | 对比 Agent 上报总量与抓包总量 | 偏差小于 5% |
| 回滚 | 停引擎,恢复原 ACL 备份 | 终端 1 分钟内恢复上网 |
这套回归跑一遍大概十分钟,比在生产环境上试错便宜太多。
最后说个我自己的习惯。每次改判定引擎,我都会先把它切到监控模式跑满 24 小时,去t_event里数一遍result=2的条数,跟改之前的基线对比。有一次新加的补丁规则多拦了 40 台机器,就是因为漏了这个步骤,第二天上班被叫去机房挨个放行。从那以后我每次动规则都强制走一遍「监控模式 + 数量核对 + 小范围切强制」,再也没翻过车。希望帮到你。
本文还有配套的精品资源,点击获取