通過 JNI 移植一個 tracepath 追蹤路由數據鏈給你的應用
背景
Linux 的 tracepath 指令可以追蹤數據到達目標主機的路由信息,同時還能夠發現 MTU 值。它跟蹤路徑到目的地,沿著這條路徑發現 MTU。它使用 UDP 端口或一些隨機端口。它類似于 Traceroute,只是不需要超級用戶權限,并且沒有花哨的選項。
Android 也是移植的它,其源碼放置位置在platform/external/iputils/tracepath6.c。我們之所以直接移植tracepath6.c而不是tracepath.c的原因是 tracepath6 支持 IPV6 和 IPV4 兩種模式,而tracepath.c僅僅支持 IPV4,所以一把梭后我們直接完美兼容了兩種。
最近剛好在調研網絡診斷覆蓋能力,所以順手移植了下它,大致效果如下。
demo 效果
移植后開箱即用地址https://github.com/yanbober/android-tracepath,喜歡就給個小星星唄,一閃一閃亮晶晶。
開始移植
本想撿個現成,去看了platform/external/iputils/tracepath6.c源碼發現這貨直接是寫死默認假定 IPV6 模式的,需要不同模式的話需要自己執行命令時傳遞模式參數,不支持自己動態識別模式,所以需要移植改造。
定義 Java 接口約定
為了方便給 app 使用,需要通過 JNI 包裝到 Java 層接口,約定如下:
- package cn.yan.android.tracepath;
- public final class AndroidTracePath {
- static {
- System.loadLibrary("tracepath-compat");
- }
- private StateListener mStateListener;
- public AndroidTracePath(StateListener stateListener) {
- this.mStateListener = stateListener;
- }
- //業務方調用開始 tracepath 的方法,hostName 是你的域名或者 ip
- public void startTrace(String hostName) {
- nativeInit();
- nativeStartTrace(hostName);
- }
- public native void nativeInit();
- public native void nativeStartTrace(String hostName);
- public void nativeOnStart() {
- if (null != mStateListener) {
- mStateListener.onStart();
- }
- }
- public void nativeOnUpdate(String update) {
- if (null != mStateListener) {
- mStateListener.onUpdate(update);
- }
- }
- public void nativeOnEnd() {
- if (null != mStateListener) {
- mStateListener.onEnd();
- }
- }
- //tracepath 回調狀態
- public interface StateListener {
- void onStart();
- void onUpdate(String update);
- void onEnd();
- }
- }
接著就是對應 JNI 層的接口了,這里沒啥說的,都是老套路,一鍵生成也罷,動態映射也罷,隨意擺弄,反正最終能調用到tracepath6.c源碼就行。
我們重點是改造tracepath6.c,由于這玩意默認編譯后是一個可執行文件,我們通過 cmake 需要當作依賴編譯,所以他的 main 方法入口就不再適合我們了,我們需要進行改造(換個方法名即可),如下:
- //int main(int argc, char **argv) 替換為 tracepath 函數
- int tracepath(int argc, char **argv)
這玩意需要給我們的 JNI 包裝接口(apicompat.c)調用,所以我們給他新建一個頭文件把這個方法報漏一下,如下:
- //tracepath6.h
- #ifndef ANDROIDTRACEPATH_TRACEPATH6_H
- #define ANDROIDTRACEPATH_TRACEPATH6_H
- int tracepath(int argc, char** arg);
- #endif //ANDROIDTRACEPATH_TRACEPATH6_H
手機連著 ipv4 的網絡一運行,臥槽,跪了,定位代碼發現tracepath6.c里面是寫死模式的,需要動態適配,改造點如下:
- ......
- sa_family_t family = AF_UNSPEC; //把這里初值AF_INET6換成AF_UNSPEC
- ......
- int tracepath(int argc, char **argv)
- {
- ......
- memset(&hints, 0, sizeof(hints));
- hints.ai_family = AF_UNSPEC; //把這里family變量換成AF_UNSPEC
- hints.ai_socktype = SOCK_DGRAM;
- hints.ai_protocol = IPPROTO_UDP;
- #ifdef USE_IDN
- hints.ai_flags = AI_IDN;
- #endif
- gai = getaddrinfo(argv[0], pbuf, &hints, &ai0);
- if (gai) {
- fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai));
- return 1;
- }
- fd = -1;
- for (ai = ai0; ai; ai = ai->ai_next) {
- //這里一段判斷family的邏輯刪掉
- if (ai->ai_family != AF_INET6 &&
- ai->ai_family != AF_INET)
- continue;
- family = ai->ai_family;
- fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
- if (fd < 0)
- continue;
- memcpy(&target, ai->ai_addr, sizeof(target));
- targetlen = ai->ai_addrlen;
- break;
- }
- ......
- }
可以看到,貫穿全流程的模式是通過一個全局的 family 變量類型來維護的,默認改為 AF_UNSPEC 后就會自動探測類型,匹配到 AF_INET6 或者 AF_INET 則走自己對應邏輯,這樣就能完美兼容 IPV6 和 IPV4 了。
到此運行能出結果了,但是 tracepath 的打印結果沒法輸出到 Java 層回調中,我們需要繼續改造。常規想法就是一個一個換掉tracepath6.c里面的 printf 函數為 JNI 回調 Java 方法實現,這樣比較麻煩。我們還是采用了一把梭的模式,如下:
- //tracepath6.c
- #include "./../apicompat.h"
- #define printf(...) callbackOnUpdate(__VA_ARGS__)
如上通過一個宏直接替換 printf 為我們 JNI 接口層的 callbackOnUpdate 函數,這個函數的作用就是調用 Java 方法,這樣就能把數據傳遞回去了。
到此基本 ok 了,還差最后的優化,你也看到了,tracepath6.c編寫初衷是一個可執行程序,現在把它移植成 so,所以我們不能在直接 exit 了,相關地方都需要一把梭的替換為 return 解決問題,到此完美解決所有。