【Nmap 源码深度解析】nmap_main 函数核心机制详解(二)
模块化与分层将"环境初始化"“参数解析”“扫描执行”"结果输出"拆分为独立阶段每个阶段职责单一,逻辑清晰,易于维护和扩展效率优先分组扫描减少系统调用开销,批量探测提升速度端口随机化、常用端口前置,平衡扫描效率和隐蔽性预分配内存减少动态扩容开销兼容性与鲁棒性适配 Windows/Linux/WSL 等多平台处理信号、超时、内存泄漏等异常情况支持"恢复扫描",避免扫描中断后重新执行标准化输出XML 作
Nmap 源码深度解析:nmap_main 函数核心机制详解(二)
本文是 Nmap 源码分析系列的第二篇,将深入剖析
nmap_main函数的核心执行流程、关键数据结构以及网络地址解析机制。通过本文,你将全面理解 Nmap 如何从命令行参数到完成网络扫描的完整过程。
目录
一、nmap_main 函数整体执行流程
1.1 函数定位与核心目标
nmap_main 是 Nmap 程序的总控入口函数,承担了从"接收用户命令行参数"到"完成扫描、输出结果、释放资源"的全流程管理。其核心目标是:
将用户的扫描指令转化为具体的网络探测行为,并将结果以标准化格式输出
这个函数的设计体现了大型网络工具的典型架构模式:初始化 → 前置处理 → 核心扫描 → 后置收尾。
1.2 六阶段执行流程详解
整个 nmap_main 函数的执行流程可以清晰地划分为六个阶段,每个阶段都有明确的目标和核心操作。
阶段 1:系统与环境初始化(基础准备)
核心目标:适配操作系统环境、初始化基础组件,为后续扫描铺路。
关键操作详解:
-
系统兼容性处理
// 检测运行环境 #ifdef WIN32 // Windows 特定初始化 win_init(); // 加载 WinPcap 驱动、检查权限 #elif defined(__linux__) // Linux 特定初始化 if (is_wsl()) { // WSL 环境警告 log_write(LOG_STDOUT, "WARNING: You are running Nmap inside WSL. " "Some features may not work properly.\n"); } #endif // 初始化时区 tzset(); // 确保时间戳准确设计意图:Nmap 需要在多种操作系统上运行,不同平台的网络栈、权限模型、驱动加载方式都不同。通过条件编译和运行时检测,确保程序在各平台都能正常工作。
-
参数合法性校验
// 无参数时显示帮助信息 if (argc == 1) { print_usage(stdout); exit(0); } // 解析命令行参数 parse_options(argc, argv, &o);parse_options 的作用:
- 将用户输入(如
-sS、--script、目标 IP)填充到全局配置对象o中 - 验证参数组合的合法性(如
-sS和-sT不能同时使用) - 设置默认值(如未指定端口时使用常用端口)
- 将用户输入(如
-
基础组件初始化
// 初始化日志系统 log_init(o.debugging, o.verbose); // 初始化终端(支持实时按键检测) tty_init(); // 忽略 SIGPIPE 信号 signal(SIGPIPE, SIG_IGN);为什么忽略 SIGPIPE?
- 当向已关闭的管道写入数据时,系统会发送 SIGPIPE 信号
- 默认行为是终止程序,但 Nmap 需要优雅处理这种情况
- 忽略信号后,写入操作会返回错误码,程序可以继续执行
阶段 2:前置业务处理(非核心扫描的辅助操作)
核心目标:处理用户指定的"非扫描类指令",完成扫描前的业务校验。
关键操作详解:
-
路由解析
if (o.route_dst) { struct sockaddr_storage ss; size_t sslen = sizeof(ss); // 解析目标地址 if (resolve(o.route_dst, 0, &ss, &sslen, o.af()) == 0) { // 获取路由信息 struct route_nfo rn; if (get_route_info(&ss, &rn)) { // 打印路由信息 print_route_info(&rn); } } exit(0); // 仅打印路由,不执行扫描 }应用场景:用户想了解访问某个目标需要经过哪些路由节点,而不想执行实际扫描。
-
接口列表打印
if (o.iflist) { print_iflist(); // 打印所有网络接口信息 exit(0); }输出内容示例:
Starting Nmap 7.98 ( https://nmap.org ) INTERFACE: eth0 (192.168.1.100/255.255.255.0) MAC: 00:11:22:33:44:55 (Intel Corporation) UP, BROADCAST, RUNNING, MULTICAST -
FTP 弹跳扫描初始化
if (o.bounce) { // 解析 FTP 服务器地址 if (resolve(o.bounce, 21, &ftp_ss, &ftp_sslen, o.af()) != 0) { fatal("Failed to resolve FTP bounce host: %s", o.bounce); } // 验证 FTP 服务器可达性 if (!ftp_anon_connect(&ftp_ss)) { fatal("FTP bounce host does not support anonymous login"); } }FTP 弹跳扫描原理:
- 利用 FTP 协议的
PORT命令,让 FTP 服务器代替 Nmap 发送探测包 - 可以隐藏 Nmap 的真实 IP 地址
- 需要支持匿名登录的 FTP 服务器
- 利用 FTP 协议的
-
资源预分配
std::vector<Target *> Targets; Targets.reserve(100); // 预分配 100 个元素的内存空间为什么预分配?
std::vector动态扩容时需要重新分配内存并拷贝元素- 预分配可以减少扩容次数,提升性能
- 100 是经验值,适用于大多数扫描场景
阶段 3:输出框架初始化(结果存储准备)
核心目标:搭建扫描结果的输出框架(尤其是 XML),确保结果结构化、可追溯。
关键操作详解:
-
XML 报告初始化
if (o.xml_output) { if (o.resume) { // 恢复扫描:复用已有 XML 框架 xml_open_log_file(o.xml_filename, true); } else { // 首次扫描:生成 XML 根节点 xml_start_document(); xml_write_start_tag("nmaprun"); // 写入扫描元数据 xml_write_attribute("scanner", "nmap"); xml_write_attribute("args", o.cmdline); xml_write_attribute("start", time(NULL)); xml_write_attribute("version", NMAP_VERSION); // 写入扫描配置 xml_write_start_tag("scaninfo"); xml_write_attribute("type", "syn"); xml_write_attribute("protocol", "tcp"); xml_write_attribute("services", "1-1024"); xml_write_end_tag(); } }XML 输出结构示例:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE nmaprun> <?xml-stylesheet href="nmap.xsl" type="text/xsl"?> <nmaprun scanner="nmap" args="nmap -sS 192.168.1.1" start="1704528000" version="7.98"> <scaninfo type="syn" protocol="tcp" services="1-1024"/> <host> <address addr="192.168.1.1" addrtype="ipv4"/> <ports> <port protocol="tcp" portid="80"> <state state="open"/> </port> </ports> </host> </nmaprun> -
日志输出初始化
// 写入启动信息到人类可读日志 log_write(LOG_NORMAL, "Starting Nmap %s ( https://nmap.org )", NMAP_VERSION); log_write(LOG_NORMAL, " at %s", ctime(&now)); // 写入启动信息到机器可读日志 if (o.logfile) { log_open(o.logfile, LOG_NORMAL); log_write(LOG_NORMAL, "Starting Nmap %s", NMAP_VERSION); }
阶段 4:扫描配置准备(扫描规则落地)
核心目标:把用户的扫描规则(端口、排除列表、脚本)转化为可执行的扫描配置。
关键操作详解:
-
端口配置
// 加载端口→服务名映射关系 if (o.services_scan) { load_services("nmap-services"); } // 随机化端口列表 if (o.randomize_ports) { randomize_port_order(&ports); }端口映射示例:
80/tcp http 443/tcp https 22/tcp ssh 3306/tcp mysql随机化端口的作用:
- 避免按固定顺序扫描,降低被防火墙检测的风险
- 常用端口(80、443、22)前置,提升扫描效率
-
排除列表加载
// 解析 --exclude 参数 if (o.exclude) { parse_excludes(o.exclude, &exclude_group); } // 解析 --excludefile 参数 if (o.excludefile) { load_exclude_file(o.excludefile, &exclude_group); }排除列表格式:
192.168.1.0/24 10.0.0.0/8 172.16.0.1 -
NSE 脚本初始化
if (o.script) { // 加载脚本数据库 script_scan_results = get_script_scan_results_obj(); // 初始化 Lua 解释器 script_init(o.scriptargs, o.scripttimeout); // 执行预扫描脚本 script_scan(Targets, SCRIPT_PRE_SCAN); }预扫描脚本的作用:
- 在主机发现之前执行全局信息收集
- 例如:网络拓扑探测、DNS 枚举、端口扫描前的准备工作
- 不针对特定主机,而是针对整个扫描任务
阶段 5:核心扫描循环(核心业务执行)
核心目标:按"分组扫描"的思路,批量处理目标主机,执行具体的网络探测并输出结果。
这是函数最核心的阶段,采用"分组→扫描→输出→释放"的循环逻辑。
核心流程图:
关键设计细节详解:
-
目标分组机制
do { // 计算本次理想扫描的主机数量 ideal_scan_group_sz = determineScanGroupSize(o.numhosts_scanned, &ports); // 内层循环:填充 Targets 直到达到理想数量 while (Targets.size() < ideal_scan_group_sz) { // 生成下一个待扫描主机 currenths = nexthost(&hstate, exclude_group, &ports, o.pingtype); if (!currenths) break; // 过滤逻辑:只有"存活+需要端口扫描"的主机才加入 Targets if (currenths->flags & HOST_UP) { o.numhosts_up++; // 设置源地址 if (o.spoofsource) { currenths->setSourceSockAddr(&ss, sslen); } // 加入 Targets Targets.push_back(currenths); } else { // 非存活主机:直接输出+删除 print_host_basic_info(currenths); delete currenths; o.numhosts_scanned++; } } // 执行扫描... } while (o.numhosts_scanned < o.max_ips_to_scan);分组大小的计算策略:
int determineScanGroupSize(int scanned, PortList *ports) { int ideal_size; // 根据已扫描主机数动态调整 if (scanned < 10) { ideal_size = 10; // 初始阶段,小批量 } else if (scanned < 100) { ideal_size = 50; // 中期阶段,中批量 } else { ideal_size = 100; // 后期阶段,大批量 } // 考虑端口数量 if (ports->port_count > 1000) { ideal_size = ideal_size / 2; // 端口多时减少主机数 } return ideal_size; }为什么需要分组?
- 内存管理:避免一次性加载过多主机导致内存溢出
- 性能优化:批量处理减少系统调用开销
- 负载均衡:避免短时间内发送过多探测包被防火墙检测
- 同构性保证:确保分组内主机使用相同的源地址和网卡
-
扫描执行
// TCP SYN 扫描(批量处理) if (o.synscan) { ultra_scan(Targets, &ports, SYN_SCAN); } // TCP Connect 扫描(批量处理) if (o.connectscan) { ultra_scan(Targets, &ports, CONNECT_SCAN); } // UDP 扫描(批量处理) if (o.udpscan) { ultra_scan(Targets, &ports, UDP_SCAN); } // OS 扫描(批量处理) if (o.osscan) { OSScan os_engine; os_engine.os_scan(Targets); } // 脚本扫描(批量处理) if (o.script) { script_scan(Targets, SCRIPT_SCAN); }ultra_scan 的核心机制:
- 使用原始套接字发送探测包
- 异步接收响应,非阻塞 I/O
- 支持多种扫描类型(SYN、ACK、FIN、XMAS、NULL)
- 自动调整扫描速度(基于网络延迟和丢包率)
-
结果输出
// 遍历 Targets,输出每个主机的扫描结果 for (targetno = 0; targetno < Targets.size(); targetno++) { currenths = Targets[targetno]; // 输出端口状态 print_port_info(currenths); // 输出 OS 识别结果 if (o.osscan) { print_os_info(currenths); } // 输出脚本结果 if (o.script) { print_script_result(currenths); } // 输出到 XML if (o.xml_output) { xml_write_host(currenths); } }输出内容示例:
Nmap scan report for 192.168.1.1 Host is up (0.0023s latency). Not shown: 998 closed ports PORT STATE SERVICE 22/tcp open ssh 80/tcp open http MAC Address: 00:11:22:33:44:55 (Intel Corp) OS CPE: cpe:/o:linux:linux_kernel OS details: Linux 4.15 - 5.6 -
内存释放
// 释放所有 Target 对象,清空 Targets while (!Targets.empty()) { currenths = Targets.back(); delete currenths; // 释放堆内存 Targets.pop_back(); } o.numhosts_scanning = 0;为什么需要手动释放?
Targets存储的是指针,不是对象本身std::vector析构时只释放指针,不释放指针指向的对象- 必须手动
delete每个对象,否则会导致内存泄漏
阶段 6:后置处理与资源释放(收尾)
核心目标:完成扫描后的收尾工作,确保资源不泄漏、结果完整。
关键操作详解:
-
脚本后扫描
if (o.script) { script_scan(Targets, SCRIPT_POST_SCAN); }后扫描脚本的作用:
- 在所有主机扫描完成后执行
- 用于数据分析、报告生成、结果聚合
- 例如:生成漏洞报告、统计开放端口分布
-
资源释放
// 释放排除列表 free_excludes(&exclude_group); // 释放端口列表 free_port_list(&ports); // 关闭文件句柄 if (o.inputfd != -1) { close(o.inputfd); } // 关闭以太网套接字 if (o.rawethsd) { eth_close(o.rawethsd); } -
最终输出
// 打印扫描摘要 log_write(LOG_NORMAL, "Nmap done: %d IP addresses (%d hosts up) scanned in %.2f seconds", o.numhosts_scanned, o.numhosts_up, (double)(time(NULL) - start_time)); // 打印数据文件路径 if (o.verbose) { log_write(LOG_VERBOSE, "Data files: %s", o.datadir); } // 关闭 XML 输出 if (o.xml_output) { xml_write_end_tag("nmaprun"); xml_close_log_file(); }
1.3 核心设计思路总结
-
模块化与分层
- 将"环境初始化"“参数解析”“扫描执行”"结果输出"拆分为独立阶段
- 每个阶段职责单一,逻辑清晰,易于维护和扩展
-
效率优先
- 分组扫描减少系统调用开销,批量探测提升速度
- 端口随机化、常用端口前置,平衡扫描效率和隐蔽性
- 预分配内存减少动态扩容开销
-
兼容性与鲁棒性
- 适配 Windows/Linux/WSL 等多平台
- 处理信号、超时、内存泄漏等异常情况
- 支持"恢复扫描",避免扫描中断后重新执行
-
标准化输出
- XML 作为核心输出格式,兼顾"机器可解析"和"人类可读"
- 支撑自动化工具集成和二次开发
二、Targets 容器的存储机制与变化流程
2.1 Targets 的核心存储内容
Targets 是一个 std::vector<Target *> 类型的动态数组,核心存储的是指向 Target 类对象的指针。每个 Target 对象对应一个待执行端口扫描/深度扫描的存活目标主机,包含该主机的全量扫描相关信息。
Target 类的关键属性:
class Target {
public:
// 主机基础信息
std::string targetipstr() const; // IP 地址字符串
std::string HostName() const; // 主机名
struct sockaddr_storage targetss; // 套接字地址
u8 MAC[MAC_ADDR_LEN]; // MAC 地址
// 扫描状态
u32 flags; // 标志位(HOST_UP、HOST_DOWN 等)
struct timeval starttime; // 扫描开始时间
struct timeval endtime; // 扫描结束时间
PortList ports; // 端口列表(开放/关闭/过滤状态)
FingerPrintResults FP_results; // 操作系统识别结果
// 网络配置
struct sockaddr_storage sourcesock; // 源套接字地址
char *deviceName; // 扫描使用的网卡设备名
// 脚本/路由信息
ScriptResults scriptResults; // 脚本扫描结果
TracerouteResults tracerouteResults; // 路由追踪结果
};
重要说明:
Targets是"待扫描存活主机的指针集合"- 只有需要执行端口扫描/OS 扫描/脚本扫描的存活主机才会被加入
- 列表扫描、非存活主机不会进入这个容器
2.2 Targets 的完整变化流程
下面按代码执行阶段拆解 Targets 的变化,核心是"空容器→批量填充→扫描→输出→清空→循环"的闭环。
阶段 1:初始化(函数开头)—— 空容器
std::vector<Target *> Targets;
Targets.reserve(100);
变化说明:
- 创建一个空的
vector容器 - 调用
reserve(100)预留 100 个元素的内存空间 - 此时状态:
Targets.size() = 0,无任何主机指针
为什么预分配 100?
- 经验值,适用于大多数扫描场景
- 避免后续频繁扩容(扩容需要重新分配内存并拷贝元素)
- 100 个主机约占用 100 * sizeof(Target*) = 800 字节(64 位系统)
阶段 2:预扫描脚本阶段 —— 仍为空
if (o.script) {
script_scan_results = get_script_scan_results_obj();
script_scan(Targets, SCRIPT_PRE_SCAN); // 传入空的 Targets
}
变化说明:
- 此时
Targets依然为空 - 只是把空容器传给
script_scan函数 - 预扫描脚本此时无实际目标可处理
核心原因:
- 预扫描阶段仅做脚本初始化
- 还没开始主机发现(ping)
- 自然没有可扫描的主机
阶段 3:核心扫描循环 —— 批量填充 Targets(最关键)
这是 Targets 从空到有、数量增长的核心阶段。
do {
// 1. 计算本次理想扫描的主机数量
ideal_scan_group_sz = determineScanGroupSize(o.numhosts_scanned, &ports);
// 2. 内层循环:填充 Targets 直到达到理想数量
while (Targets.size() < ideal_scan_group_sz) {
// 2.1 生成下一个待扫描主机
currenths = nexthost(&hstate, exclude_group, &ports, o.pingtype);
if (!currenths) break; // 无更多主机,终止填充
// 2.2 过滤逻辑:只有"存活+需要端口扫描"的主机才加入 Targets
if (currenths->flags & HOST_UP) {
o.numhosts_up++;
// ❶ 列表扫描/无需端口扫描:直接输出+删除,不加入 Targets
if ((o.noportscan && !o.traceroute && !o.script) || o.listscan) {
print_host_info(currenths);
delete currenths;
o.numhosts_scanned++;
continue;
}
// ❷ 主机不存活:输出+删除,不加入 Targets
if (!(currenths->flags & HOST_UP)) {
print_host_basic_info(currenths);
delete currenths;
o.numhosts_scanned++;
continue;
}
// ❸ 存活且需要端口扫描:设置源地址,加入 Targets
if (o.spoofsource) {
currenths->setSourceSockAddr(&ss, sslen);
}
Targets.push_back(currenths); // 核心操作:将主机指针加入 Targets
}
}
// ... 后续扫描逻辑
} while (o.numhosts_scanned < o.max_ips_to_scan);
变化结果:
Targets被填充为"一批(理想数量)存活且需端口扫描的主机指针"- 关键规则:只有存活(HOST_UP)且需要端口/OS/脚本扫描的主机才会进入 Targets
过滤逻辑详解:
| 情况 | 处理方式 | 是否加入 Targets |
|---|---|---|
| 列表扫描模式 | 直接输出主机信息,删除对象 | ❌ 否 |
| 无需端口扫描 | 直接输出主机信息,删除对象 | ❌ 否 |
| 主机不存活 | 输出主机基本信息,删除对象 | ❌ 否 |
| 存活且需扫描 | 设置源地址,加入 Targets | ✅ 是 |
阶段 4:扫描执行阶段 —— Targets 内部数据更新
填充完成后,代码对 Targets 中的所有主机执行各类扫描。此时 Targets 的元素数量不变,但每个指针指向的 Target 对象内部数据被更新。
// TCP/UDP/SCTP 等端口扫描(批量处理 Targets)
if (o.synscan) {
ultra_scan(Targets, &ports, SYN_SCAN);
}
if (o.udpscan) {
ultra_scan(Targets, &ports, UDP_SCAN);
}
// OS 扫描(批量处理 Targets)
if (o.osscan) {
OSScan os_engine;
os_engine.os_scan(Targets); // 填充 Target 的 OS 信息
}
// 脚本扫描(批量处理 Targets)
if (o.script) {
script_scan(Targets, SCRIPT_SCAN);
}
变化说明:
Targets中每个Target对象的属性被更新- 端口状态从"未知"变为"开放/关闭/过滤"
- 填充 OS 版本、脚本扫描结果等
示例变化:
// 扫描前
currenths->ports.hasOpenPorts() // false
currenths->os_info // 空
// 扫描后
currenths->ports.hasOpenPorts() // true
currenths->os_info // "Linux 4.15"
currenths->ports.port_count // 5
阶段 5:结果输出 + 清空 Targets(释放内存)
扫描完成后,输出结果并清空 Targets,避免内存泄漏。
// 1. 遍历 Targets,输出每个主机的扫描结果
for (targetno = 0; targetno < Targets.size(); targetno++) {
currenths = Targets[targetno];
print_port_info(currenths); // 输出端口状态
print_os_info(currenths); // 输出 OS 识别结果
print_script_result(currenths); // 输出脚本结果
if (o.xml_output) {
xml_write_host(currenths); // 输出到 XML
}
}
// 2. 核心操作:释放所有 Target 对象,清空 Targets
while (!Targets.empty()) {
currenths = Targets.back(); // 取最后一个元素
delete currenths; // 释放 Target 对象内存(关键!避免泄漏)
Targets.pop_back(); // 从 vector 中移除指针
}
o.numhosts_scanning = 0;
变化说明:
- 先输出每个主机的扫描结果
- 逐个
delete指针指向的Target对象(释放堆内存) - 再
pop_back移除指针 - 最终
Targets.size() = 0,回到空容器状态
为什么从后往前删除?
pop_back()是 O(1) 操作,效率最高- 从前往后删除需要移动后续元素,效率较低
阶段 6:循环重复 + 后扫描脚本
// do-while 循环:回到"阶段3",继续填充新的一批主机到 Targets
// 直到扫描完所有目标(达到 o.max_ips_to_scan)
// 后扫描脚本阶段
if (o.script) {
script_scan(Targets, SCRIPT_POST_SCAN); // 此时 Targets 已清空
}
变化说明:
do-while循环继续填充新的一批主机到Targets- 后扫描脚本阶段:
Targets已清空,仅执行收尾脚本逻辑
阶段 7:函数结束
Targets 是局部变量,函数执行完毕后被自动销毁,无残留。
2.3 内存管理的关键点
-
指针与对象的区别
Targets存储的是指针,不是对象本身std::vector析构时只释放指针,不释放指针指向的对象- 必须手动
delete每个对象,否则会导致内存泄漏
-
内存泄漏的后果
- 长时间运行的 Nmap 会占用大量内存
- 可能导致系统内存不足,影响其他程序
- 严重时会导致程序崩溃
-
正确的内存管理方式
// 错误方式:只清空 vector,不释放对象 Targets.clear(); // ❌ 内存泄漏! // 正确方式:先释放对象,再清空 vector while (!Targets.empty()) { delete Targets.back(); Targets.pop_back(); } // ✅ 正确
2.4 总结
-
存储内容:
Targets存储指向Target对象的指针,仅包含"存活且需要端口/OS/脚本扫描"的目标主机 -
变化流程:
- 空容器初始化
- 预扫描阶段为空
- 核心循环批量填充存活主机指针
- 执行扫描(更新对象内部数据)
- 输出结果
- 释放对象 + 清空容器
- 循环直至扫描完成
-
内存管理:
Targets存储的是指针- 必须手动
delete每个对象再清空容器 - 否则会导致严重的内存泄漏
三、网络地址解析系统
Nmap 的网络地址解析系统是其核心基础设施之一,负责将用户输入的主机名或 IP 地址转换为程序可用的套接字地址结构。本节将深入解析这个系统的各个组件。
3.1 resolve 函数:地址解析的简化接口
函数定义
/**
* @brief 网络地址解析的对外封装函数(简化版接口)
* 用于将主机名/IP字符串 + 端口号解析为通用的套接字地址结构体
*
* @param hostname 待解析的主机名(如"www.baidu.com")或IP字符串(如"192.168.1.1")
* @param port 目标端口号(如80、443)
* @param ss 输出参数:指向sockaddr_storage结构体的指针,存储解析后的完整地址信息
* @param sslen 输入输出参数:入参时指定ss缓冲区的大小,出参时返回实际填充的地址结构长度
* @param af 地址族(Address Family),指定解析类型(AF_INET=IPv4,AF_INET6=IPv6)
* @return 整型返回值:0表示解析成功,非0表示失败(通常是系统错误码)
*/
int resolve(const char *hostname, unsigned short port,
struct sockaddr_storage *ss, size_t *sslen, int af) {
int flags;
struct sockaddr_list sl;
int result;
// 处理 --nodns 选项
flags = 0;
if (o.nodns)
flags |= AI_NUMERICHOST;
// 调用底层解析函数
result = resolve_internal(hostname, port, &sl, af, flags, 0);
// 将解析结果返回给调用者
*ss = sl.addr.storage;
*sslen = sl.addrlen;
return result;
}
参数详解
| 参数名 | 类型 | 作用与特性 |
|---|---|---|
hostname |
const char * |
待解析的目标地址:可以是域名(如google.com)、IPv4字符串(1.1.1.1)、IPv6字符串(2001:4860:4860::8888);const 保证函数不会修改传入的字符串内容 |
port |
unsigned short |
网络端口号,取值范围0-65535(符合TCP/UDP端口的标准范围),无符号短整型刚好适配这个范围 |
ss |
struct sockaddr_storage * |
输出参数:解析后的地址会写入这个结构体;sockaddr_storage 是通用地址类型,兼容IPv4(sockaddr_in)和IPv6(sockaddr_in6),避免单独处理两种地址类型 |
sslen |
size_t * |
输入输出参数: - 入参:告诉函数 ss 缓冲区的总大小(如sizeof(struct sockaddr_storage));- 出参:函数返回时,会把实际填充的地址结构长度(如IPv4是16字节,IPv6是28字节)写回这个指针 |
af |
int |
指定解析的地址族:AF_INET(强制解析为IPv4)、AF_INET6(强制解析为IPv6),对应你之前问的af()函数的返回值 |
核心设计意图
resolve 函数的设计目标是:
提供一个简单、安全、统一接口,将用户输入(域名/IP)解析为 socket 可用的地址结构,同时尊重 Nmap 的全局参数(如
--nodns)。
关键特性:
- 简化接口:隐藏了
resolve_internal的复杂参数,只暴露核心功能 - 参数安全:使用
const修饰输入参数,防止意外修改 - 兼容性:通过
sockaddr_storage支持 IPv4 和 IPv6 - 全局参数集成:自动处理
--nodns选项
典型使用示例
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct sockaddr_storage ss;
size_t sslen = sizeof(ss);
const char *hostname = "www.baidu.com";
unsigned short port = 80;
// 调用 resolve 解析百度 80 端口(IPv4)
int ret = resolve(hostname, port, &ss, &sslen, AF_INET);
if (ret == 0) {
// 转换为 IPv4 地址结构并打印
struct sockaddr_in *ipv4_addr = (struct sockaddr_in *)&ss;
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &ipv4_addr->sin_addr, ip_str, INET_ADDRSTRLEN);
printf("解析成功:%s:%d\n", ip_str, ntohs(ipv4_addr->sin_port));
} else {
printf("解析失败,错误码:%d\n", ret);
}
return 0;
}
3.2 resolve_internal 函数:地址解析的核心实现
函数定义
/*
* 内部辅助函数:为 resolve 和 resolve_numeric 提供核心地址解析逻辑
* addl_flags 会按位或到 hints.ai_flags 中,可用于添加 AI_NUMERICHOST 等标志
* AI_NUMERICHOST:强制将hostname当作数字IP解析(如192.168.1.1),跳过DNS查询
*/
static int resolve_internal(const char *hostname, unsigned short port,
struct sockaddr_storage *ss, size_t *sslen, int af, int addl_flags) {
// 用于给 getaddrinfo 传递"解析提示"的结构体
struct addrinfo hints;
// 存储 getaddrinfo 返回的解析结果链表头
struct addrinfo *result;
// 临时缓冲区:将数字端口转为字符串(getaddrinfo 要求端口是字符串格式)
char portbuf[16];
// 指向端口字符串的指针(传给 getaddrinfo 的 servname 参数)
char *servname = NULL;
// 函数返回码/临时状态码
int rc;
// 调试阶段断言:确保输入参数非空,避免空指针访问
assert(hostname); // 主机名/IP不能为空
assert(ss); // 输出地址结构体指针不能为空
assert(sslen); // 输出地址长度指针不能为空
// 初始化 hints 结构体为全0,避免脏数据影响解析
memset(&hints, 0, sizeof(hints));
hints.ai_family = af; // 指定解析的地址族(AF_INET=IPv4,AF_INET6=IPv6)
hints.ai_socktype = SOCK_DGRAM;// 指定套接字类型为UDP(数据报)
hints.ai_flags |= addl_flags; // 合并附加标志(如AI_NUMERICHOST)
/* 将数字端口转换为字符串:getaddrinfo 的 servname 参数要求是字符串格式 */
if (port != 0) { // 仅当端口非0时处理(端口0通常表示不指定)
// 安全格式化端口号到 portbuf(Snprintf 是安全版 snprintf,避免缓冲区溢出)
rc = Snprintf(portbuf, sizeof(portbuf), "%hu", port);
// 断言:格式化成功,且结果长度未超出缓冲区
assert(rc >= 0 && (size_t) rc < sizeof(portbuf));
servname = portbuf; // 让 servname 指向转换后的端口字符串
}
// 核心调用:通过 getaddrinfo 解析主机名/IP + 端口,获取套接字地址信息
rc = getaddrinfo(hostname, servname, &hints, &result);
if (rc != 0) // 解析失败:直接返回 getaddrinfo 的错误码(如EAI_NONAME=域名不存在)
return rc;
if (result == NULL) // 解析无结果:返回EAI_NONAME(无匹配名称)
return EAI_NONAME;
// 断言:解析结果的地址长度有效(大于0且不超过 sockaddr_storage 的大小)
assert(result->ai_addrlen > 0 && result->ai_addrlen <= (int) sizeof(struct sockaddr_storage));
// 将解析结果的长度和地址拷贝到输出参数中
*sslen = result->ai_addrlen; // 输出实际的地址结构长度
memcpy(ss, result->ai_addr, *sslen); // 拷贝地址数据到 ss 缓冲区
// 释放 getaddrinfo 分配的内存(必须调用,否则内存泄漏)
freeaddrinfo(result);
return 0; // 解析成功,返回0
}
核心逻辑分步拆解
关键技术点
-
getaddrinfo 的使用
getaddrinfo是 POSIX 标准的地址解析函数- 替代了老旧的
gethostbyname和inet_addr - 支持 IPv4/IPv6 兼容、域名解析、端口转换
- 返回标准化的错误码(如
EAI_NONAME、EAI_AGAIN)
-
hints 结构体的作用
struct addrinfo { int ai_flags; // AI_NUMERICHOST、AI_PASSIVE 等标志 int ai_family; // AF_INET、AF_INET6、AF_UNSPEC int ai_socktype; // SOCK_STREAM、SOCK_DGRAM int ai_protocol; // IPPROTO_TCP、IPPROTO_UDP size_t ai_addrlen; // 地址长度 struct sockaddr *ai_addr; // 地址指针 char *ai_canonname; // 规范名称 struct addrinfo *ai_next; // 链表下一个节点 }; -
端口转换的必要性
getaddrinfo的servname参数要求是字符串(如"80")- 需要将数字端口(
unsigned short)转为字符串 - 使用
Snprintf安全格式化,避免缓冲区溢出
-
内存管理
getaddrinfo会动态分配内存,必须调用freeaddrinfo释放- 否则会导致内存泄漏
典型调用场景
// 示例:强制解析数字IP(192.168.1.1)的80端口,跳过DNS查询
struct sockaddr_storage ss;
size_t sslen = sizeof(ss);
// 附加标志传 AI_NUMERICHOST,强制数字IP解析
int ret = resolve_internal("192.168.1.1", 80, &ss, &sslen, AF_INET, AI_NUMERICHOST);
if (ret == 0) {
printf("解析成功,地址长度:%zu字节\n", sslen);
} else {
printf("解析失败:%s\n", gai_strerror(ret)); // 转换错误码为可读字符串
}
3.3 升级版 resolve_internal:支持多地址返回
函数定义
/*
* 内部辅助函数:为 resolve 和 resolve_numeric 提供核心地址解析逻辑(支持多地址返回)
* 关键特性:
* 1. addl_flags 按位或到 hints.ai_flags,可添加 AI_NUMERICHOST 等标志(强制数字IP解析)
* 2. sl 指向 sockaddr_list 链表的首个元素(静态分配),后续节点动态分配
* 3. multiple_addrs 控制是否返回多个地址:false 仅返回第一个,true 返回所有解析结果
*
* @param hostname 待解析的主机名/数字IP(非空)
* @param port 目标端口号(需转为字符串传给 getaddrinfo)
* @param sl 输出参数:指向 sockaddr_list 链表首节点的指针,存储解析后的地址
* @param af 地址族(AF_INET=IPv4,AF_INET6=IPv6)
* @param addl_flags 附加标志(如 AI_NUMERICHOST),合并到 hints.ai_flags
* @param multiple_addrs 是否返回多个解析地址:false=仅第一个,true=所有
* @return 0=成功,非0=错误码(如 EAI_NONAME=域名不存在)
*/
static int resolve_internal(const char *hostname, unsigned short port,
struct sockaddr_list *sl, int af, int addl_flags, int multiple_addrs)
{
// 用于给 getaddrinfo 传递解析提示的结构体
struct addrinfo hints;
// 存储 getaddrinfo 返回的解析结果链表头
struct addrinfo *result;
// 遍历 getaddrinfo 结果链表的临时指针
struct addrinfo *next;
// 指向 sockaddr_list 链表节点指针的指针(用于构建链表)
struct sockaddr_list **item_ptr = &sl;
// 指向当前 sockaddr_list 节点的指针
struct sockaddr_list *new_item;
// 临时缓冲区:将数字端口转为字符串(getaddrinfo 要求端口是字符串)
char portbuf[16];
// 函数返回码/临时状态码
int rc;
// 断言:确保主机名非空
ncat_assert(hostname != NULL);
// 初始化 hints 结构体为全0,避免脏数据影响解析
memset(&hints, 0, sizeof(hints));
hints.ai_family = af; // 指定解析的地址族(IPv4/IPv6)
hints.ai_socktype = SOCK_DGRAM;// 指定套接字类型为UDP(数据报)
hints.ai_flags |= addl_flags; // 合并附加标志(如 AI_NUMERICHOST)
/* 将数字端口转换为字符串:getaddrinfo 的 servname 参数要求是字符串格式 */
rc = Snprintf(portbuf, sizeof(portbuf), "%hu", port);
// 断言:格式化成功且未超出缓冲区
ncat_assert(rc >= 0 && (size_t) rc < sizeof(portbuf));
// 核心调用:解析主机名+端口,获取地址信息链表
rc = getaddrinfo(hostname, portbuf, &hints, &result);
if (rc != 0) // 解析失败:返回 getaddrinfo 错误码
return rc;
if (result == NULL) // 解析无结果:返回 EAI_NONAME(无匹配名称)
return EAI_NONAME;
// 断言:解析结果的地址长度有效(大于0且不超过 sockaddr_storage 大小)
ncat_assert(result->ai_addrlen > 0 && result->ai_addrlen <= (int) sizeof(struct sockaddr_storage));
// 遍历 getaddrinfo 返回的所有地址结果
for (next = result; next != NULL; next = next->ai_next) {
// 如果当前链表节点为空,动态分配新节点
if (*item_ptr == NULL)
{
*item_ptr = (struct sockaddr_list *)safe_malloc(sizeof(struct sockaddr_list));
(**item_ptr).next = NULL; // 初始化新节点的 next 指针为 NULL
}
// 指向当前要填充的链表节点
new_item = *item_ptr;
// 填充地址长度
new_item->addrlen = next->ai_addrlen;
// 拷贝地址数据到节点的通用地址存储区(sockaddr_storage)
memcpy(&new_item->addr.storage, next->ai_addr, next->ai_addrlen);
// 如果不需要返回多个地址,跳出循环(仅保留第一个地址)
if (!multiple_addrs)
break;
// 否则,移动指针到下一个节点,准备存储下一个地址
item_ptr = &new_item->next;
}
// 释放 getaddrinfo 分配的内存(必须调用,避免内存泄漏)
freeaddrinfo(result);
return 0; // 解析成功,返回0
}
核心新增特性
| 特性 | 旧版 resolve_internal | 新版 resolve_internal |
|---|---|---|
| 地址存储 | 单个 sockaddr_storage 结构体 | sockaddr_list 链表(支持多个地址) |
| 多地址返回 | 不支持(仅返回第一个) | 支持(multiple_addrs 控制) |
| 内存分配 | 无动态分配(仅拷贝) | 动态分配 sockaddr_list 节点(safe_malloc) |
sockaddr_list 结构体
// 存储套接字地址的链表节点结构体
struct sockaddr_list {
size_t addrlen; // 地址结构体的实际长度
union {
struct sockaddr_storage storage; // 通用地址存储(兼容IPv4/IPv6)
struct sockaddr sa;
struct sockaddr_in sin; // IPv4 地址
struct sockaddr_in6 sin6; // IPv6 地址
} addr;
struct sockaddr_list *next; // 指向下一个节点的指针
};
二级指针的巧妙使用
二级指针 item_ptr 是构建动态链表的关键:
// 初始状态:sl 是链表头(NULL),item_ptr = &sl
item_ptr = &sl;
// 第一次循环:*item_ptr 为 NULL,分配新节点后 *item_ptr 指向节点A
*item_ptr = safe_malloc(...);
// 如果 multiple_addrs=true,item_ptr 指向节点A的next指针
item_ptr = &new_item->next;
// 第二次循环:*item_ptr 为 NULL,分配新节点后 *item_ptr 指向节点B,节点A的next=节点B
优势:
- 无需额外维护"尾指针"
- 直接通过二级指针遍历并扩展链表
- 代码更简洁,逻辑更清晰
典型使用场景
// 示例:解析百度域名,返回所有IPv4地址
struct sockaddr_list sl = {0}; // 初始化链表头
int ret = resolve_internal(
"www.baidu.com", 80, // 主机名+端口
&sl, // 输出链表
AF_INET, // 仅IPv4
0, // 无附加标志
1 // 返回多个地址
);
if (ret == 0) {
// 遍历链表打印所有IP
struct sockaddr_list *cur = &sl;
while (cur != NULL) {
const char *ip = inet_ntop_ez(&cur->addr.storage, cur->addrlen);
printf("解析到IP:%s\n", ip);
cur = cur->next;
}
}
3.4 inet_ntop_ez 函数:简化的 IP 地址转换
函数定义
/*
* 简化版的 inet_ntop 函数:无需手动传入目标缓冲区,降低调用复杂度
* 特性:
* 1. 返回静态缓冲区指针,可直接使用(但再次调用会覆盖内容)
* 2. 自动识别 IPv4/IPv6 地址族,无需手动指定
* 3. 长度校验:若 sslen 过小(不足以容纳对应地址结构体),返回 NULL
* 4. 兼容禁用网卡的异常场景(这类场景会返回 NULL)
*
* @param ss 指向通用套接字地址结构体的指针(存储 IPv4/IPv6 地址)
* @param sslen ss 结构体的实际长度(用于校验有效性)
* @return 成功返回指向静态缓冲区的 IP 字符串指针;失败/异常返回 NULL
*/
const char *inet_ntop_ez(const struct sockaddr_storage *ss, size_t sslen) {
// 强制转换为 IPv4 地址结构体指针(先假设是 IPv4,后续判断地址族)
const struct sockaddr_in *sin = (struct sockaddr_in *) ss;
// 静态缓冲区:存储转换后的 IP 字符串,大小兼容 IPv6(INET6_ADDRSTRLEN=46)
// 注意:静态缓冲区会被多次调用复用,线程不安全
static char str[INET6_ADDRSTRLEN];
#if HAVE_IPV6 // 条件编译:仅当系统支持 IPv6 时定义 IPv6 相关变量
const struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) ss;
#endif
// 初始化缓冲区为空字符串,避免脏数据干扰
str[0] = '\0';
// 处理 IPv4 地址
if (sin->sin_family == AF_INET) {
// 校验 sslen:必须至少容纳 IPv4 地址结构体,否则返回 NULL(防止越界)
if (sslen < sizeof(struct sockaddr_in))
return NULL;
// 调用标准 inet_ntop 转换 IPv4 地址到静态缓冲区,返回缓冲区指针
return inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str));
}
#if HAVE_IPV6 // 系统支持 IPv6 时处理 IPv6 地址
else if(sin->sin_family == AF_INET6) {
// 校验 sslen:必须至少容纳 IPv6 地址结构体
if (sslen < sizeof(struct sockaddr_in6))
return NULL;
// 调用标准 inet_ntop 转换 IPv6 地址到静态缓冲区
return inet_ntop(AF_INET6, &sin6->sin6_addr, str, sizeof(str));
}
#endif
// 注释说明:部分笔记本的禁用无线网卡会上报空的 IP/地址族,因此会走到这里
// 其他未知地址族也会走到这里,返回 NULL
return NULL;
}
核心设计目标
标准 inet_ntop 要求调用者手动分配缓冲区并传入,使用繁琐且容易因缓冲区大小不足导致问题;inet_ntop_ez 做了两层简化:
- 内置静态缓冲区,无需调用者管理内存
- 自动识别地址族(IPv4/IPv6),无需手动指定
AF_INET/AF_INET6
静态缓冲区的"便利"与"坑"
便利:
// 简化调用示例
struct sockaddr_storage ss;
size_t sslen = sizeof(ss);
resolve("www.baidu.com", 80, &ss, &sslen, AF_INET);
const char *ip = inet_ntop_ez(&ss, sslen);
if (ip) printf("IP: %s\n", ip); // 直接使用返回的字符串
风险:
- 线程不安全:多线程同时调用会覆盖缓冲区,导致返回的 IP 字符串错乱
- 内容覆盖:连续调用会覆盖前一次结果
const char *ip1 = inet_ntop_ez(&ss1, sslen1); // ip1指向"192.168.1.1" const char *ip2 = inet_ntop_ez(&ss2, sslen2); // ip1和ip2都指向新值(如"8.8.8.8") - 解决办法:若需保存结果,需立即将返回的字符串拷贝到自己的缓冲区(如
strcpy/strdup)
与标准 inet_ntop 的对比
| 特性 | 标准 inet_ntop | inet_ntop_ez |
|---|---|---|
| 缓冲区管理 | 调用者手动分配、传入 | 内置静态缓冲区,无需管理 |
| 地址族指定 | 需手动传入 AF_INET/AF_INET6 | 自动识别地址族 |
| 线程安全 | 线程安全(缓冲区由调用者管理) | 线程不安全(静态缓冲区复用) |
| 调用复杂度 | 高(需处理缓冲区、地址族) | 低(仅传 ss 和 sslen) |
| 异常容错 | 无长度校验,需调用者自行处理 | 内置长度校验、禁用网卡容错 |
四、路由信息结构体详解
4.1 route_nfo 结构体定义
/**
* @brief 路由信息结构体
* 用于存储与目标网络/主机通信时的核心路由相关信息,包括接口、直连状态、源地址、下一跳等
*/
struct route_nfo {
/**
* 关联的网络接口信息
* interface_info 是另一个预定义的结构体(通常包含接口名、接口IP、MAC地址等)
* 存储当前路由对应的出口网络接口的基础信息
*/
struct interface_info ii;
/**
* 直连标志位(布尔型)
* - 非0(true):目标主机/网络与本机直连,无需经过路由器转发
* - 0(false):目标非直连,需要通过路由转发才能到达
*/
int direct_connect;
/**
* 数据包发送时应使用的源IP地址
* 注意:此字段可能与 ii.addr(接口本身的IP)不同
* 典型场景:使用本地回环接口(localhost)扫描本机另一块网卡的IP时,源地址需指定为对应网卡的IP
* sockaddr_storage 是通用地址结构体,兼容IPv4(sockaddr_in)和IPv6(sockaddr_in6)
*/
struct sockaddr_storage srcaddr;
/**
* 下一跳(网关)地址
* 仅当 direct_connect = 0(非直连)时有效,存储前往目标的第一个路由节点(网关)地址
* 若 direct_connect = 1(直连),此字段无实际意义(通常置空或未初始化)
*/
struct sockaddr_storage nexthop;
};
4.2 逐成员详细解释
1. struct interface_info ii
这是一个嵌套的结构体,interface_info 是网络编程中常见的"接口信息结构体",通常包含:
struct interface_info {
char devname[32]; // 接口名称(如 "eth0"、"wlan0")
struct sockaddr_storage addr; // 接口的IP地址
u8 mac[MAC_ADDR_LEN]; // 接口的MAC地址
int netmask_bits; // 子网掩码位数(如 24 表示 255.255.255.0)
int is_up; // 接口是否启用
};
作用:绑定当前路由对应的出口网络接口,告诉程序"要访问目标,需要从哪个网卡出去"。
2. int direct_connect
本质是布尔型标志(C语言无原生bool,用int替代),核心作用是判断目标是否在本机的直连网段内。
举例:
- 你的电脑和路由器直连(
direct_connect=1) - 但访问外网服务器时需要经过路由器转发(
direct_connect=0)
判断逻辑:
// 伪代码:判断是否直连
if ((target_ip & netmask) == (interface_ip & netmask)) {
direct_connect = 1; // 在同一网段,直连
} else {
direct_connect = 0; // 不在同一网段,需要路由
}
3. struct sockaddr_storage srcaddr
sockaddr_storage 是 POSIX 标准的通用地址存储结构,解决了 sockaddr(IPv4)和 sockaddr_in6(IPv6)的兼容性问题,能存储任意类型的IP地址。
作用:指定发送数据包时的源IP——比如本机有多个网卡(192.168.1.100、10.0.0.5),访问目标时需要明确用哪个IP作为数据包的"源地址",而非默认的接口IP。
典型场景:
// 场景1:多网卡环境
// eth0: 192.168.1.100
// eth1: 10.0.0.5
// 访问 192.168.1.1 时,srcaddr 应设置为 192.168.1.100
// 访问 10.0.0.1 时,srcaddr 应设置为 10.0.0.5
// 场景2:IP 欺骗
// 用户指定 --spoof-source 1.2.3.4
// srcaddr 应设置为 1.2.3.4(即使本机没有这个IP)
4. struct sockaddr_storage nexthop
仅在非直连场景下有效,存储"下一跳网关"的IP地址。
举例:
- 你要访问 8.8.8.8(谷歌DNS)
- 本机路由表显示下一跳是 192.168.1.1(路由器IP)
- 则
nexthop就存储 192.168.1.1
路由表查询流程:
// 伪代码:查询路由表
route_entry = lookup_route_table(target_ip);
if (route_entry) {
if (route_entry->gateway != 0) {
nexthop = route_entry->gateway; // 有网关,使用网关地址
direct_connect = 0;
} else {
nexthop = target_ip; // 无网关,直接访问目标
direct_connect = 1;
}
}
4.3 实际应用示例
// 示例:获取访问 8.8.8.8 的路由信息
struct sockaddr_storage target_ss;
size_t target_sslen = sizeof(target_ss);
resolve("8.8.8.8", 0, &target_ss, &target_sslen, AF_INET);
struct route_nfo rn;
if (get_route_info(&target_ss, &rn)) {
printf("接口名称:%s\n", rn.ii.devname);
printf("接口IP:%s\n", inet_ntop_ez(&rn.ii.addr, sizeof(rn.ii.addr)));
printf("源地址:%s\n", inet_ntop_ez(&rn.srcaddr, sizeof(rn.srcaddr)));
if (rn.direct_connect) {
printf("直连:是\n");
} else {
printf("直连:否\n");
printf("下一跳:%s\n", inet_ntop_ez(&rn.nexthop, sizeof(rn.nexthop)));
}
}
输出示例:
接口名称:eth0
接口IP:192.168.1.100
源地址:192.168.1.100
直连:否
下一跳:192.168.1.1
4.4 总结
-
route_nfo结构体的核心作用是封装访问目标网络/主机的全量路由上下文,整合了接口、直连状态、源地址、下一跳四大核心信息。 -
direct_connect是核心标志位,决定了nexthop字段是否有效,是区分"直连通信"和"路由转发通信"的关键。 -
sockaddr_storage的使用让结构体兼容 IPv4 和 IPv6,是网络编程中处理多协议的通用做法。
五、总结与思考
5.1 核心要点回顾
通过本文的深入分析,我们全面理解了 Nmap 的核心机制:
-
nmap_main 函数的六阶段执行流程
- 系统与环境初始化
- 前置业务处理
- 输出框架初始化
- 扫描配置准备
- 核心扫描循环
- 后置处理与资源释放
-
Targets 容器的存储机制
- 存储指向
Target对象的指针 - 仅包含"存活且需要扫描"的主机
- 遵循"空容器→批量填充→扫描→输出→清空→循环"的闭环
- 存储指向
-
网络地址解析系统
resolve:简化接口,处理全局参数resolve_internal:核心实现,支持多地址返回inet_ntop_ez:简化的 IP 地址转换
-
路由信息结构体
- 封装接口、直连状态、源地址、下一跳
- 支持直连和路由转发两种通信模式
- 兼容 IPv4 和 IPv6
5.2 设计思想与最佳实践
-
模块化设计
- 将复杂功能拆分为独立的函数和模块
- 每个模块职责单一,易于维护和扩展
-
内存管理
- 使用智能指针或 RAII 模式(C++)
- 手动管理时,确保配对分配和释放
- 使用
assert和断言进行调试校验
-
错误处理
- 使用标准化的错误码
- 提供详细的错误信息
- 优雅处理异常情况
-
跨平台兼容
- 使用条件编译处理平台差异
- 使用标准 API(如
getaddrinfo) - 提供统一的接口隐藏平台细节
5.3 学习建议
-
深入理解网络编程基础
- 套接字编程
- 地址解析(DNS)
- 路由和网关
-
阅读源码的方法
- 从核心函数入手(如
nmap_main) - 理解数据结构和算法
- 关注内存管理和错误处理
- 从核心函数入手(如
-
实践与实验
- 使用调试工具(如 GDB)
- 添加日志输出
- 编写测试用例
5.4 后续学习方向
-
扫描引擎实现
ultra_scan函数的详细机制- 原始套接字的使用
- 异步 I/O 和事件驱动
-
操作系统识别
osscan2.cc的实现- 指纹匹配算法
- 主动和被动探测
-
NSE 脚本引擎
- Lua 解释器的集成
- 脚本执行流程
- 脚本库和 API
参考资源
作者注:本文基于 Nmap 7.98 版本源码进行分析,部分实现细节可能在不同版本中有所差异。建议读者结合实际源码进行学习和实践。
版权声明:本文遵循 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
更多推荐



所有评论(0)