Nmap 扫描引擎核心函数深度解析:从批次管理到主机调度
前言
在网络扫描工具的设计中,如何高效地管理待扫描主机、合理分配扫描批次、平衡扫描效率与用户体验,是工程师们需要精心设计的核心问题。本文将深入剖析 Nmap 扫描引擎中的三个关键函数——determineScanGroupSize、nexthost 和 refresh_hostbatch,揭示它们如何协同工作,构建出一个高效、灵活、可控的扫描调度系统。
第一部分:智能批次大小计算器——determineScanGroupSize
函数概述
determineScanGroupSize 是网络扫描工具中动态计算单次扫描主机组大小的核心函数。它的核心作用是根据扫描类型(UDP/SCTP/TCP)、已扫描主机数、端口数量、扫描时序等级等参数,自适应调整每批扫描的主机数量(分组大小)。这个函数的设计体现了扫描工具在效率、用户体验和配置约束之间的精妙平衡。
设计理念
在实际的网络扫描场景中,不同的扫描协议有着截然不同的特性:
- UDP 扫描:UDP 是无连接协议,扫描单个端口的耗时相对较短,因此可以批量扫描更多主机以提升整体效率
- SCTP 扫描:与 UDP 类似,SCTP 的批量处理效率更高,无需动态调整
- TCP 扫描:TCP 是面向连接的协议,需要建立三次握手,扫描耗时与端口数量密切相关。端口越多,单主机扫描耗时越长,因此需要动态调整分组大小
完整代码实现
/**
* @brief 动态计算单次扫描的主机组大小(每批扫描的主机数量)
* @details 根据扫描类型(UDP/SCTP/TCP)、已扫描主机数、端口数量、时序等级等参数自适应调整分组大小,
* 优先保证TCP扫描的首批快速结果,同时遵守用户配置的上下限和最大扫描总数限制
*
* @param hosts_scanned_so_far 已扫描的主机数量(用于首批/次批分组大小的特殊调整)
* @param ports 扫描端口列表结构体(包含TCP端口数量等关键信息)
* @return int 最终确定的每批扫描主机数量(分组大小)
* @note 1. 不同协议的默认分组大小不同(UDP/SCTP=128,TCP动态计算);
* 2. TCP扫描会优先让首批分组返回快速结果,提升用户体验;
* 3. 最终分组大小会被限制在用户配置的min/max之间,且不超过剩余待扫描主机数
*/
int determineScanGroupSize(int hosts_scanned_so_far,
const struct scan_lists *ports) {
// 初始化默认分组大小为16(基础值,不同协议会覆盖)
int groupsize = 16;
// 根据扫描协议类型调整分组大小
if (o.UDPScan()) {
// UDP扫描:分组大小固定为128(UDP无连接,可批量扫描更多主机)
groupsize = 128;
} else if (o.SCTPScan()) {
// SCTP扫描:分组大小固定为128(类似UDP,批量扫描效率更高)
groupsize = 128;
} else if (o.TCPScan()) {
/* TCP扫描:动态计算分组大小
核心逻辑:1024 / TCP端口数量(至少为1),且不小于64
解释:端口越多,单主机扫描耗时越长,分组大小越小(避免单批耗时过久);
端口越少,分组大小越大(提升批量扫描效率),最低保障64 */
groupsize = MAX(1024 / (ports->tcp_count ? ports->tcp_count : 1), 64);
/* 特殊优化:TCP端口数>1000且时序等级≤4时,优先返回首批扫描结果 */
if (ports->tcp_count > 1000 && o.timing_level <= 4) {
// 快速分组大小:时序等级4时为8,其他≤4时为4
int quickgroupsz = 4;
if (o.timing_level == 4)
quickgroupsz = 8;
// 首次扫描(已扫描数=0):强制使用快速分组大小,优先返回首批结果
if (hosts_scanned_so_far == 0)
groupsize = quickgroupsz;
// 次批扫描(已扫描数=快速分组大小)且当前分组大小>2倍快速值:
// 调整分组大小,对齐网络边界(如/24网段),抵消首批快速扫描的影响
else if (hosts_scanned_so_far == quickgroupsz &&
groupsize > quickgroupsz * 2)
groupsize -= quickgroupsz;
}
}
/* 将分组大小限制在用户配置的[minHostGroupSz, maxHostGroupSz]范围内
box函数作用:若groupsize < min则返回min,>max则返回max,否则返回原值 */
groupsize = box(o.minHostGroupSz(), o.maxHostGroupSz(), groupsize);
/* 最终校验:若剩余待扫描主机数 < 当前分组大小,调整为剩余数量
避免扫描超过用户指定的最大IP数(尤其是随机生成IP的场景) */
if (o.max_ips_to_scan && (o.max_ips_to_scan – hosts_scanned_so_far) < (unsigned int)groupsize)
groupsize = o.max_ips_to_scan – hosts_scanned_so_far;
// 返回最终确定的分组大小
return groupsize;
}
核心逻辑深度解析
1. 不同协议的分组大小设计策略
| UDP | 固定128 | UDP是无连接协议,扫描单端口耗时短,批量扫描更多主机可提升效率 |
| SCTP | 固定128 | 类似UDP,SCTP扫描的批量处理效率更高,无需动态调整 |
| TCP | 动态计算:MAX(1024/端口数, 64) | TCP是面向连接协议,端口越多→单主机扫描耗时越长→分组大小越小(避免单批卡住);端口越少→分组越大(提升效率),最低保障64 |
TCP 动态计算公式的深层含义:
公式 MAX(1024 / 端口数, 64) 的设计非常巧妙:
- 分子 1024:这是一个经验值,代表"理想情况下单批扫描的总端口数"。无论端口数量多少,都希望单批扫描的总端口数保持在 1024 左右,这样可以保证扫描的稳定性
- 除以端口数:端口越多,单主机扫描耗时越长,因此需要减少每批的主机数量,避免单批扫描时间过长
- 下限 64:即使端口数量很少(比如只有 1 个端口),每批至少扫描 64 台主机,保证批量扫描的效率优势
2. TCP 扫描的"快速首批结果"优化(用户体验的核心亮点)
当 TCP 端口数 > 1000(扫描耗时久)且时序等级 ≤ 4(非极速扫描)时,函数会执行一个特殊的优化逻辑:
首次扫描(已扫描数 = 0):
- 强制使用小分组(时序等级 4 时为 8,其他 ≤ 4 时为 4)
- 目的:优先返回少量主机的扫描结果,避免用户长时间等待第一批结果
- 这是一种"快速反馈"的设计理念,让用户尽早看到扫描进展
次批扫描(已扫描数 = 快速分组大小):
- 如果当前分组大小 > 2 倍快速值,则减去快速分组大小
- 目的:抵消首批小分组的影响,让后续分组对齐网络边界
- 例如:正常分组大小是 64,首批用了 4,次批就调整为 60,这样 4 + 60 = 64,正好对齐网络边界(如 /24 网段的 256 台主机)
为什么需要对齐网络边界?
网络扫描中,很多目标是以网段形式指定的(如 192.168.1.0/24)。如果分组大小不能整除网段大小,会导致跨网段的扫描,可能影响扫描的连续性和效率。通过这种调整,可以保证扫描批次更好地对齐网络拓扑结构。
3. 关键辅助函数与变量说明
| box(min, max, val) | 范围限制函数:保证val在[min, max]之间,是扫描工具中常用的参数约束函数 |
| o.timing_level | 扫描时序等级(0-5):等级越高扫描越快,等级4时快速分组大小从4提升到8 |
| o.max_ips_to_scan | 用户指定的最大扫描IP数:避免超量扫描(尤其是随机生成IP的场景) |
| ports->tcp_count | 待扫描的TCP端口数量:核心影响TCP扫描的分组大小 |
4. 实际计算示例
让我们通过一个具体的场景来理解这个函数的工作流程:
场景设定:
- TCP 扫描
- 端口数 = 2000
- 时序等级 = 3
- minHostGroupSz = 4
- maxHostGroupSz = 1024
- 已扫描数 = 0
- max_ips_to_scan = 1000
首次扫描计算过程:
初始 TCP 分组大小计算:
groupsize = MAX(1024 / 2000, 64) = MAX(0.512, 64) = 64
因为端口数很多,所以初始分组大小取下限 64
触发快速首批结果优化:
- 端口数 2000 > 1000 ✓
- 时序等级 3 ≤ 4 ✓
- 因此 quickgroupsz = 4
首次扫描特殊处理:
- 已扫描数 = 0
- 强制 groupsize = 4(首批快速结果)
用户配置范围限制:
groupsize = box(4, 1024, 4) = 4
4 在 [4, 1024] 范围内,保持不变
剩余扫描数校验:
- 剩余待扫描数 = 1000 – 0 = 1000
- 1000 > 4,无需调整
- 最终返回:4
次批扫描计算过程(已扫描数 = 4):
初始 TCP 分组大小计算:
groupsize = MAX(1024 / 2000, 64) = 64
次批扫描特殊处理:
- 已扫描数 = 4(等于 quickgroupsz)
- 当前分组大小 64 > 4 × 2 = 8 ✓
- 调整:groupsize = 64 – 4 = 60
用户配置范围限制:
groupsize = box(4, 1024, 60) = 60
剩余扫描数校验:
- 剩余待扫描数 = 1000 – 4 = 996
- 996 > 60,无需调整
- 最终返回:60
通过这个示例,我们可以看到:
- 首批只扫描 4 台主机,快速返回结果
- 次批扫描 60 台主机,4 + 60 = 64,正好对齐正常分组大小
- 后续批次将保持 60 台主机的分组大小
5. 边界条件处理
函数在设计时充分考虑了各种边界情况:
剩余主机数不足:
- 如果用户指定了 max_ips_to_scan,且剩余待扫描数 < 当前分组大小
- 直接将分组大小调整为剩余数量
- 避免扫描超过用户指定的最大 IP 数
用户配置约束:
- 所有场景下的分组大小都会被限制在 minHostGroupSz 和 maxHostGroupSz 之间
- 保证符合用户的配置预期
端口数为 0 的处理:
- 使用三元运算符 ports->tcp_count ? ports->tcp_count : 1 避免除以 0
- 即使没有 TCP 端口,也能正常计算分组大小
总结
determineScanGroupSize 函数体现了扫描工具设计的几个核心原则:
- TCP 扫描优先返回首批快速结果,提升用户体验
- UDP/SCTP 固定大分组提升效率
- 最终分组大小受用户配置和剩余扫描数约束
- TCP 分组大小与端口数成反比(端口越多分组越小)
- 首批扫描强制小分组,次批调整对齐网络边界
简单来说,这个函数是扫描工具的"批次大小智能调节器"——不同协议用不同策略,TCP 扫描优先让用户快速看到首批结果,同时不超配置限制。
第二部分:主机取数器——nexthost
函数概述
nexthost 是网络扫描工具中获取下一个待扫描主机的核心函数。它的核心逻辑是:先检查当前批次的主机是否已全部取完,若取完则刷新批次缓存(加载新一批待扫描主机),再从批次缓存中按顺序取出下一个主机;若批次缓存无可用主机,则返回 NULL。
这个函数是连接"主机批次管理(HostGroupState)"和"实际扫描逻辑"的关键桥梁,是扫描主循环的核心入口。
设计理念
在批量扫描系统中,如何高效地管理待扫描主机是一个重要问题。nexthost 函数采用了"批次缓存"的设计模式:
这种设计的优势:
- 减少频繁的主机生成和筛选操作
- 提高内存访问的局部性
- 简化扫描主循环的逻辑
- 支持批次的随机化和预处理
完整代码实现
/**
* @brief 获取下一个待扫描的主机(Target实例)
* @details 从HostGroupState的批次缓存(hostbatch)中按顺序取主机,若当前批次已取完则刷新缓存,
* 刷新后仍无可用主机则返回NULL,是扫描循环中获取目标主机的核心入口
*
* @param hs 主机组状态管理器(包含批次缓存、随机化、预扫描等状态)
* @param exclude_group 排除的地址组(扫描时需跳过的IP/网段)
* @param ports 扫描端口列表(用于计算批次大小、端口随机化等)
* @param pingtype ping扫描类型(如ICMP echo、TCP SYN ping等,用于预扫描筛选存活主机)
* @return Target* 成功返回下一个待扫描的主机实例;无可用主机返回NULL
* @note 1. 依赖refresh_hostbatch刷新批次缓存,该函数会加载新一批主机到hs->hostbatch;
* 2. hs->next_batch_no是当前批次的取数指针,每取一个主机自增1;
* 3. 两次检查hs->next_batch_no是为了处理refresh_hostbatch刷新失败的情况
*/
Target *nexthost(HostGroupState *hs, struct addrset *exclude_group,
const struct scan_lists *ports, int pingtype) {
// 第一步:检查当前批次是否已取完(取数指针 ≥ 当前批次大小)
if (hs->next_batch_no >= hs->current_batch_sz) {
// 批次已空,刷新批次缓存:加载新一批待扫描主机到hs->hostbatch
// 刷新逻辑包含:计算批次大小、生成/筛选主机、随机化、排除黑名单等
refresh_hostbatch(hs, exclude_group, ports, pingtype);
}
// 第二步:再次检查批次是否为空(处理refresh_hostbatch刷新失败/无新主机的情况)
if (hs->next_batch_no >= hs->current_batch_sz) {
// 无可用主机,返回NULL(扫描结束)
return NULL;
}
// 第三步:返回当前批次的下一个主机,并将取数指针自增(指向下一个主机)
return hs->hostbatch[hs->next_batch_no++];
}
核心逻辑深度解析
1. 核心变量与执行流程
关键变量说明:
| hs->next_batch_no | 批次取数指针(初始0):标记当前批次中"下一个要取的主机下标",每取一个主机自增1; |
| hs->current_batch_sz | 当前批次的有效主机数量:refresh_hostbatch 刷新后会更新该值; |
| hs->hostbatch | 批次缓存数组:存储当前批的待扫描主机指针(Target*); |
执行流程示例:
假设 current_batch_sz = 4(批次有 4 台主机):
首次调用 nexthost:
- next_batch_no = 0 < 4 → 不触发刷新
- 直接返回 hostbatch[0]
- next_batch_no 变为 1
第二次调用:
- next_batch_no = 1 < 4 → 不触发刷新
- 返回 hostbatch[1]
- next_batch_no 变为 2
第三次调用:
- next_batch_no = 2 < 4 → 不触发刷新
- 返回 hostbatch[2]
- next_batch_no 变为 3
第四次调用:
- next_batch_no = 3 < 4 → 不触发刷新
- 返回 hostbatch[3]
- next_batch_no 变为 4
第五次调用:
- next_batch_no = 4 ≥ 4 → 触发 refresh_hostbatch 刷新批次
- 假设新批次 current_batch_sz = 6
- 刷新后 next_batch_no = 4 < 6 → 返回 hostbatch[4]
- next_batch_no 变为 5
若 refresh_hostbatch 刷新后 current_batch_sz = 4:
- 第二次检查 next_batch_no = 4 ≥ 4 → 返回 NULL(扫描结束)
2. 两次检查 next_batch_no 的必要性
函数中两次检查 hs->next_batch_no >= hs->current_batch_sz 的设计非常巧妙:
第一次检查:
- 目的:触发批次刷新
- 场景:当前批次已取完,需要加载新一批主机
- 操作:调用 refresh_hostbatch 刷新批次缓存
第二次检查:
- 目的:处理"刷新失败"的边界情况
- 场景:
- 已无新主机可扫描(所有目标都已扫描完成)
- 所有主机都被排除(在黑名单中)
- 网段表达式已耗尽
- 操作:返回 NULL,通知扫描结束
为什么需要两次检查?
因为 refresh_hostbatch 可能会刷新失败(比如没有新主机),此时 current_batch_sz 可能仍然是 0 或小于 next_batch_no。如果没有第二次检查,就会返回 hostbatch 数组的越界元素,导致未定义行为。
3. refresh_hostbatch 的隐含逻辑(核心依赖)
nexthost 的核心能力完全依赖 refresh_hostbatch 函数,该函数会完成以下关键操作:
4. 典型使用场景(扫描主循环)
// 扫描主循环示例
HostGroupState *hs = new HostGroupState(100, 1, false, 0, argc, argv);
struct addrset *exclude = load_exclude_list("exclude.txt");
struct scan_lists *ports = load_scan_ports("80,443,22");
int pingtype = PING_TCP_SYN;
Target *target;
// 循环获取下一个主机,直到返回NULL
while ((target = nexthost(hs, exclude, ports, pingtype)) != NULL) {
// 扫描该主机的指定端口
scan_host(target, ports, pingtype);
// 释放主机资源(若由nexthost分配)
delete target;
}
// 清理资源
free_exclude_list(exclude);
free_scan_ports(ports);
delete hs;
这个示例展示了 nexthost 在扫描主循环中的典型用法:
- 初始化 HostGroupState、排除列表、端口列表
- 使用 while 循环持续获取主机
- 对每个主机执行扫描
- 扫描完成后释放资源
- 最后清理所有资源
总结
nexthost 函数体现了扫描工具设计的几个核心原则:
- 两次检查批次指针避免越界
- 依赖 refresh_hostbatch 完成主机加载/筛选/随机化
- 简洁的接口设计,隐藏复杂的批次管理逻辑
- HostGroupState 的批次管理
- determineScanGroupSize 的批次大小计算
- refresh_hostbatch 的批次刷新
简单来说,这个函数是扫描工具的"主机取数器"——一批一批地拿主机,拿完一批自动补新一批,没新的就结束,是连接主机管理和实际扫描的核心纽带。
第三部分:主机批次刷新与目标生成——refresh_hostbatch 及相关函数
整体架构概述
这组函数构成了网络扫描工具中"主机批次刷新+目标主机生成"的完整链路。核心作用是:当 nexthost 检测到当前批次主机已取完时,refresh_hostbatch 会触发新批次主机的加载——从目标表达式/随机 IP 中生成主机、过滤黑名单、分组校验、随机化、预扫描(ARP/ICMP ping)、DNS 解析,最终填充到 hostbatch 批次缓存中。
而 next_target/get_next_host/TargetGroup::get_next_host 则是生成单个目标主机的底层支撑,负责从网段表达式中逐个生成 IP、排除黑名单、处理续扫逻辑。
函数调用链路
#mermaid-svg-gBIck8gk6OTuyNJQ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gBIck8gk6OTuyNJQ .error-icon{fill:#552222;}#mermaid-svg-gBIck8gk6OTuyNJQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gBIck8gk6OTuyNJQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gBIck8gk6OTuyNJQ .marker.cross{stroke:#333333;}#mermaid-svg-gBIck8gk6OTuyNJQ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gBIck8gk6OTuyNJQ p{margin:0;}#mermaid-svg-gBIck8gk6OTuyNJQ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster-label text{fill:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster-label span{color:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster-label span p{background-color:transparent;}#mermaid-svg-gBIck8gk6OTuyNJQ .label text,#mermaid-svg-gBIck8gk6OTuyNJQ span{fill:#333;color:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .node rect,#mermaid-svg-gBIck8gk6OTuyNJQ .node circle,#mermaid-svg-gBIck8gk6OTuyNJQ .node ellipse,#mermaid-svg-gBIck8gk6OTuyNJQ .node polygon,#mermaid-svg-gBIck8gk6OTuyNJQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gBIck8gk6OTuyNJQ .rough-node .label text,#mermaid-svg-gBIck8gk6OTuyNJQ .node .label text,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape .label,#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-gBIck8gk6OTuyNJQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gBIck8gk6OTuyNJQ .rough-node .label,#mermaid-svg-gBIck8gk6OTuyNJQ .node .label,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape .label,#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape .label{text-align:center;}#mermaid-svg-gBIck8gk6OTuyNJQ .node.clickable{cursor:pointer;}#mermaid-svg-gBIck8gk6OTuyNJQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gBIck8gk6OTuyNJQ .arrowheadPath{fill:#333333;}#mermaid-svg-gBIck8gk6OTuyNJQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gBIck8gk6OTuyNJQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gBIck8gk6OTuyNJQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gBIck8gk6OTuyNJQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gBIck8gk6OTuyNJQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gBIck8gk6OTuyNJQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster text{fill:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster span{color:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gBIck8gk6OTuyNJQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape p,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape rect,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gBIck8gk6OTuyNJQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gBIck8gk6OTuyNJQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gBIck8gk6OTuyNJQ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
nexthost触发refresh_hostbatch
重置批次状态+处理延迟主机
调用next_target加载新主机
优先取undeferred列表的延迟主机
无延迟主机则调用get_next_host生成新IP
get_next_host调用TargetGroup::get_next_host从网段生成IP
过滤黑名单,返回有效IP
处理续扫逻辑,跳过已扫描IP
setup_target初始化Target实例
加入排除列表去重
校验主机是否需要单独分组(需要则延迟)
将有效主机加入hostbatch批次缓存
填满批次后随机化主机顺序
ARP ping/批量IP ping检测存活
批量DNS解析
hostbatch填充完成,返回nexthost
核心函数详解
1. refresh_hostbatch – 批次刷新的核心逻辑
/**
* @brief 刷新主机批次缓存(核心逻辑)
* @details 清空旧批次→加载新一批主机→分组校验→随机化→ARP/MassPing预扫描→DNS解析,
* 最终将有效主机填充到hostbatch数组,是nexthost的核心依赖
*/
static void refresh_hostbatch(HostGroupState *hs, struct addrset *exclude_group,
const struct scan_lists *ports, int pingtype) {
int i;
bool arpping_done = false; // 标记是否已完成ARP ping
struct timeval now; // 当前时间(用于超时判断)
// 1. 重置批次状态:清空当前批次大小和取数指针
hs->current_batch_sz = hs->next_batch_no = 0;
// 2. 将延迟处理的主机移到undeferred列表(优先处理)
hs->undefer();
// 3. 循环加载主机,直到填满批次缓存(max_batch_sz)
while (hs->current_batch_sz < hs->max_batch_sz) {
Target *t = NULL;
// 3.1 获取下一个目标主机(优先处理undeferred,再生成新主机)
t = next_target(hs, exclude_group, ports, pingtype);
if (t == NULL) break; // 无新主机,终止加载
// 3.2 校验:该主机是否需要单独分组(如跨网段/跨网卡)
if (target_needs_new_hostgroup(hs->hostbatch, hs->current_batch_sz, t)) {
// 需要单独分组:放入延迟列表,继续加载下一个
if (hs->defer(t)) continue;
// 延迟失败,终止加载
else break;
}
// 3.3 设置诱饵IP(反检测:轮询设置 decoys 诱饵地址)
o.decoys[o.decoyturn] = t->source();
// 3.4 将主机加入批次缓存,批次大小+1
hs->hostbatch[hs->current_batch_sz++] = t;
}
// 无主机加载成功,直接返回
if (hs->current_batch_sz == 0) return;
// 4. 随机化批次内的主机顺序(若开启随机化)
if (hs->randomize) {
hoststructfry(hs->hostbatch, hs->current_batch_sz); // 类似shortfry的主机洗牌函数
}
// 5. ARP ping/二层MAC地址预处理(仅以太网网卡且非强制IP发送时)
gettimeofday(&now, NULL);
Target *current_target = hs->hostbatch[0];
if (current_target->ifType() == devt_ethernet && o.sendpref != PACKET_SEND_IP_STRONG) {
// 打开网卡的以太网缓存(获取数据链路层信息)
netutil_eth_t *eth = eth_open_cached(current_target->deviceName());
if (DLT_EN10MB == netutil_eth_datalink(eth)) { // 确认是以太网数据链路层
// 5.1 对直连主机执行ARP/ND ping(快速检测存活)
if (current_target->directlyConnected() && o.implicitARPPing) {
arpping(hs->hostbatch, hs->current_batch_sz);
arpping_done = true;
}
// 5.2 若需要二层发送(ETH),为每个主机设置下一跳MAC地址
if ((o.sendpref & PACKET_SEND_ETH)) {
for (i=0; i < hs->current_batch_sz; i++) {
current_target = hs->hostbatch[i];
// 跳过已标记为下线/超时的主机
if (!(current_target->flags & HOST_DOWN) && !current_target->timedOut(&now)) {
// 获取MAC地址失败:标记为主机下线,原因是无路由
if (!setTargetNextHopMAC(current_target)) {
error("%s: Failed to determine dst MAC address for target %s",
__func__, current_target->NameIP());
current_target->flags = HOST_DOWN;
current_target->reason.reason_id = ER_NOROUTE;
}
}
}
}
}
}
// 6. IP层批量Ping(预扫描:检测主机存活)
// 场景1:无需ping/已ARP ping,或回环网卡 → 直接标记为上线
if ((pingtype == PINGTYPE_NONE && !arpping_done) || current_target->ifType() == devt_loopback) {
for (i=0; i < hs->current_batch_sz; i++) {
current_target = hs->hostbatch[i];
if (!(current_target->flags & HOST_DOWN || current_target->timedOut(&now))) {
initialize_timeout_info(¤t_target->to); // 初始化超时信息
current_target->flags |= HOST_UP; // 标记为主机上线
// 设置上线原因:用户指定/本地回环
current_target->reason.reason_id = (pingtype == PINGTYPE_NONE && !arpping_done) ? ER_USER : ER_LOCALHOST;
}
}
}
// 场景2:未执行ARP ping → 执行IP层批量ping(如ICMP/TCP SYN ping)
else if (!arpping_done) {
massping(hs->hostbatch, hs->current_batch_sz, ports);
}
// 7. 批量反向DNS解析(若未禁用解析)
if (!o.noresolve) {
nmap_mass_rdns(hs->hostbatch, hs->current_batch_sz);
}
}
核心步骤解析:
步骤 1-2:批次状态重置与延迟主机处理
- 清空当前批次,准备加载新主机
- 将上一批次延迟的主机移到 undeferred 列表,优先处理
步骤 3:循环加载主机
- 调用 next_target 获取下一个主机
- 检查是否需要单独分组(如跨网段、跨网卡)
- 如果需要单独分组,放入延迟列表,继续加载下一个
- 否则,将主机加入批次缓存
步骤 4:随机化
- 如果开启了随机化,对批次内的主机进行洗牌
- 目的:避免固定的扫描顺序,增加反检测能力
步骤 5:ARP ping 与 MAC 地址预处理
- 对以太网直连主机执行 ARP ping,快速检测存活
- 为需要二层发送的主机设置下一跳 MAC 地址
- 如果 MAC 地址获取失败,标记主机为下线
步骤 6:IP 层批量 Ping
- 如果无需 ping 或已执行 ARP ping,直接标记为上线
- 否则,执行 IP 层批量 ping(ICMP/TCP SYN ping)
步骤 7:DNS 解析
- 如果未禁用 DNS 解析,批量反向解析主机名
2. next_target – 获取单个目标主机
/**
* @brief 获取单个目标主机(优先处理延迟列表,再生成新主机)
* @details 先处理延迟的主机→再从网段生成新IP→处理续扫逻辑→初始化Target实例→加入黑名单(去重)
*/
static Target *next_target(HostGroupState *hs, struct addrset *exclude_group,
const struct scan_lists *ports, int pingtype) {
struct sockaddr_storage ss; // 存储生成的IP地址
size_t sslen; // IP地址结构体长度
Target *t = NULL;
// 1. 优先处理上一批次延迟的主机(undeferred列表)
if (!hs->undeferred.empty()) {
t = hs->undeferred.front();
hs->undeferred.pop_front();
return t;
}
tryagain: // 循环生成新主机的标签(失败则重试)
// 2. 从网段表达式/随机IP生成下一个IP(排除黑名单)
if (!hs->get_next_host(&ss, &sslen, exclude_group)) {
return NULL; // 无新主机,返回NULL
}
// 断言:生成的IP地址族与用户指定的一致(如IPv4/IPv6)
assert(ss.ss_family == o.af());
// 3. 处理续扫逻辑:跳过已扫描到resume_ip的主机
if (o.resume_ip.ss_family != AF_UNSPEC) {
// 找到resume_ip,后续不再跳过
if (!sockaddr_storage_cmp(&o.resume_ip, &ss)) {
o.resume_ip.ss_family = AF_UNSPEC;
}
goto tryagain; // 跳过当前IP,继续生成下一个
}
// 4. 初始化Target实例(填充IP、端口、网卡等信息)
t = setup_target(hs, &ss, sslen, pingtype);
if (t == NULL) goto tryagain; // 初始化失败,重试
// 5. 去重:将该IP加入排除列表,避免重复扫描
if (o.unique) {
addrset_add_spec(exclude_group, t->targetipstr(), o.af(), 0);
}
return t;
}
核心逻辑解析:
优先处理延迟主机:
- 如果 undeferred 列表不为空,优先返回延迟的主机
- 这样可以保证扫描不遗漏任何主机
生成新主机:
- 调用 get_next_host 从网段表达式生成下一个 IP
- 如果生成失败(网段耗尽),返回 NULL
续扫逻辑:
- 如果用户指定了 resume_ip,跳过该 IP 之前的所有主机
- 找到 resume_ip 后,清除标记,后续不再跳过
初始化与去重:
- 调用 setup_target 初始化 Target 实例
- 如果开启了 unique 模式,将 IP 加入排除列表,避免重复扫描
3. HostGroupState::get_next_host – 生成下一个 IP 地址
/**
* @brief 生成下一个IP地址(控制总数+从网段生成+排除黑名单)
*/
bool HostGroupState::get_next_host(struct sockaddr_storage *ss, size_t *sslen, struct addrset *exclude_group) {
// 1. 校验:是否超过用户指定的最大扫描IP数
unsigned long num_queued = o.numhosts_scanned + current_batch_sz;
if (o.max_ips_to_scan > 0 && num_queued >= o.max_ips_to_scan) {
return false;
}
// 2. 循环生成IP,直到找到不在黑名单中的IP
do {
// 2.1 从当前网段表达式生成IP(无更多则加载下一个表达式)
while (current_group.get_next_host(ss, sslen) != 0) {
// 无更多表达式可加载,返回false
if (!current_group.load_expressions(this, o.af())) {
return false;
}
}
// 2.2 检查是否在排除列表中:不在则保留该IP,终止循环
if (!addrset_contains(exclude_group, (const struct sockaddr *) ss)) {
current_group.reject_last_host(); // 标记该IP为已分配
break;
}
} while (true);
return true; // 生成成功,返回true
}
核心逻辑解析:
总数控制:
- 检查是否超过用户指定的最大扫描 IP 数
- 如果超过,返回 false,停止生成
循环生成 IP:
- 从当前网段表达式生成 IP
- 如果网段耗尽,加载下一个表达式
- 如果所有表达式都耗尽,返回 false
黑名单过滤:
- 检查生成的 IP 是否在排除列表中
- 如果在黑名单中,继续生成下一个 IP
- 如果不在黑名单中,标记为已分配,返回成功
4. TargetGroup::get_next_host – 从网段表达式中生成下一个 IP
/**
* @brief 从网段表达式中生成下一个IP(底层核心)
* @details 遍历网段块(NetBlock),逐个生成IP,网段耗尽则删除并加载下一个
*/
int TargetGroup::get_next_host(struct sockaddr_storage *ss, size_t *sslen) {
// 遍历所有网段块(如192.168.1.0/24、8.8.8.0/24)
while (!netblocks.empty()) {
NetBlock *nb = netblocks.front(); // 取第一个网段块
// 从该网段生成下一个IP(成功则返回0,填充ss/sslen)
if (nb->next(ss, sslen)) {
return 0;
}
// 该网段IP已耗尽:删除网段块,继续下一个
netblocks.pop_front();
delete nb;
}
// 所有网段都已耗尽,返回-1
return –1;
}
核心逻辑解析:
遍历网段块:
- netblocks 是一个队列,存储所有待扫描的网段块
- 每个网段块可以是一个 IP 地址、一个网段(如 192.168.1.0/24)或一个 IP 范围
生成 IP:
- 调用 nb->next() 从当前网段块生成下一个 IP
- 如果成功,返回 0,填充 ss 和 sslen
- 如果失败(网段耗尽),删除该网段块,继续下一个
结束条件:
- 所有网段块都耗尽,返回 -1
关键细节深度解析
1. 主机生成的核心规则
优先级:
- 延迟主机(undeferred)> 新生成主机
- 这样可以保证扫描不遗漏任何主机
过滤:
- 生成的 IP 必须不在 exclude_group 黑名单中
- 黑名单可以包含单个 IP、网段或 IP 范围
总数控制:
- 不超过 o.max_ips_to_scan 用户指定的最大扫描数
- 避免扫描过多主机,浪费时间和资源
续扫:
- 跳过 o.resume_ip 之前的所有 IP
- 从指定 IP 开始扫描,支持中断后继续扫描
2. 预扫描(Ping)的分层逻辑
| 以太网直连主机 | ARP ping | 二层快速检测存活,无需IP层交互 |
| 回环网卡/无需ping | 直接标记上线 | 本地主机无需检测,提升效率 |
| 其他场景 | massping(ICMP/TCP SYN) | IP层批量检测存活,覆盖跨网段主机 |
为什么需要分层预扫描?
不同场景下,检测主机存活的最佳方式不同:
- ARP ping:对于以太网直连主机,ARP ping 是最快的方式,因为它工作在数据链路层,无需 IP 层交互
- 直接标记上线:对于本地回环网卡或用户明确指定无需 ping 的场景,直接标记为上线,提升效率
- massping:对于跨网段主机,需要使用 IP 层的 ping(ICMP 或 TCP SYN),覆盖范围更广
3. 关键边界处理
网段耗尽:
- TargetGroup::get_next_host 遍历完一个网段后删除该网段块
- 自动加载下一个表达式,继续生成 IP
MAC 地址获取失败:
- 标记主机为 HOST_DOWN
- 原因设置为 ER_NOROUTE(无路由)
- 避免后续扫描尝试连接不可达的主机
随机化:
- 批次填充完成后调用 hoststructfry 洗牌
- 避免固定的扫描顺序,增加反检测能力
去重:
- 开启 o.unique 时,扫描过的 IP 加入排除列表
- 永不重复扫描,避免浪费资源
4. 延迟主机机制
为什么需要延迟主机?
在某些情况下,主机需要单独分组:
- 跨网段:不同网段的主机可能需要不同的扫描策略
- 跨网卡:不同网卡的主机可能需要不同的路由
- 特殊配置:某些主机可能有特殊的扫描需求
延迟主机的工作流程:
总结
这组函数体现了扫描工具设计的几个核心原则:
核心链路:nexthost → refresh_hostbatch → next_target → get_next_host → TargetGroup::get_next_host,从"批次刷新"到"单个 IP 生成"形成完整的主机加载流程
设计亮点:
- 优先处理延迟主机,保证扫描不遗漏
- 分层预扫描(ARP/IP),兼顾效率和兼容性
- 随机化 + 去重 + 续扫,平衡反检测、准确性和用户需求
核心约束:
- 严格控制扫描总数
- 排除黑名单
- 匹配地址族
- 避免无效扫描
适用场景:网络扫描工具的核心主机调度逻辑,实现高效、灵活、可控的目标主机加载
简单来说,这组函数是扫描工具的"主机工厂"——从网段表达式中批量"生产"主机,经过过滤、预扫描、随机化等工序,最终打包成"批次"供扫描逻辑使用,兼顾效率、反检测和用户配置。
整体架构总结
通过深入分析这三个核心函数,我们可以看到 Nmap 扫描引擎的精妙设计:
1. 三层架构
┌─────────────────────────────────────────┐
│ nexthost – 主机取数器(用户接口层) │
├─────────────────────────────────────────┤
│ refresh_hostbatch – 批次刷新(管理层) │
├─────────────────────────────────────────┤
│ next_target/get_next_host – 主机生成 │
│ (底层实现层) │
└─────────────────────────────────────────┘
2. 核心设计原则
- 分层设计:每层负责不同的职责,职责清晰,易于维护
- 批量处理:通过批次缓存减少频繁的主机生成和筛选操作
- 智能调度:根据扫描类型、端口数量、时序等级等参数动态调整策略
- 用户体验:优先返回首批快速结果,减少用户等待时间
- 反检测:支持随机化、诱饵 IP 等反检测机制
- 容错性:完善的边界条件处理,避免崩溃和未定义行为
3. 性能优化
- ARP ping 优先:对以太网直连主机使用 ARP ping,快速检测存活
- 批量 DNS 解析:一次性解析批次内所有主机的 DNS,减少网络请求
- 批次随机化:避免固定的扫描顺序,增加反检测能力
- 延迟主机机制:优化跨网段、跨网卡主机的扫描效率
4. 扩展性
- 插件化设计:通过 pingtype 参数支持不同的 ping 扫描类型
- 配置灵活:支持用户配置最小/最大分组大小、最大扫描数等参数
- 协议支持:支持 TCP、UDP、SCTP 等多种扫描协议
- 地址族支持:同时支持 IPv4 和 IPv6
结语
Nmap 的扫描引擎设计充分体现了系统工程的精髓:在效率、用户体验、反检测、配置约束等多个维度之间找到最佳平衡点。通过深入理解这些核心函数,我们不仅可以更好地使用 Nmap,还可以学习到优秀的系统设计思想和编程技巧。
希望本文能够帮助读者深入理解网络扫描工具的核心原理,为后续的学习和实践打下坚实的基础。
参考资料:
- Nmap 源代码:https://github.com/nmap/nmap
- Nmap 官方文档:https://nmap.org/book/man.html
- 网络扫描技术原理与实践
作者注:本文基于 Nmap 7.98 版本的源代码进行分析,不同版本可能存在细微差异。如有疑问或建议,欢迎交流讨论。




