本文同步發布於 糖菓・部落
這只是一篇開發碎碎念,裡面沒什麼實質性的技術內容,並且 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
andICMP Destination (Port) Unreachable
packets to be received.
netsh
examplenetsh 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 強大的兼容性確實可以(溜
以及它似乎可以無視防火牆的規則,這就更值得好好深入研究一下了!