写一个 Windows 下运行的路由追踪程序

本文同步发布于 糖菓・部落

这只是一篇开发碎碎念，里面没什么实质性的技术内容，并且 NyaTrace 项目也已经大改并实现了很多预期的目标所以文章内容可能会有些过时；如果您想要的是代码或是可执行程式的话，请移步 nyatrace.app 。

作为买到 GeoIP2 后的第三个项目（前两个分别是喵窝的登录位置标注和 NyaSpeed 的真实位置显示），这次我希望能完成一个长久未了的心愿：写一个可视化的、附带 IP 详细信息的路由追踪程序。

灵感来源

您可能听说过 17monipdb.exe 这个工具，或者它的后继者 Best Trace ，它曾经是我用于路由追踪工作的不二选择。但随着它的开发者 IPIP.NET 逐渐转向生态封闭的商业化（所有产品都是咨询定价的企业模式），出于一种本能的排斥心理，我开始寻找替代的解决方案。

后来有一款新的工具 WorstTrace 兴起（估计是为了对标 Best Trace 吧），但其使用 Electron 封装导致体积庞大，虽然 UI 更为现代化，却依然并不被我认为是一种好的解决思路。

加上上述的这两种工具都是闭源产品，代码安全审计无从谈起，也因此在很长的一段时间里，我实际上是依赖系统自带的 tracert 和 HE BGP Toolkit 与 Censys Search 配合使用的。

但这毕竟不是长久之计，一来需要人工手动操作，不适合快速判断链路情况；二来较为依赖与 HE 和 Censys 的连接情况，在一些特殊场合下并不能得到需要的数据，因而也就萌生了依赖本地运行环境执行路由追踪的任务。前段时间挤出一些资金采购了 MaxMind 的 GeoIP2 City 和 ISP 一个月的订阅，就想着能不能利用好这两个数据库，填一个心心念念了那么久的坑。

开发进行

开发工作的第一步就是分析需求，所以我拆分出了三个模块：

路由追踪

图形界面

IP 数据库读取

路由追踪

寻找参考

第一步上来就撞了南墙。搜索 route trace open source ，第一个跳出来的 Open Visual Traceroute ，是一个使用 Java 开发的工具。可能是对 Java 有偏见，我总是认为其开发的软件既臃肿又高度依赖环境，一想到为了实现路由追踪这么个小玩具的功能就需要所有用户装一个巨大的硬盘吞噬者，只感觉悲从心中来。后来又用中文搜索 开源 路由追踪 ，搜索到了 NextTrace ，但当我满心欢喜想要运行，却发现它并不支持 Windows 的时候，心又凉了一截。

期间搜索到了 golang 版的 traceroute 实现 ，看到了它提到了 golang.org/x/net/ipv4 这个包，但发现其并不支持 Windows 功能时，有想过依照它的 TraceRoute 样例 封装一个 Docker 镜像，来实现 Windows 套 Docker 的 Linux 实现思路的时候，因为太过复杂，又被我摇头否决了。

我忘了是什么时候搜索到 TraceRoute 的实现（Windows 下 C/C++ 基于原始套接字） 这篇 Blog 的了，只记得终于看到一篇能明白我在想什么的文章，就差当场感动到大哭了。言归正传，这篇 blog 讲述的正是我需要的路由追踪功能的底层套接字实现（即不依赖任何外部组件，完全依赖底层的系统交互），所以在仔细地研读了它提到的实现方式描述之后，我决定先把代码拿下来测试下看看。

结果么，只能用又喜又悲来形容。喜的是这个程序它能运行，和其他日常时候找到牛头不对马嘴的报错地狱代码相比完全不是一类；悲的是它的结果并不尽如人意，除了最后一跳能获得 IP 之外其他所有的报文都显示超时。

我开了 WireShark 抓包，却发现有很多明明是有返回的 Time-to-live exceeded 数据包，但套接字的 recvFrom 就是获取不到。

于是我以为是传入的参数问题，找了半天，无果；又以为是 Windows 11 改动了底层套接字的配置方案（参考的这篇文章是 2020 年的），就到处去找有没有相关的资料的时候，得到的结果只能用完完全全的一无所获来形容。一筹莫展之际，我打算换一种语言试试。

我想到了 python ，想着大不了打一个大一点的环境包，也不是不能用。恰好 python 上有一个操作库支持路由追踪，那就是 Scapy 。不过很可惜的是，我找了各种文档各种 blog ，似乎人们总是很喜欢把官方那语焉不详的文档拿出来用中文翻译一遍，再贴上一些看似运行结果都一样的没有上下文的代码残肢，但对于怎么好好使用这个路由追踪功能来完成一件事，实在没有搜罗到什么有价值的信息，就又只好作罢。

之后搜索到了 nodejs-traceroute 这个库，发现它用了一个巧妙的技巧来实现路由追踪，即调用系统本身自带的 tracert 功能，接收其返回值来用于构建结果。当时的我基本已经处于在无效的信息海洋中翻滚的状态，也没想那么多，就只希望能尽快完成这个任务了。但具体为什么没有选择这个实现方案，则是和后文要提到的图形界面有关，等会再去读吧～

解决包超时问题

总之，当第二天我迷迷糊糊随搜乱翻的时候，看到了 rust 实现 tracert 里对于 Windows 用户的提示：

You may need to set up firewall rules that allow ICMP Time-to-live Exceeded and ICMP Destination (Port) Unreachable packets to be received.
netsh example

Copy
netsh advfirewall firewall add rule name="All ICMP v4" dir=in action=allow protocol=icmpv4:any,any
netsh advfirewall firewall add rule name="All ICMP v6" dir=in action=allow protocol=icmpv6:any,any

我当时完全没有想到防火墙竟然会拦截这些入站的请求包，而 WireShark 之所以能捕捉到可能是因为使用了 WinCap 进一步降低了层级，所以才能捕获到网卡上的纯数据报文。本着试一试的心态，我执行了上述的代码（需要使用管理员权限），结果完全可以用惊喜来形容：

至于 Windows 自带的 tracert 为什么能绕过这个限制，我需要研究研究文章末尾提到的 WinMTR 再说。 因为它调用的是系统提供的动态链接库接口来实现的，而不是手动构建请求报文。 NyaTrace 已经更新了它的路由追踪算法，现在可以不需要加防火墙规则啦 ♥

图形界面

选择图形界面库

基础功能实验成功之后自然就进入到了下一个模块：图形界面。由于最先实验成功的是基于 NodeJS 的包，所以我就想基于它来试试。因为嫌弃 Electron 的资源占用不尽如人意（为了跑个路由追踪容易吗我！），我选择了 nodegui ，并且想试一试它的 React 封装 React NodeGui 。不过当我兴冲冲初始化样例项目，却发现编译器提示错误的时候，算了算了就还是老实点去看基础的用法吧～

于是就用回了最原版的 nodegui 用法，发现它其实调用的是 Qt 引擎库，所以和 Qt 的操作有点相似之处；经过一段时间的折腾，成功拼凑出了一个基础功能还算完整的窗口界面：

运行成功！趁热打铁，写完追踪与内容填充逻辑，点击运行，输入地址，按下开始按钮 ——

友谊的小船说翻就翻。

意识到这条路可能走不通之后，我又返回去研究其他的解决方案，直到后来解决了防火墙导致的包超时问题之后，还是选择了 C++ 作为开发主要使用的语言。

这时候就会进入下一个议题： C++ 的 GUI 库那么多，选择哪一个更好？

学生时代我没少写过 C++ ，也因此稍微接触过 MFC 、 MSVC 与 Qt 这三大经典图形界面库。虽然本项目的开发主要目标是 Windows 平台，但很有可能在未来的某个时间我会将所有的开发环境迁移到其他的平台，例如 Linux 或是 macOS 上。因而为了能确保未来的兼容性，还是选择了 Qt 作为图形库。并且 Qt 可以手捏 UI ，这对于我这种想要偷懒的开发者而言算得上是非常友好了。

但 Qt 本身并不友好，因为它是一款价格极其高昂的商业解决方案（只有企业版和专业版两种付费租赁方案，专业版只比企业版便宜了 8% 这摆明了就是卖企业版 395 USD 每月 啊），免费使用的社区版本只有基础的功能和资源，并且受到其开源许可证的限制。不过对我来说实现功能更重要，也不需要担心开源问题（这个项目本来就是要开源的，我写的东西基本上都开源），所以并不存在这些纠结。

担心 Qt 6 拿开源社区当试验场的行为可能会导致一些意想不到的问题，我使用的是 5 LTS 版本。

事实上这个决策很英明，因为 Qt 6 还没完成 QtLocation 和 QtPositioning 等地图相关组件的迁移工作，所以如果当初选的是 Qt 6 的话，现在的地图功能就加不上去了。

简单拼凑了一下 UI ，然后迭代了几版，截至发稿的时候长这样：

走的依旧是极简风格，把涉及到的功能组件放上去就是了。以后可能还会加一个地图功能，不过现在就先这样吧。

LOGO 使用的是 Nucleo 图标库里面，选择了一个 world-marker 图标，将图钉的颜色从红色改成了我们标志性的蓝色 62b6e7 做出来的，没什么技巧。

线程优化

在开发的时候我遇到一个问题：路由追踪是一个连续且阻塞的过程，如果把追踪的流程函数放在主线程里，通过按下按钮启动，那么在直到结果出现之前，渲染主线程会一直保持阻塞状态，导致程序交互卡顿，且系统会提示程序未响应，无法完成拖动窗口等操作。

Qt 针对这种情况，设计了 QThread 类以方便地管理后台线程的任务，只需设计一个继承 QThread 的类，将会导致阻塞的操作放入 run () 函数中，通过在主线程调用 start () 函数就能启动。

需要注意的是子线程不可以调用 UI 执行变更操作，需要通过 signals 信号槽将处理的结果 emit 给主线程，让主线程来执行 UI 的变更。

缩放优化

Qt 默认的界面排布模式会让窗口放大缩小时其中的组件无法跟着变化，因而变得非常丑。

我在设置排布模式为栅格模式（ Grid ）之后它自己就解决了缩放问题，就很舒适。

IP 数据库读取

MaxMind 的其他语言（ nodejs ， go 等）的客户端 SDK 封装得都很不错，我也以为 C++ 上的客户端会很方便好用，但我忽略了 C++ 并不存在的包管理系统的问题。

官网给出的样例代码里的示例为 C# ，使用 NuGet 进行包管理；但 C / C++ 并不能以同样简单的方法使用，所以很尴尬地只能去找其他的操作方案。

有趣的是，其实官方是有开发 C 操作的客户端的，罗列在 GeoIP2 and GeoLite2 Database Documentation 的 Official API Clients 段中，为 libmaxminddb ，但它看起来似乎需要构建安装，并且似乎并不对 Windows 平台非常友好的样子。

因而还是求助于万能的搜索引擎，但依旧没有什么收获，得到的信息看起来似乎都只是在 Linux 上的构建安装与开发操作，这让我感到很无奈。

其实这时候已经比较疲惫了，有点想放弃，但本着死马当活马医的摆烂心态，直接无脑将项目仓库里的代码文件和头文件加到 NyaTrace 项目中。可能是因为开发者本来就是作为多平台兼容的方式开发的，直接这么使用不但没有报错，而且还省去了编译动态链接库再连接并打包的繁琐流程，这不禁让我大为振奋，甚至好像有点忘记了此时的时间早已是深夜。

但还没高兴透，新的问题出现了：我应该如何调用其中的操作函数？查询了一些中文资料，其不外乎都是把匹配到的 IP 地址所有的信息可视化打印到标准输出，而这严格来讲并不符合我的需求，所以又还是求助于官方文档。

好在官方文档相对较为详细地描述了如何读取数据的调用操作，即先获取完整的 Map Object ，再通过层级 K-V 去选择其中需要的键。

先按照文档和各种资料提示的 dump 用法，取出来所有的数据：

数据很长，这里就只罗列一点点。

按照其中的键层级顺序，使用 MMDB_get_value 函数读取，最后需要填一个 NULL （不太明白为什么，但不填就取不出来）：

我取到了需要的字段。很快我又发现了新的问题，即这些字符串本身并没有使用 \0 作为结尾，导致引用头拉出来的字符串超长，包含了很多无效的数据。

我选择求助于上面那个能正确打印的 MMDB_dump_entry_data_list 函数 —— 阅读其中的代码发现，它使用了 data_size 来规定字段的长度，在提取数据时新建一块空间，并将完整的字符串复制过去，填充尾 0 后返回。

本着同样的思路，我调用了包含这个操作的头文件，却发现由于 C++ 下对于指针类型的定义比 C 严格，原本正常执行的函数此时出现了类型不匹配的报错。并且更糟糕的是， Windows 上似乎并没有实现这个字符串处理函数（也可能是我没看到）。没有别的办法，那就复制过来，对指针执行一次强制类型转换，作为一个独立的工具函数存在。

此时的代码已经变得一团糟，但好在各个模块各自负责的部分没有出现什么冲突，功能还都算正常，所以也就草草混杂在一起打包提交了。后续又执行了一些优化处理，将 IP 读取的调用封装成一个 IPDB 类，在追踪线程启动的时候同时构建这个类，以便在执行过程中执行对象级别的调用，可以方便后续可能的操作升级或是接口分离等等。

到这时， NyaTrace 的基础功能已经基本整理完成了，因此也就有了这篇贴文：

构建打包

这一块就是标准流程了：

切换 Qt 左下角的模式选择为 Release （发布）模式

点击 🔨 按钮构建可执行程序包

找到构建的可执行程序包（一般是在项目的上级目录中，会有 build-项目名-构建环境-Release 命名的工作环境，进入其中的 release 子目录，将构建的 .exe 文件拿出来放到一个空目录中

在开始菜单中找到对应的打包环境命名的控制台（比如 MSVC 构建的那就是 MSVC ， MinGW 构建的那就是 MinGW ），单击它

使用盘符操作和 cd 命令进入刚刚放置 .exe 文件的空目录，执行 windeployqt 可执行文件名.exe 指令，让命令行将需要的动态链接库等文件复制过来（或是生成出来）。蛮多东西的，本来小小一个程序一下子多了一堆运行环境（但依然比 Electron 和 Java 轻就是了）。

此时的程序就可以运行了！

需要注意的是因为我们在使用的时候需要用到 GeoIP2 作为查询依赖 ，所以最好在发布的时候就创建一个名为 mmdb 的空目录，方便指引用户放置数据库进去使用（ MaxMind 的用户协议是不允许在软件打包时带上他们任何的数据库产品的，并且考虑到数据库的时效性，让用户自行下载最新的更好）。

后记

Best Trace 为什么那么快

因为它使用的是异步并发发包的思路，而不是这里实现的同步顺序发包，因而很快能出现对应的结果，超时部分最多也只会触发一轮。

tracert 为什么那么慢

因为它不但使用同步顺序发包，而且每一跳都会发三个包，并且出于某些不知名的原因它即使是三个成功包也会等上几秒；加上有些没法回应的中继就会连续三次都是请求超时，一跳就要吃掉 3 * 3 = 9 秒，所以自然就会显得很慢了。

不过重复发包也有一个好处，就是有些时候中继并不是完全不回包的，而如果刚好能在当它愿意发回包的时候成功接收到了，就能获得它的 IP 地址了。

有没有其他的解决方案

开发完成后，偶然间找到了 WinMTR (Redux) 这个项目，应该可以作为进一步开发路由追踪的核心功能可以使用的参考。

虽然古老，但是好用， Windows 强大的兼容性确实可以（溜

以及它似乎可以无视防火墙的规则，这就更值得好好深入研究一下了！

参考资料

图解 | 9 分钟看懂 traceroute（路由追踪）的原理与实现

TraceRoute 的实现（Windows 下 C/C++ 基于原始套接字）

QThread Class

Qt 中多线程的使用

libmaxminddb - a library for working with MaxMind DB files

Linux C 使用 libmaxminddb 读取 GeoIP2 MMDB 获取 IP 的地理位置

使用 GeoIP2 获取 IP 的地理位置

Qt 如何让窗口随着页面放大而变大

Qt 打包程序详解（适用于 Windows 平台）