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 development ramble, and there isn't much substantial technical content in it. The NyaTrace project has also 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 for Meow Nest and the real location display for 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 (with all products following a consulting pricing model), I instinctively began to look for alternative solutions.

Later, a new tool called 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.

Moreover, both of these tools are closed-source products, making code security auditing impossible. For a long time, I 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 connectivity with HE and Censys, and in some special cases, the required data cannot be obtained. Thus, the idea of relying on a locally running environment to perform route tracing tasks was born. Recently, I managed to squeeze out 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 thought that software developed in it is both bloated and highly dependent on the environment. The thought of requiring all users to install a massive disk-hogging application just to implement the functionality of a small toy like route tracing made me feel disheartened. Later, I searched in Chinese for 开源 路由追踪 and found NextTrace, but when I was excited to run it, I discovered that it does not support Windows, which was quite disappointing.

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

I don't remember when I stumbled upon the blog post TraceRoute Implementation (C/C++ based on raw sockets under Windows), but I remember feeling 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 functionality I need (i.e., relying entirely on system interactions without any external components). 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 was that the program could run, and compared to the hell of error messages I usually encountered, it was a different category altogether; the sorrow was that its results were not satisfactory. Except for the last hop, which could obtain an IP, all other packets showed timeouts.

All prompts timed out

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

ICMP TTL exceeded error message packet (dark part)

I thought it was a parameter issue and searched for a long time without results; then I suspected that Windows 11 had changed the underlying socket configuration (the reference article was from 2020), so I looked for related information but found nothing. In despair, I decided to try a different language.

I thought of Python, reasoning that I could just install a larger environment package, which was not impossible. Fortunately, there is a library in Python that supports route tracing, which is Scapy. However, I regrettably found that various documents and blogs seemed to love to take the official vague documentation and translate it into Chinese, then post some seemingly similar code snippets without context. Still, I could not find any valuable information on how to effectively use this route tracing functionality to accomplish a task, so I had to give up.

Later, I discovered 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 rolling in a sea of ineffective information and didn't think too 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, when I was searching randomly the next day, I came across the Rust implementation of 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 because it used 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 result can only be described as a pleasant surprise:

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. It is 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 decided to try it out. Displeased with Electron's resource consumption (is it easy to run a route tracing program?), 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 thought it would be better to stick to the basic usage.

Sample project error

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

nodegui pieced together interface

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

Crashed

The friendship boat capsized just like that.

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

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

During my student days, I wrote quite a bit of C++, and thus I 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, such as Linux or macOS, at some point in the future. Therefore, to ensure future compatibility, I chose Qt as the graphical library. Additionally, 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 because it is an extremely expensive commercial solution (there are only two paid leasing options: enterprise and professional versions, with the professional version being only 8% cheaper than the enterprise version, which clearly indicates the enterprise version costs 395 USD per month). The free community version only has basic functionality and resources and is subject to its open-source license restrictions. 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 these concerns do not apply.

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

In fact, this decision was quite wise because Qt 6 has not yet completed the migration of components related to maps, such as QtLocation and QtPositioning. Therefore, if I had chosen Qt 6 initially, I would not have been able to add the map functionality now.

I quickly pieced together a UI and iterated through several versions, and as of the time of writing, it looks like this:

NyaTrace's UI

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

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 was not particularly tricky.

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, causing the program to become unresponsive and preventing actions like dragging the window.

Stuck

Qt designed the QThread class to conveniently manage background thread tasks in such situations. You only need to design a class that inherits from QThread, 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 change operations directly; it must 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 within the window to not resize when the window is enlarged or reduced, making it look very ugly.

Ugly UI layout after scaling

After setting the layout mode to grid mode (Grid), it resolved the scaling issue by itself, which was very comfortable.

Nice UI layout after scaling

IP Database Reading#

The client SDKs for other languages (nodejs, go, etc.) from MaxMind are well-packaged, and I thought the C++ client would be easy 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 the same simple method, so I awkwardly had to look for other operational solutions.

Interestingly, the official development of a C client does exist, listed in the Official API Clients section of GeoIP2 and GeoLite2 Database Documentation, which is libmaxminddb. However, it seems to require building and installation and 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 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 into 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, connecting, and packaging. This greatly excited me, and I even forgot 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 searching for some Chinese materials, I found that they mostly just printed all the information of the matched IP address to standard output, which strictly speaking did not meet my needs. So I sought help from the official documentation.

Fortunately, the official documentation relatively detailed how to read data, stating that you first need to obtain the complete Map Object and then select the required keys through hierarchical K-V.

Following the dump usage suggested in 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 bit.

According to the key hierarchy, I used the MMDB_get_value function to read, and finally needed to fill in a NULL (I don't quite understand why, but it doesn'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, resulting in the referenced string being excessively long and containing a lot of invalid data.

Incorrect string

I chose to seek help from the MMDB_dump_entry_data_list function that could correctly print—reading its code revealed that it used data_size to specify the length of the field, creating a new space when extracting data 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, the various modules were still functioning correctly without conflicts, so I hastily mixed everything together and submitted it. Later, I performed some optimization, encapsulating the IP reading calls into an IPDB class, which is constructed when the tracing thread starts, allowing for object-level calls during execution, facilitating possible future operational upgrades or interface separation, 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 part follows the 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 the release subdirectory, and take out the built .exe file to place in an empty directory).
  4. In the Start menu, find the console named after the packaging environment (for example, if built with MSVC, it will be MSVC; if built with MinGW, it will be MinGW), and click it.
    Find Qt
  5. Use drive letter operations and the cd command to enter the empty directory where the .exe file is placed, and execute the command windeployqt executable_filename.exe to let the command line copy the required dynamic link libraries and other files (or generate them). A lot of things are needed; originally a small program suddenly requires a bunch of runtime environments (but still lighter than Electron and Java).
  6. The program can now 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 process 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 Best Trace is 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 parts will trigger at most one round.

Why tracert is So Slow#

Because it not only uses synchronous sequential packet sending but also sends three packets for each hop, and for some unknown reasons, it waits a few seconds even for three successful packets; combined with some relays that cannot respond, it can lead to three consecutive timeouts, consuming 3 * 3 = 9 seconds for one hop, making it appear very slow.

However, the repeated packet sending has a benefit: sometimes a relay does not completely ignore packets, and if we happen to receive a packet when it is willing to respond, we can obtain its IP address.

tracert request records

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 still useful, and Windows' strong compatibility is indeed a plus (slips away).

Moreover, it seems to be able to ignore firewall rules, which is even more worth investigating!

References#

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