Nya Candy

Nya Candy

aka. Nya Crypto FSD(Fish Stack Developer) working at @rss3 with friends, Cɾყρƚσ Adventurer. candinya.eth
misskey

Write a routing trace program that runs on Windows

This article is simultaneously published on Candy·Tribe

This is just a collection of development thoughts, with no substantial technical content, and the NyaTrace project has undergone significant changes and achieved many expected goals, so the content of the article may be somewhat outdated; if you are looking for code or executable programs, please visit nyatrace.app.

As the third project after acquiring GeoIP2 (the first two being the login location marking of Meow Nest and the real location display of NyaSpeed), this time I hope to fulfill a long-standing wish: to write a visual route tracing program with detailed IP information.

Source of Inspiration#

You may have heard of the tool 17monipdb.exe or its successor Best Trace, which was once my go-to choice for route tracing work. However, as its developer IPIP.NET gradually shifted towards a closed commercial ecosystem (all products are priced based on consulting), I instinctively began to look for alternative solutions.

Later, a new tool WorstTrace emerged (probably to compete with Best Trace), but its use of Electron packaging resulted in a large size; although the UI is more modern, I still do not consider it a good solution.

Additionally, both of these tools are closed-source products, making code security audits impossible. For a long time, I actually relied on the system's built-in tracert and used HE BGP Toolkit along with Censys Search.

However, this is not a long-term solution. First, it requires manual operation, which is not suitable for quickly assessing link conditions; second, it heavily relies on the connection status with HE and Censys, which may not provide the necessary data in certain special circumstances, thus giving rise to the idea of relying on a local runtime environment to perform route tracing tasks. Recently, I managed to allocate some funds to purchase a one-month subscription for MaxMind's GeoIP2 City and ISP, hoping to make good use of these two databases to fill a long-held gap.

Development Progress#

The first step in the development process is to analyze the requirements, so I broke it down into three modules:

  1. Route Tracing
  2. Graphical Interface
  3. IP Database Reading

Route Tracing#

Finding References#

The first step hit a wall. Searching for route trace open source, the first result was Open Visual Traceroute, a tool developed in Java. Perhaps due to a bias against Java, I always think that software developed in it is both bloated and highly dependent on the environment. The thought of requiring all users to install a huge disk-hogging application just to implement a small toy function like route tracing makes me feel despondent. Later, I searched in Chinese for 开源 路由追踪 and found NextTrace, but when I was excited to run it, I found that it does not support Windows, which was a letdown.

During this time, I found a Golang implementation of traceroute that mentioned the package golang.org/x/net/ipv4, but when I discovered that it does not support Windows functionality, I considered packaging a Docker image based on its TraceRoute example to achieve a Linux implementation under Windows, but it was too complicated, and I dismissed the idea.

I can't remember when I stumbled upon the blog post Implementation of TraceRoute (C/C++ based on raw sockets under Windows), but I remember being moved to tears upon finally finding an article that understood what I was thinking. To get back on track, this blog describes the low-level socket implementation of the route tracing function that I need (i.e., it does not rely on any external components and completely depends on low-level system interactions). After carefully studying the implementation method described, I decided to test the code.

The result can only be described as a mix of joy and sorrow. The joy is that this program runs, and compared to the hell of error messages I usually encounter, it is not in the same category; the sorrow is that its results are not satisfactory, as all packets except the last hop show timeouts.

All prompts timed out

I opened WireShark to capture packets, but found many Time-to-live exceeded packets that clearly had returns, yet the socket's recvFrom could not capture them.

ICMP TTL exceeded error message packet (dark part)

I thought it was a problem with the incoming parameters and searched for a long time without results; then I thought Windows 11 had changed the underlying socket configuration (the reference article was from 2020), and when I looked for related information, I found nothing at all. In despair, I decided to try a different language.

I thought of Python, thinking that at worst I would just have to install a larger environment package, which is not unmanageable. Fortunately, there is a library in Python that supports route tracing, called Scapy. However, unfortunately, I found various documents and blogs, but it seems people always like to take the official vague documentation and translate it into Chinese, then post some seemingly similar code snippets without context. However, I couldn't find any valuable information on how to properly use this route tracing function to accomplish a task, so I had to give up.

I later found the nodejs-traceroute library, which cleverly implements route tracing by calling the system's built-in tracert function and receiving its return values to construct results. At that time, I was basically in a state of rolling in a sea of ineffective information, and I didn't think much about it; I just hoped to complete this task as soon as possible. However, the reason I did not choose this implementation scheme is related to the graphical interface mentioned later, which I will discuss shortly.

Solving Packet Timeout Issues#

In any case, the next day when I was groggy and searching randomly, I saw the Rust implementation tracert with a note for Windows users:

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

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

I had never thought that the firewall would block these inbound request packets, and the reason WireShark could capture them might be due to using WinCap to lower the level further, allowing it to capture raw data packets on the network card. With a mindset of trying it out, I executed the above code (which requires administrator privileges), and the results were surprisingly good:

Successfully captured!

As for why the built-in tracert in Windows can bypass this restriction, I need to study the WinMTR mentioned at the end of the article. Because it calls the dynamic link library interface provided by the system to implement it, rather than manually constructing request packets. NyaTrace has updated its route tracing algorithm, and now it can run without needing to add firewall rules ♥

Graphical Interface#

Choosing a Graphical Interface Library#

After successfully experimenting with the basic functionality, I naturally moved on to the next module: the graphical interface. Since the first successful experiment was based on a NodeJS package, I wanted to try it out. Displeased with Electron's resource consumption (is it easy to run a route tracing tool?), I chose nodegui and wanted to try its React wrapper React NodeGui. However, when I excitedly initialized the sample project, I found that the compiler reported an error, so I decided to stick to the basic usage instead.

Sample project error

So I reverted to the original nodegui usage and discovered that it actually calls the Qt engine library, so there are some similarities with Qt operations. After some time of tinkering, I successfully pieced together a window interface with fairly complete basic functionality:

nodegui pieced together interface

It ran successfully! Seizing the momentum, I wrote the tracing and content filling logic, clicked run, entered the address, and pressed the start button—

Crashed

The friendship boat capsized at will.

Realizing that this path might not work, I returned to explore other solutions until I resolved the firewall-induced packet timeout issue, I still chose C++ as the primary development language.

At this point, the next question arises: with so many C++ GUI libraries, which one is better to choose?

During my student days, I wrote quite a bit of C++, and thus had some exposure to the three classic graphical interface libraries: MFC, MSVC, and Qt. Although the primary development goal of this project is for the Windows platform, it is very likely that I will migrate all development environments to other platforms in the future, such as Linux or macOS. Therefore, to ensure future compatibility, I chose Qt as the graphical library. Moreover, Qt allows for manual UI design, which is very friendly for a developer like me who wants to take shortcuts.

However, Qt itself is not friendly, as it is an extremely expensive commercial solution (there are only two paid leasing options: enterprise and professional, with the professional version being only 8% cheaper than the enterprise version, which clearly indicates the enterprise version costs 395 USD per month), and the free community version only has basic functionality and resources, restricted by its open-source license. However, for me, achieving functionality is more important, and I do not need to worry about open-source issues (this project is intended to be open-source, and most of what I write is open-source), so there are no such dilemmas.

Worried that Qt 6's behavior of using the open-source community as a testing ground might lead to unexpected issues, I opted for the 5 LTS version.

In fact, this decision was very wise, as Qt 6 had not yet completed the migration of map-related components like QtLocation and QtPositioning, so if I had chosen Qt 6 initially, the map functionality could not have been added now.

After quickly piecing together the UI and iterating through several versions, it looks like this at the time of publication:

NyaTrace's UI

It still follows a minimalist style, simply placing the functional components involved. I may add a map feature later, but for now, this is it.

The logo uses an icon from the Nucleo icon library, selecting a world-marker icon and changing the color of the pin from red to our signature blue 62b6e7, which is quite simple.

Thread Optimization#

During development, I encountered a problem: route tracing is a continuous and blocking process. If the tracing function is placed in the main thread and started by pressing a button, the rendering of the main thread will remain blocked until the result appears, leading to program interaction stuttering, and the system will prompt that the program is unresponsive, making it impossible to drag the window, etc.

Stuck

Qt designed the QThread class to conveniently manage background thread tasks for such situations. You only need to design a class that inherits from QThread and place the blocking operations in the run() function, and you can start it by calling the start() function in the main thread.

It is important to note that the child thread cannot call UI execution change operations; it needs to emit the processed results to the main thread through signals and slots, allowing the main thread to execute UI changes.

Scaling Optimization#

The default layout mode of Qt causes components in the window to not change when the window is resized, making it look very ugly.

Ugly UI layout after scaling

After setting the layout mode to grid mode (Grid), it automatically resolved the scaling issue, making it very comfortable.

Nice UI layout after scaling

IP Database Reading#

MaxMind's client SDKs for other languages (nodejs, go, etc.) are well-packaged, and I thought the client for C++ would be very convenient to use, but I overlooked the issue of the non-existent package management system in C++.

The sample code provided on the official website is in C#, using NuGet for package management; however, C/C++ cannot use it in the same simple way, so I awkwardly had to look for other operational solutions.

Interestingly, the official has developed a C client, listed in the Official API Clients section of the GeoIP2 and GeoLite2 Database Documentation, which is libmaxminddb, but it seems to require building and installation, and it does not appear to be very friendly to the Windows platform.

Thus, I sought help from the omnipotent search engine, but still found nothing. The information I obtained seemed to only pertain to building and developing operations on Linux, which left me feeling quite helpless.

At this point, I was already quite fatigued and somewhat wanted to give up, but with a mindset of trying anything, I directly added the code files and header files from the project repository to the NyaTrace project. Perhaps because the developer originally designed it for multi-platform compatibility, using it this way not only did not produce errors but also saved me from the tedious process of compiling dynamic link libraries and linking and packaging them, which greatly excited me, even making me forget that it was already late at night.

However, before I could celebrate too much, a new problem arose: how should I call the operational functions within it? After consulting some Chinese materials, they mostly just visualize and print all the information of the matched IP address to standard output, which strictly speaking does not meet my needs, so I had to consult the official documentation.

Fortunately, the official documentation describes in detail how to read data, which involves first obtaining the complete Map Object and then selecting the required keys through hierarchical K-V.

Following the dump usage suggested by the documentation and various materials, I extracted all the data:

A long string of data read

The data is long, so I will only list a little.

According to the key hierarchy, I used the MMDB_get_value function to read, and finally needed to fill in a NULL (not quite sure why, but it wouldn't work without it):

Calling function to read data

I obtained the required fields. Soon, I discovered a new problem: these strings themselves did not use \0 as a terminator, leading to the referenced string being excessively long, containing a lot of invalid data.

Incorrect string

I chose to consult the MMDB_dump_entry_data_list function that could print correctly—reading its code revealed that it used data_size to specify the length of the field, creating a new space during data extraction and copying the complete string over, filling in the null terminator before returning.

Correct dump function for reading data

Following the same idea, I called the header file containing this operation, but found that due to stricter pointer type definitions in C++ compared to C, the originally functioning function now produced a type mismatch error. Moreover, it seemed that this string processing function was not implemented on Windows (or perhaps I just didn't see it). With no other options, I copied it over, performed a forced type conversion on the pointer, and created an independent utility function.

Another function to copy specified length

At this point, the code had become a mess, but fortunately, each module was responsible for its part without conflicts, and the functionality was still relatively normal, so I hastily mixed it all together and submitted it. I later executed some optimization processes, encapsulating the IP reading calls into an IPDB class, which would be constructed when the tracing thread started, allowing for object-level calls during execution, facilitating possible future operational upgrades or interface separations, etc.

By this time, the basic functionality of NyaTrace had been basically organized, leading to this post:

Completed, let’s celebrate!

Build and Package#

This section follows a standard process:

  1. Switch the mode selection in the lower left corner of Qt to Release mode.
  2. Click the 🔨 button to build the executable package.
  3. Find the built executable package (usually in the parent directory of the project, there will be a working environment named build-ProjectName-BuildEnvironment-Release, enter its release subdirectory, and take out the built .exe file to place it in an empty directory).
  4. Find the console named after the corresponding packaging environment in the start menu (for example, if built with MSVC, it will be MSVC; if built with MinGW, it will be MinGW).
    Find Qt
  5. Use drive letter operations and the cd command to enter the empty directory where the .exe file was placed, and execute the command windeployqt executable_file_name.exe to let the command line copy the required dynamic link libraries and other files (or generate them). A lot of things are needed, and what was originally a small program suddenly requires a bunch of runtime environments (but still lighter than Electron and Java).
  6. At this point, the program can run!

It is important to note that since we need to use GeoIP2 as a query dependency, it is best to create an empty directory named mmdb during the release to guide users to place the database for use (MaxMind's user agreement does not allow including any of their database products in software packaging, and considering the timeliness of the database, it is better for users to download the latest version themselves).

Postscript#

Why is Best Trace so fast?#

Because it uses an asynchronous concurrent packet sending approach, rather than the synchronous sequential packet sending implemented here, it can quickly produce corresponding results, and the timeout part will trigger at most one round.

Why is tracert so slow?#

Because it not only uses synchronous sequential packet sending, but also sends three packets for each hop, and for some unknown reason, even if all three packets succeed, it will still wait a few seconds; coupled with some relays that cannot respond, it will lead to three consecutive timeouts, consuming 3 * 3 = 9 seconds for one hop, so it naturally appears very slow.

However, repeated packet sending has a benefit: sometimes relays do not completely ignore packets, and if we happen to receive a packet when they are willing to respond, we can obtain their IP address.

tracert request record

Are there other solutions?#

After completing the development, I accidentally found the project WinMTR (Redux), which could serve as a reference for further developing the core functionality of route tracing.

Although it is old, it is useful, Windows' strong compatibility indeed can (sneak away)

Moreover, it seems to ignore firewall rules, which makes it even more worthy of in-depth research!

References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.