1. 先搞清楚这个工具到底解决什么问题
机房搬迁前那一周,我翻出来的IP资产表是三年前的,标注着「测试机-勿动」「临时借用」的备注有十几条,实际 ping 过去,一半地址无人应答,另一半应答的机器型号完全对不上。这种场景做运维的都懂:IP地址是网络里最不稳定的资产,DHCP 会回收,物理机会挪位置,虚拟机几分钟就能开一台,但几乎没人会主动更新那张 Excel。网络扫描这件事之所以必要,不是因为它听起来专业,而是因为它是唯一能把「我以为网络里有什么」变成「网络里实际有什么」的手段。Advanced_IP_Scanner就是在这种需求下长出来的一个小工具——把 IP 存活发现、端口探测、指纹识别、资产沉淀和变更追踪串成一条流水线,而不是每次盘点都靠零散命令拼凑。
它适合谁?我按经验分成三类。第一类是中小规模网络的运维人员,手里管着几百个地址,没有预算上商业资产管理系统,需要一份能自动跑、能对比历史的结果;第二类是想理解网络探测原理的开发者,直接调第三方库只能看到黑盒输出,自己写一遍才知道超时、并发、重传这些参数为什么会把结果带偏;第三类是做内网安全自查的工程师,需要在授权范围内摸清暴露面。IP扫描工具在错误的场景下是风险,在正确的场景下是体检报告,这个界限后面我会专门用一节讲清楚。
需要先声明一点:本文所有内容都建立在「你对自己拥有管理权限、或已取得书面授权的网络进行探测」这个前提上。没有这个前提,剩下的内容一个字都不该往下看。工具本身是中性的,用它的人决定了它是什么。
至于为什么不直接用一个现成的图形化扫描器,我在下面第一节会细说。简单讲,现成工具给我的是「一次性的答案」,我真正需要的是「可对比的时间序列」——今天的 192.168.10.0/24 和上周的有什么不同,哪台机器换了 MAC,哪个端口是新开的。这个需求决定了整套东西的形态:扫描只是入口,管理才是主体。
2. 整体设计与技术选型:为什么这么搭
2.1 现成扫描器为什么不够用
我试过好几种现成方案,问题不在功能弱,而在形态不对。图形化扫描器输出的是报表,报表看完就完了,下次再扫一遍,两份报表放在一起,人工比对几乎不可能。命令行工具灵活,但你得自己记住一长串参数,换个人接手就抓瞎。更要命的是,多数工具默认「一次性最大化」——把并发拉满、超时压到极短,结果就是漏报和误报同时变多,而你无从判断哪个是哪个。
真正卡住我的是三件事。第一,结果不可追溯。机器今天在线,明天离线,后天换了个系统,如果每次都只保存「当前状态」,你永远不知道变化发生在什么时候。第二,语义缺失。工具告诉你 22 端口开着,但没告诉你这是台网络设备还是台开发机,而这两者的处置策略完全不同。第三,无法裁剪。内网里有大量不该被扫的设备,工业控制设备、老旧的打印服务器、某些对探测包敏感的终端,一旦被打,轻则卡顿重则掉线,现成工具很难做精细的排除。
所以这个项目的设计目标很明确:慢一点没关系,结果必须可解释、可对比、可回滚。这句话看起来像口号,但它直接决定了后面每一个参数的选择。宁可把并发从 500 降到 200,也要保证扫描期间办公室没人喊网卡;宁可多花五分钟做二次确认,也不要交一份带着十几个幽灵设备的报表。
2.2 四层架构:发现、识别、沉淀、呈现
把功能拆开看,整套东西是四层,层与层之间只通过数据结构耦合,任何一层都可以单独替换。这个分层不是为了好看,是为了调试的时候能快速定位问题出在哪。
| 层级 | 职责 | 关键输出 | 常见故障点 |
|---|---|---|---|
| 发现层 | 判断地址上有没有活物 | IP、MAC、响应时延 | 跨网段 ARP 失效、ICMP 被策略拦截 |
| 识别层 | 判断活物是什么 | 开放端口、服务 banner、主机名 | 超时过短导致漏报、banner 混淆 |
| 沉淀层 | 存下来、比对历史 | 资产表、变更记录 | 主键设计错误导致同一设备被记成两台 |
| 呈现层 | 给人看 | 报表、差异清单、告警 | 只输出结果不给上下文 |
发现层我只做一件事:确定地址上有没有设备在响应,不做任何端口尝试。这个约束很重要,因为存活探测的报文极轻,即便扫错范围,影响也有限;而端口探测的报文重得多,必须建立在准确的存活清单上。把两件事混在一起做,结果就是对着 254 个地址里那 30 台真正在线的机器旁边,还有 224 个不存在的主机白白等待超时,浪费掉 80% 的时间预算。
识别层是重头戏,也是唯一可能给目标造成压力的部分。我的做法是分两轮:第一轮只扫一份「高价值端口清单」,大概 60 到 100 个,覆盖常见的管理端口、Web 端口、数据库端口和远程访问端口;第二轮针对第一轮有响应的主机,做一次全端口或大范围扫描,并且降低并发。这种「先粗后细」的节奏,实测能把总耗时压到单轮全端口扫描的三分之一左右。
沉淀层的设计踩过坑,我一开始用 IP 做主键,结果 DHCP 环境下一周之内同一个 IP 被记成了七台不同设备,变更记录爆炸。后来改成用 MAC 作为设备身份的锚点,IP 只是「当前使用的地址」,逻辑立刻就顺了。呈现层反而是最简单的,因为数据规范了,输出什么格式都只是拼装。
2.3 技术栈取舍:Python + asyncio + Scapy + SQLite
选 Python 不是因为性能好,是因为改起来快。这类工具的需求几乎一定会变——今天要加一个端口,明天要加一个排除网段,后天要接一个告警渠道。用编译型语言写,改一次编译一次,热情很快就磨没了。
并发模型用 asyncio 而不是多线程,理由很实在:这个任务是典型的 IO 密集型,绝大部分时间花在等超时上,线程模型下每个线程占着几 MB 栈空间,几百个线程就开始疼了;协程几乎没有切换成本,开两三千个并发任务内存也稳。而且 asyncio 的wait_for语义清晰,超时控制不会有线程池那种任务堆积的问题。
原始报文操作用 Scapy,主要为了 ARP 扫描。ARP 没有对应的 TCP/UDP 端口,普通 socket 玩不转,必须自己构造以太网帧。Scapy 写起来直观,一行就能拼出广播 ARP 请求。代价是它的性能一般,接收大量响应时解析较慢,所以我把 ARP 扫描限制在单次小网段,不拿它做全量扫描的引擎。
存储用 SQLite。有人会说几百台设备用什么数据库不行,但我要的就是「拷一个文件就能带走」的便利性。整个资产库就是一个.db文件,备份就是复制,交给同事就是发个附件,不需要装服务、不需要配账号。几百到几千台设备的量级,SQLite 的写入性能完全够看,加个索引查起来也是毫秒级。
2.4 扫描节奏控制的整体思路
这里说一个贯穿全文的原则:扫描是一次对网络的写入行为,不是只读。你发出的每一个包都会占用交换机的转发表、防火墙的会话表、目标主机的 CPU。所以节奏控制不是可选项。
我的控制手段有三层。第一层是全局并发上限,用信号量卡死,任何时候同时进行的探测不超过设定值。第二层是分批提交,不是一次性创建几万个协程,而是按主机分批,一批做完再进下一批,这样内存占用可控,中断时也有中间结果。第三层是网段级的错峰,如果一次要扫多个网段,网段之间加个短间隔,避免瞬间冲击同一台汇聚交换机。
这三层加起来的实际效果是:扫描 /24 网段的一百个常用端口,在有三十台在线设备的情况下,总耗时稳定在 20 秒上下,出口流量峰值不到 2 Mbps,办公室完全无感。对比一开始我图快用的「全量协程 + 0.1 秒超时」,耗时确实降到 4 秒,但漏了三台响应慢的老设备,还把一台网络打印服务器打到假死,挨了一顿说。
3. 核心原理拆解:每一步到底在发生什么
3.1 二层 ARP 与三层探测的适用边界
这是最容易被忽略、也最容易导致「扫不到」的地方。ARP 工作在二层,本质是广播「谁是这个 IP,请把你的 MAC 告诉我」,只有同一个广播域内的设备才会回应。所以你扫192.168.10.0/24,如果自己的机器在192.168.20.0/24,ARP 请求根本发不过去,结果必然是零。很多人第一次跑不通就是这个原因,还以为是工具坏了。
跨网段怎么办?只能退回三层:ICMP 回显请求,或者 TCP 连接尝试。这两种方式的问题在于,它们都是「可以被策略关掉」的。大量终端和服务器出于安全加固考虑会禁用 ICMP,这时候 ping 不通不代表设备不在。所以我实际的存活判断是三级串联:
- ARP 探测(同网段优先,最快最准)
- ICMP 探测(跨网段时的第一尝试)
- TCP 探测(对 80、443、22、445 这几个端口做连接尝试,任意一个成功即判定存活)
这个顺序有讲究。ARP 放第一位是因为它几乎不会误判,收到应答就是真的有设备占着这个地址。ICMP 排第二是因为快,但可靠性一般。TCP 放最后兜底,因为它最慢但最难被完全屏蔽——除非设备什么服务都不开,否则总有端口能连上。
需要注意的是,ARP 扫描在虚拟化环境里会有个副作用:某些宿主机上的虚拟网卡会对整个网段做代理 ARP 应答,导致你扫出大量 MAC 相同的「幽灵设备」。我在一个虚拟化平台上扫出过 60 多个不同 IP 对应同一个 MAC 的情况,一开始以为是拓扑错了,后来才反应过来是代理 ARP。处理办法是在结果里做 MAC 聚合,同一个 MAC 对应超过 3 个 IP 的,单独标记出来人工确认。
3.2 端口探测方式与超时时间的确定
端口探测有三种常见写法,特点和代价差别很大。
第一种是全连接扫描,就是老老实实完成 TCP 三次握手,连上之后再主动断开。优点是不需要特殊权限,普通用户就能跑,而且结果最可靠——连上了就是连上了,没有歧义。缺点是会在目标的应用日志里留下完整记录,如果你的目的只是清点而不是留痕,这就不太合适。另外它消耗目标的连接资源,对老旧设备压力较大。
第二种是半开扫描,只发 SYN,收到 SYN-ACK 就判定开放,然后直接发 RST 断开,不完成握手。优点是快、隐蔽、对目标压力小。缺点是需要构造原始报文,通常需要管理员权限,而且不同系统的行为差异较大,实现起来麻烦。
第三种是间接推断,比如发一个畸形包看返回的 RST 类型,或者利用某些协议的特性。这种方式我不推荐用于生产盘点,误判率太高,只在特定排查场景下用。
我在这个工具里主用全连接扫描,理由是可靠性优先,且大多数内网盘点场景对「留痕」并不敏感——本来就是自己的网络,留痕反而是好事,出问题能查到是谁在什么时候扫的。只有在需要覆盖大量地址、追求速度时,才切换到半开模式。
超时时间怎么定?这是最需要实测的参数。我做过对比测试,在同一个千兆内网里对不同状态的端口做连接尝试:
| 端口状态 | 典型响应时间 | 建议超时 |
|---|---|---|
| 开放,服务正常 | 1 到 5 ms | 0.3 s 足够 |
| 关闭,内核回 RST | 1 到 3 ms | 0.3 s 足够 |
| 被防火墙丢弃(无响应) | 无返回,靠超时 | 需要 0.5 到 1 s |
| 跨网段、经过多跳 | 10 到 50 ms | 0.8 s 稳妥 |
| 老旧设备或高负载主机 | 100 到 800 ms | 1.5 s 起 |
关键洞察是:真正拖时间的不是开放端口,而是被防火墙静默丢弃的端口。开放和关闭的端口都是毫秒级返回,只有被 DROP 的才会一直等到超时。而一份典型的内网端口清单里,被丢弃的比例可能占到 70% 以上。所以超时设置的每一个 0.1 秒,都会被放大几十倍。
我最终的取值是:同网段扫描用 0.6 秒,跨网段用 1.2 秒。这个数字是拿漏报率换来的——把超时从 0.3 提到 0.6,漏报的设备从 4 台降到 0 台,总耗时从 12 秒涨到 21 秒。我认为这笔交易划算。
3.3 并发数的量化推导
并发数不是拍脑袋定的,它和三个量强相关:待探测的总次数、单次超时、以及你能接受的总时长。
先算总量。一个 /24 网段有 254 个可用地址,假设存活率 15%,大约 38 台在线。对这 38 台扫 100 个端口,总探测次数是 3800 次。如果并发是 200,最坏情况下的轮数是3800 / 200 = 19轮,每轮都吃满超时 0.6 秒,总时长19 × 0.6 = 11.4秒。这是理论上限,实际情况因为有大量端口会立刻返回 RST,平均每轮远小于 0.6 秒,实测在 6 到 8 秒之间。
如果把并发提到 500,轮数降到 8 轮,理论上限 4.8 秒。看起来收益明显,但代价是什么?500 个并发连接意味着瞬间要向网络里抛出 500 个 SYN 包。假设每个包 60 字节,加上响应,瞬时带宽不到 1 Mbps,带宽不是瓶颈。真正的瓶颈是中间设备的会话表。很多接入层交换机和小型防火墙的并发会话上限只有几千,500 个瞬时新建连接会让某些设备的表项快速老化,影响正常的业务流量。我在一台老式防火墙上实测过,并发 800 时,同一时间段内办公网的网页打开明显变慢。
所以我的建议值是:普通办公网 150 到 250,服务器区可以到 400,经过防火墙的跨区扫描压到 100 以内。这个数字不是固定的,可以按下面的公式粗算再留 30% 余量:
并发数 = 目标可承受的新建连接速率 × 单次探测平均耗时举例:如果一台设备能承受每秒 500 个新建连接,单次探测平均耗时 0.02 秒(健康内网的典型值),那么并发500 × 0.02 = 10,这个结果明显偏保守了,因为它假设单次只占一瞬间。实际使用中我按「目标设备每秒能接收的新建连接数除以 2」作为并发上限,再叠加前面说的分段和错峰,基本不会出问题。
3.4 轻量指纹识别:不追求精确,追求可用
识别层的目标不是把每台设备精确到型号和固件版本,那需要庞大的指纹库和大量主动探测,代价太高。我要的是「够用」——能区分出服务器、网络设备、终端、打印机、摄像头这几个大类,就足以支撑后续的处置策略。
策略是组合式的,按成本从低到高排:
第一优先是 MAC 前缀。拿到 MAC 之后查厂商前缀,准确率高、成本为零。这一步能识别出绝大部分网络设备、虚拟化平台、品牌终端。局限是很多设备会伪造或随机化 MAC,虚拟机的 MAC 前缀只能看出宿主机厂商。
第二优先是端口组合特征。单看一个端口意义不大,但组合起来信息量很大。比如 22 + 3306 + 6379 同时开着,大概率是台开发测试机;161 + 23 + 两个高位端口,大概率是网络设备;9100 + 515 开着,基本可以确定是打印机;554 + 8000 + 37777 这种组合,多半是视频监控设备。我把这些组合整理成一张规则表,命中即打标签。
第三优先是 banner 抓取。对明确的文本协议端口,连上去读第一段返回内容。SSH 会返回版本串,FTP 会返回欢迎信息,SMTP 会报主机名。这一步能拿到最具体的信息,但也最容易出问题——有些服务连上就挂住不返回,必须设短超时;有些返回的是二进制协议头,直接当文本读会乱码,需要先判断可打印字符比例。
第四优先才是主动探测。对 80 和 443 端口发一个 HEAD 请求,看响应头里的 Server 字段;对 443 发一个 TLS 握手,从证书里读组织名。这层我默认关闭,只在需要深度盘点时手动打开,因为它产生的流量最明显,也最容易被目标的审计系统记录。
提示:banner 抓取一定要设置「读超时」和「最大读取字节数」两个限制。我吃过亏,有个服务连上之后持续推送日志,程序一直读到内存爆掉。限制成 512 字节、超时 2 秒之后就再没出过事。
4. 关键模块实现与可直接复用的代码
4.1 工程结构与依赖清单
结构刻意做得很平,不搞复杂的包划分,因为这个工具的规模撑不起那么多层目录,层级一多自己找文件都费劲。
ipscanner/ main.py 入口,参数解析与流程编排 discovery.py 存活发现(ARP / ICMP / TCP) portscan.py 端口探测引擎 fingerprint.py 指纹识别 store.py 数据库读写与变更比对 report.py 报表与差异输出 config.yaml 端口清单、排除网段、并发参数依赖就四个,都是纯 Python 或者有预编译包的,安装不折腾:
pip install scapy aiohttp pyyaml tabulatescapy负责 ARP 构造,aiohttp负责 HTTP 指纹探测,pyyaml读配置,tabulate输出表格。数据库用标准库的sqlite3,不需要额外装。刻意不引入重量级框架,因为这类工具的部署环境往往很受限,依赖越少越好活。
配置外置成 YAML 而不是写在代码里,是为了让不写代码的同事也能改。这一点看起来是小事,实际价值很大——端口清单和排除列表是高频变动项,让运维同事自己去改配置,比我每次改代码重新部署要快得多。
4.2 地址池生成:CIDR、排除项与特殊地址
地址生成看着简单,坑却不少。直接把 CIDR 展开成列表会包含网络地址和广播地址,扫这两个地址在某些设备上会引起异常行为。另外内网里总有需要排除的段:管理网段、存储网段、工控设备段、还有领导的固定 IP(这个真不是玩笑,有台机器的防火墙会把扫描识别为攻击并自动封禁源 IP)。
import ipaddress def build_targets(include_cidrs, exclude_cidrs=(), exclude_ips=()): include_nets = [ipaddress.ip_network(c, strict=False) for c in include_cidrs] exclude_nets = [ipaddress.ip_network(c, strict=False) for c in exclude_cidrs] excluded = set(exclude_ips) targets = [] for net in include_nets: for ip in net.hosts(): # hosts() 自动跳过网络地址和广播地址 if ip in excluded: continue if any(ip in ex for ex in exclude_nets): continue targets.append(str(ip)) return targets这段代码的关键在net.hosts()。它会自动排掉网络地址和广播地址,对于 /31 和 /32 这种特殊掩码也有正确处理。手写range循环很容易在这几个边界上翻车,尤其是 /31 的点对点链路,很多人的代码会一个地址都生成不出来。
另外我加了一个隐式规则:默认排除.1和.254,除非用户显式要求扫描。理由是这两个地址绝大多数情况下是网关,而网关往往是最敏感的设备。这条规则救过我一次——某次误操作把管理网段的网关扫了一遍,好在提前排除了,否则整栋楼的网络都要受影响。
还有一个细节是地址去重和排序。多个 CIDR 之间可能有重叠,直接拼接会产生重复地址,导致同一台设备被探测两次、结果里出现两条记录。用set去重后再按 IP 数值排序,输出就干净了。
4.3 异步探测引擎的核心实现
引擎的核心是三个东西:信号量控并发、wait_for控超时、分批控内存。先看基础的单次探测:
import asyncio async def probe_tcp(host, port, timeout): try: reader, writer = await asyncio.wait_for( asyncio.open_connection(host, port), timeout=timeout ) banner = b"" try: banner = await asyncio.wait_for(reader.read(512), timeout=1.0) except (asyncio.TimeoutError, Exception): pass writer.close() try: await writer.wait_closed() except Exception: pass return host, port, True, banner except (asyncio.TimeoutError, ConnectionRefusedError, OSError): return host, port, False, b""这里有个容易被忽略的点:writer.close()之后要调wait_closed(),而且得用 try 包住。不这么做的话,在某些情况下会产生「连接未正确关闭」的警告,大量出现时会刷屏,还会占着文件描述符。这个坑我排查了很久,一开始以为是并发太高导致的资源泄漏,最后发现是连接没优雅关闭。
再看批量调度:
async def scan_batch(hosts, ports, concurrency=200, timeout=0.6, batch_size=64): sem = asyncio.Semaphore(concurrency) open_results = [] async def worker(h, p): async with sem: return await probe_tcp(h, p, timeout) for i in range(0, len(hosts), batch_size): chunk = hosts[i:i + batch_size] tasks = [worker(h, p) for h in chunk for p in ports] results = await asyncio.gather(*tasks, return_exceptions=True) for r in results: if isinstance(r, Exception): continue host, port, ok, banner = r if ok: open_results.append({ "ip": host, "port": port, "banner": banner[:200].decode("utf-8", "ignore").strip() }) await asyncio.sleep(0.05) # 批次间微停顿,给中间设备喘息 return open_resultsbatch_size取 64 是权衡的结果。取得太大,一批创建的任务太多,信号量排队严重,中断时丢失的中间结果也多;取得太小,批次切换开销占比上升。64 台主机 × 100 端口是 6400 个任务,配合 200 的并发,一批大概 5 到 8 秒跑完,出错时损失可控。
那个asyncio.sleep(0.05)是我特意加的。理论上不需要,协程切换本来就是协作式的,但实际测试发现去掉它之后,某些老式交换机的端口状态会短暂异常。0.05 秒的停顿不影响整体耗时,却能明显降低对网络的冲击,属于「花小钱办大事」。
4.4 结果落库与资产变更比对
数据库设计的关键决策是设备身份用什么标识。前面提过,用 IP 会出错。最终的方案是用 MAC 作为设备主键,同时保留 IP 的当前绑定关系。
CREATE TABLE devices ( mac TEXT PRIMARY KEY, last_ip TEXT, vendor TEXT, hostname TEXT, category TEXT, first_seen TEXT, last_seen TEXT, online INTEGER DEFAULT 1 ); CREATE TABLE services ( mac TEXT, port INTEGER, banner TEXT, first_seen TEXT, last_seen TEXT, PRIMARY KEY (mac, port) ); CREATE TABLE changes ( ts TEXT, mac TEXT, change_type TEXT, detail TEXT );变更比对的逻辑是每次扫描结束后跑一遍差异计算,产生三类事件:新设备上线(库里没有这个 MAC)、设备离线(库里有但本次没扫到,且连续三次未出现)、属性变更(IP 变了、端口增减、banner 变了)。
「连续三次未出现才判定离线」这个设计很必要。内网里设备开关机、休眠、网络抖动都是常态,如果一次扫不到就标离线,变更日志会被噪音淹没,真正重要的变化反而看不见。三次是个经验值,如果你的扫描频率是每天一次,那就是三天;如果是每小时一次,三小时就够。
还有一个细节是IP 复用检测。当某个 IP 上的 MAC 和上次记录的不一致时,说明这个地址换了主人。这种情况要单独记一条事件,因为它是排障时最有用的线索——很多「服务突然连不上」的问题,根源就是 DHCP 把一个地址分配给了别的机器。
4.5 报表输出:让人一眼看懂
报表我坚持两个原则:差异优先、上下文完整。人看报表的时候最关心的是「和上次比有什么不同」,所以差异清单放最前面,全量清单放后面。上下文完整的意思是,任何一个 IP 出现,旁边必须跟着 MAC、厂商、分类、最后在线时间,不能只有一个孤零零的地址。
输出的字段我固定成这几列:
| 字段 | 说明 | 为什么必须有 |
|---|---|---|
| IP | 当前地址 | 定位用 |
| MAC | 设备物理地址 | 唯一标识,判断是否换机 |
| 厂商 | 由 MAC 前缀推断 | 快速判断设备类型 |
| 分类 | 服务器、网络设备、终端、打印机等 | 决定处置策略 |
| 开放端口 | 数量加关键端口 | 暴露面概览 |
| 首次发现 | 日期 | 判断是否新入网 |
| 最后在线 | 时间戳 | 判断是否还在用 |
只输出 IP 和端口的报表是没用的,因为下次你还得重新判断这是什么东西。带上厂商和分类,隔三个月再看也能立刻想起来。
5. 一次完整的资产盘点实操记录
5.1 前置准备:权限、位置与授权
动手之前有三件事必须先确认,任何一件没做好都可能白干或者惹麻烦。
第一是授权。我经手的每个网络,扫描前都会有一份书面确认,说明扫描范围、时间窗口、执行人和回滚方案。这不是形式主义,是责任边界。有一次我在一个客户环境里扫到一半,对方的安全设备告警了,因为有这份确认,五分钟就解释清楚了;没有的话,那天可能就得收拾东西走人。
第二是网络位置。扫描源放在哪里,直接决定你能看到什么。放在核心交换机旁边,能看到所有网段但 ARP 全部失效;放在接入层某个 VLAN 里,本 VLAN 内 ARP 可用,跨 VLAN 只能靠三层探测。我的常规做法是在每个需要精细盘点的网段里各放一个探测点,用同一份程序跑,结果汇总。这一步多花的时间,远少于「扫完之后发现半个网段都是空的」的返工成本。
第三是权限。ARP 扫描需要构造原始报文,普通用户权限跑不了。Linux 下需要相应能力,Windows 下需要管理员身份。这一点没法绕,只能老实提权。如果真的拿不到,就退回到纯 TCP 探测方案,精度会降一些但能用。
5.2 第一轮:存活发现与基础信息采集
这一轮的目标很明确:拿到「谁在线」的名单。用 ARP 做本网段,用 ICMP 和 TCP 兜底跨网段。
from scapy.all import ARP, Ether, srp def arp_sweep(cidr, timeout=2, retries=2): pkt = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=cidr) answered, _ = srp(pkt, timeout=timeout, retry=retries, verbose=False) return [{"ip": r.psrc, "mac": r.hwsrc.lower()} for _, r in answered]timeout=2和retries=2是实测出来的。超时设 1 秒,在负载较高的网络里会漏掉 5% 左右的响应;设成 3 秒,耗时翻三倍收益却很小。2 秒配两次重试,是一个能覆盖绝大部分场景的平衡点。重试很重要,因为广播包在无线网络里丢包率不低,单次发送漏掉的概率明显高于两次。
跑完之后我会先做一次合理性检查:扫出的设备数 × 每台平均端口数,估算第二轮的探测总量;同时看看 MAC 厂商分布,如果出现大量同一厂商的陌生设备,先停下来确认是不是某个业务系统批量上线的,别急着一头扎进去。
5.3 第二轮:端口探测与指纹识别
拿到存活清单后,进入端口探测。我的端口清单分了三组,按需启用:
| 分组 | 端口 | 用途 |
|---|---|---|
| 基础组 | 22, 23, 80, 443, 445, 3389, 8080 | 快速判断设备类型和管理方式 |
| 服务组 | 21, 25, 110, 143, 3306, 5432, 6379, 27017, 9200 | 发现数据库和中间件 |
| 扩展组 | 161, 554, 9100, 515, 631, 9999 | 网络设备、监控、打印类 |
日常盘点用基础组加服务组,大概 15 个端口,扫 /24 网段实测 8 秒内出结果。需要深度盘点时再开扩展组。
指纹识别在第一轮端口结果出来后立即执行,对命中的端口做 banner 抓取和规则匹配。这里有个优化点:同厂商的规则合并匹配,不要对每个端口都跑一遍全量规则表,而是先用端口号索引出候选规则,再做匹配。这个改动把指纹阶段从 3 秒压到 0.4 秒,规则表越大收益越明显。
5.4 结果校验:人工复核不可省略
自动化结果永远需要人过一遍,尤其是第一次跑某个网段。我的复核清单是这样的:
先看新出现的设备。挨个对 MAC 厂商和开放端口,判断是正常新入网还是扫描误差。曾经有一次扫出十几台「新设备」,MAC 前缀都是同一个虚拟化厂商,后来确认是某业务系统做了一次批量扩容,属于正常变更——但如果当时直接把这批设备录进资产库而不标注来源,半年后又是一笔糊涂账。
再看消失的设备。连续三次没扫到的,标记为待确认,然后手动 ping 一次、查一下交换机端口状态,确认是真的下线了还是策略变了。有些设备会主动屏蔽扫描,比如装了某种防护软件的终端,这时候要把它加入「扫描豁免清单」,而不是让它在报表里反复出现。
最后看端口变化。某台服务器突然多了一个 3389,这是需要立刻确认的——是有人开了远程桌面,还是新装了软件。这类变化比设备上下线更值得关注,因为它是暴露面变化的直接信号。
5.5 定时任务与变更提醒
一次盘点只是快照,价值在时间序列里。我把扫描做成每天凌晨跑一次,结果入库后自动比对,产生变更清单。
# 定时任务示例,每天凌晨 2 点执行 0 2 * * * cd /opt/ipscanner && python main.py --config config.yaml --quiet >> /var/log/ipscan.log 2>&1时间选在凌晨是有讲究的。白天扫描会和业务抢资源,而且很多设备在业务高峰期响应变慢,误判率上升。凌晨两点这个时间点,大部分办公设备关机、业务系统进入低负载,扫描结果的「在线」状态最稳定,也最能反映设备的真实存在情况。当然,如果你关心的是「业务时段有哪些设备在线」,那就得反过来,在白天扫。这是需求决定的,没有标准答案。
变更提醒我走的是保守策略:只有三类事件触发通知——新增设备、关键端口变化(管理类端口和数据库端口)、已知设备换了 IP。设备离线不单独通知,只在日报里体现。这个策略是为了对抗告警疲劳,什么都报等于什么都没报。实际运行下来,每天的告警量在两三条,每一条都值得看一眼。
6. 常见问题与排查实录
6.1 扫不到设备:从四个方向查
这是最高频的问题,我把它拆成四条排查路径,按概率排序。
第一,跨网段用了 ARP。这是最常见的低级错误,症状是结果完全为空或者只有一个网关。判断方法很简单:看扫描源的 IP 和目标网段是否同网段同掩码,不同就换 ICMP/TCP 探测。
第二,目标屏蔽了探测。症状是部分设备扫不到,但你知道它在。验证方法是手动从别的机器 ping 一下、或者用业务访问确认它活着,然后换探测方式。很多服务器会禁用 ICMP,这时候要靠 TCP 端口探测兜底。
第三,超时太短。症状是结果不稳定,同样的参数跑两次结果不一样。验证方法把超时翻倍再跑一遍,如果结果变多,就是超时问题。这种情况在跨网段、经过多跳、或者目标负载高的时候特别明显。
第四,探测源被拦截。症状是第一次能扫到,后面全空。这多半是目标的安全策略把你的源 IP 拉黑了。处理方法是在允许的范围内调整扫描策略,或者提前把探测源加入白名单——这一步在很多环境里是必须的,别等到被封了才想起来。
6.2 结果误报:幽灵设备从哪来
误报的常见来源有三个,每个的应对方式不一样。
代理 ARP 导致的 MAC 复用。前面提过,虚拟化平台上很常见。应对方式是在结果聚合阶段按 MAC 分组,一个 MAC 对应超过 3 个 IP 的单独标记。
负载均衡设备的前端响应。一个 VIP 后面挂着十几台真实服务器,扫描时只有 VIP 响应,你会以为那是一台机器。这种情况没法从扫描结果本身分辨,只能靠资产台账和历史记录交叉验证。
服务端的通用 banner。很多中间件会对未匹配的请求返回默认页面,你抓到的 banner 其实是通用错误页,不是服务标识。应对方式是提高指纹规则的严格度,只匹配确定性的特征串,模糊匹配宁可放弃也不要乱打标签。我一开始的规则表很激进,结果把三十多台设备全标成了「未知 Web 服务」,后来砍掉一半规则,准确率反而上去了。
6.3 扫描把网络打慢了:三个止血手段
这个问题一旦出现必须立刻处理,因为它影响的是真实业务。
立刻降并发。配置文件里的并发数改成 50,重启任务。这是最快的止血手段,通常几秒内就能看到效果。
缩小端口范围。如果是在做全端口扫描,立刻切回基础端口组。全端口扫描是 65535 个探测乘以主机数,量级和基础组完全不是一个概念。
加批次间隔。把批次之间的sleep从 0.05 提到 0.5,整体耗时增加但网络压力大幅下降。
事后要认真复盘,通常是这几个原因之一:并发设太高、扫到了不该扫的网段(比如存储网络,那上面的设备对额外流量极其敏感)、或者是扫描源所在的链路本身就是瓶颈。我遇到过最坑的一次是扫描源接在一个百兆的老交换机上,程序参数完全正常,但出口带宽只有 100M,一跑就拥塞。
6.4 授权与合规边界:必须写清楚的一条
这一段我放在最后,但重要性排第一。
网络扫描的行为本质是「主动向目标发送探测报文」,它在很多环境里会被安全设备识别为攻击行为并触发告警、封禁甚至追责。所以每次动手之前的检查清单是:是否有明确授权、范围是否写清、时间窗口是否约定、是否有回滚方案、紧急联系人是否畅通。
具体到操作层面,有几条硬规矩我一直遵守:只扫授权范围内的地址,一个都不多;不扫网关和核心设备的全部端口,只做存活确认;不对任何设备做口令尝试、漏洞利用、配置修改;扫描结果只用于资产梳理,不外传、不公开。
还有一条容易被忽略:扫描日志本身就是敏感数据。它完整描述了你的网络暴露面,落到不该拿到的人手里风险很大。所以日志和数据库文件我都会做访问控制,定期归档,离职交接时明确处理方式。
7. 长期维护中我形成的几个习惯
7.1 数据比工具重要
这句话是我踩了两年坑才真正理解的。工具可以换、代码可以重写,但资产库一旦断了历史,重建成本极高。所以我把数据库的备份做成了无条件动作:每次扫描前自动备份一份,保留最近三十份,按月归档一份长期版本。
资产库的价值在于连续性。你能回答「这台机器是什么时候入网的、换过几次 IP、端口什么时候变的」这几个问题,排障效率会有质的提升。反过来,如果每次都是推倒重来,那你手里永远只有一张当天的快照,跟直接敲命令没什么区别。
为了保住连续性,主键设计要经得起考验。我现在的做法是 MAC 为主键,但对没有 MAC 的场景(跨网段扫描拿不到 MAC)用「IP + 主机名 + 首次发现时间」做复合键兜底,并且在报表里明确标注「身份置信度」。低置信度的记录不参与自动变更判定,只做展示。这个设计避免了很多误判。
7.2 值得往下做的几个扩展方向
工具跑顺之后,我陆续加了几个小功能,投入产出比都不错。
第一个是端口基线与偏离检测。给每台设备记录一个「端口基线」,也就是稳定运行一段时间后的端口集合。之后每次扫描和基线比对,出现新增端口就标记出来。这个功能在发现「有人偷偷开了个服务」这类问题上极其有效,比人工看报表快得多。
第二个是网段画像。把每个网段的设备类型分布、平均开放端口数、在线率统计出来,形成一张网段级的画像。新设备接入时,如果它的特征和所在网段的画像明显不符(比如服务器网段里出现了一台打印机),自动提醒。这个思路是从异常检测借过来的,实现简单但很实用。
第三个是结果导出成标准格式,方便和其他系统对接。我做了一个 CSV 和一个 JSON 两种输出,JSON 里带完整字段,CSV 只带核心字段给非技术同事看。格式定好之后,别的系统想消费这份数据就不用再改代码。
最后分享一个我自己的体会:这类工具最容易犯的错是「追求功能多」。我最初的版本塞了漏洞扫描、弱口令检测、拓扑发现,结果每个都做得半吊子,反而把最核心的资产盘点做砸了。后来砍到只剩发现、识别、沉淀三件事,每个做到可靠,工具的可用性反而上了一个台阶。把一个窄需求做到不会出错,比把十个宽需求做到勉强能用,价值高得多。