我部 MacBook 靠一個大疆 4G 模組上網。每次合埋部機再打開,網絡就會斷線。
模組盞燈仲係綠色,即係佢本身冇死,亦仲註冊緊電訊商個網絡。但喺 macOS 嘅網絡設定入面,張網卡就顯示「未連接」。唯一有效嘅處理方法,就係拔咗個模組再插過。
呢個問題困擾咗我好耐。到最後有一日,我決定認真捉一次蟲。搞咗大半日,行咗三次冤枉路,最後個答案其實只係一行 code。呢篇文章想記低嘅,就係嗰三條冤枉路,因為佢哋甚至比個答案本身更值得記低。
先搞清楚究竟衰咗乜
出事嗰陣,第一步係睇清楚當時嘅狀態:
1 | $ ifconfig en6 | grep -E 'status|inet ' |
條 link 係 inactive,冇 IP,亦冇 DHCP lease。
但更重要嘅係,要睇下仲有啲乜嘢喺度:network interface en6 冇消失,hardware port 個名仲喺度,MAC address 冇變,network service 嘅綁定亦冇變。攞出事前後嘅 output 做 diff,連一行差異都冇。
所以,呢個唔係「唔見咗個 device」,而係「個 device 仲喺度,但條 link 起唔返」。
有咗呢個判斷,第一時間梗係試下 restart 個 interface:
1 | sudo ifconfig en6 down && sleep 2 && sudo ifconfig en6 up |
冇用,status 依然係 inactive。
再試強制 renew DHCP lease:
1 | sudo ipconfig set en6 NONE && sudo ipconfig set en6 DHCP |
一樣冇用。事後諗返,其實呢個結果係必然嘅:條 link down 咗嗰陣,連 DHCP packet 都 send 唔到;要先有 link,之後先講得上 lease。係我將個次序調返轉咗。
行錯路一:信咗 ifconfig 個 status
查到一半,出現咗一個好矛盾嘅情況:網絡設定話「未連接」,但我上到網;調返轉亦試過,ifconfig 顯示 inactive,實際條 connection 又係通嘅。
查咗一輪先確定:呢個 driver 嘅 status 同系統設定個 panel,兩樣都信唔過。
有一次嘅 output 係咁:
1 | en6: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 |
RUNNING 已經 set 咗,下面卻又寫住 status: inactive,兩個結果互相矛盾。USB 網卡 driver 冇 update media state,其實一啲都唔出奇;但如果唔知,就會對住個假 signal 查足半日。
真正信得過嘅只有兩樣:
1 | ping -c2 192.168.225.1 # ping gateway |
Packet 數量唔會呃你。後來成套 monitoring logic 都以 ping gateway 為準,唔再睇 status。
學到嘅嘢:用一件工具觀察之前,要先驗證佢本身信唔信得過。
行錯路二:整好咗先至去測
呢個伏更加低級,但真係好容易中。
出事嗰陣上唔到網,但捉蟲又要查資料、做記錄。所以我下意識先拔插模組,等網絡恢復,之後先至行 diagnostic commands。
結果所有 diagnostic 都係喺正常狀態下做,全部 return 正常,乜都證明唔到。有幾次我甚至對住一個已經恢復正常嘅 interface,不停行 ifconfig down/up,跟住諗唔明「點解個 fix 冇效」——梗係冇效,因為根本冇嘢要整。
更麻煩嘅係,拔插會令 macOS 重新 enumerate 個 device。呢個時候,ifconfig 見到嘅可能係上一次留低嘅 interface;對住呢個殘留 object 做任何操作,都只會搞亂視線。
學到嘅嘢:排查網絡問題之前,要先準備一條獨立嘅後備網絡,例如手機 hotspot 或者 Wi-Fi。 否則就會跌入一個死循環:要查資料就要先整好,整好咗又冇咗個故障現場。
行錯路三:估錯咗層級
確定「link layer 壞咗」之後,我沿住 macOS 個 network stack,由上而下試咗一輪:
| 懷疑邊一層出事 | 點樣驗證 | 結果 |
|---|---|---|
| SystemConfiguration 唔見咗個 service | networksetup -detectnewhardware |
冇用,個 service 根本冇消失過 |
| configd state 亂咗 | launchctl kickstart -k system/com.apple.configd |
冇用 |
| CDC-ECM driver detach 咗 | kmutil unload/load com.apple.driver.usb.cdc.ecm |
做唔到,SIP 唔畀 |
| MAC randomization 令系統當佢係新網卡 | 對出事前後嘅 hardware port list 做 diff | 冇差異,可以排除 |
MAC randomization 呢條路值得講多少少。模組個 MAC address 係 ee:33:...,第二個 hexadecimal digit 係 e,轉做 binary 就係 1110;locally administered bit 係 1,即係一個 locally administered address。唔少模組每次重新 enumerate 都會 randomly generate 一個新 MAC address。咁樣 macOS 就會當佢係一張全新網卡,唔會綁定任何 network service,睇落就好似「唔見咗張網卡」。
呢個推測好合理,可惜估錯咗。出事前後嘅 diff,一行差異都冇。
仲有一條路,我原本以為行得通,最後先發現根本唔存在:向模組 send AT command,叫佢 restart 條 data connection。
1 | $ ls /dev/cu.* |
系統入面冇模組嘅 serial port。呢個模組就算正常運作,亦唔會出現 /dev/cu.*;佢完全靠 network interface 管理。換句話講,AT command 呢條路由頭到尾都行唔通。
我一度因為「唔見咗個 serial port」,就推斷「成個模組喺 USB bus 度甩咗」。呢個推斷亦係錯嘅,因為個 serial port 根本從來冇出現過。攞一樣由始至終都唔存在嘅嘢,當成「缺失」嘅證據,個推論自然唔會啱。
轉機:一條 ioreg
真正嘅突破,來自呢條 command:
1 | $ ioreg -rc IOUSBHostDevice -k "USB Product Name" -l -w0 | grep -iE "USB Product Name|idVendor|idProduct" |
個 device 仲喺 USB bus 上面,四個 interface 全部仲掛喺度。VID 0x2CA3 係大疆,PID 係 0x4006。
到呢一步就可以確定:唔係個 device 甩咗,唔係 driver detach,亦唔係 network settings 亂咗。真正嘅問題係,一個 USB composite device wake up 之後,CDC-ECM data function 嘅 alternate setting 冇重新 negotiate。MacBook 合埋嗰陣,macOS 會 suspend 個 USB port;wake up 之後,呢個 function 停咗喺 alt 0(冇 data),所以條 link 係 down,但個 device 本身仍然好端端咁留喺度。
既然個 device 仲喺 bus 上面,就仲有一條路:強制叫佢重新 enumerate,等同用 software 拔插一次。
macOS 冇 Linux 嗰種 unbind/bind,但 IOKit 有 USBDeviceReEnumerate,而 pyusb 嘅 dev.reset() call 嘅正正就係呢個 API:
1 | import usb.core |
喺故障狀態下行一次:
1 | $ ifconfig en6 | grep -E 'status|inet ' |
網絡返咗嚟,條線完全冇郁過。查咗大半日,個答案就係一行 dev.reset()。
由「整得返」到「自己識整」
手動整得返,仲未算真正解決。下一步係將佢做成 daemon;呢一段又中咗幾個工程實作上嘅伏。
寫死 sleep 會誤判。 第一版喺 reset 之後硬等 15 秒就判斷結果,log 寫住「重設無效」,但 ifconfig 顯示其實已經恢復。後來改成每 2 秒 poll 一次,最多等 60 秒。順便仲發現 reset script 尾段有個多餘嘅 time.sleep(8),同外面嗰個 sleep 8 重複咗。刪走之後,恢復時間由 10 秒縮短到 4 秒。
計時間要用 timestamp。 用 loop 次數乘 interval 去估並唔準確;用 $(date +%s) 前後相減,計出嚟先至係實際時間。
DHCP 要行啱層。 ipconfig set en6 DHCP 只會改 BSD layer,SystemConfiguration 完全唔知。結果係網絡用得,但設定 panel 永遠顯示「未連接」。改用 networksetup -setdhcp "Baiwang" 之後,panel 個狀態同步返,DNS 亦一齊重新設定,而且 macOS 恢復咗對張網卡嘅正常管理。幾得意嘅係,改完之後,連系統本身 wake up 嗰陣嘅恢復行為都變返正常。
Device name 會變。 重新 enumerate 之後,BSD name 有機會由 en6 變成 en7。所以個 script 會按 hardware port name 動態 resolve,而且 reset 前後會各自 resolve 一次。
Concurrent run 同 signal handling。 Scheduled job 同手動執行會撞埋一齊,log 亦試過出現重複記錄。於是我利用 mkdir 嘅 atomicity 做 directory lock,再為個 lock 加上 10 分鐘 timeout 自動復原,避免個 process 畀 kill -9 終止之後冇行到 trap,令個 lock 永久留低,之後每一輪都直接 skip。另外,trap 要寫齊 EXIT INT TERM;只寫 EXIT,收到 SIGTERM 嗰陣係唔會 trigger 嘅。
Backoff 期間唔好霸住個 lock。 連續失敗之後直接 sleep 1800 並唔啱,因為呢段時間個 lock 會一直畀佢霸住;就算模組自己恢復,個程式亦察覺唔到。最後改成 touch 一個 timestamp file,下一次執行時再 check 個 timestamp 過咗幾耐。
launchd 嘅 environment 比你平時個 shell 乾淨得多。 pyusb 依賴 ctypes.util.find_library 搵 libusb,喺受限 environment 入面可能會搵唔到。手動執行成功,唔代表喺 launchd 入面一樣得;一定要用 env -i 模擬一個空白 environment,再測一次。
State files 唔好放喺 /tmp。 一個 globally writable 嘅 directory,加上一個會用 root 身份寫 file 嘅 process,就會形成 symlink attack surface。將啲 file 搬去 /var/run/dj4hub/,再將 permission set 做 700;另一個好處係 reboot 之後會自動清空。
最後完成嘅版本係咁:LaunchDaemon 每 30 秒 ping 一次 gateway,通嘅話就即刻退出,開銷近乎零;唔通就 reset USB,再一路 poll 到確認恢復。實測需時 4 至 5 秒,通常我仲未察覺,佢就已經自己處理好。
1 | 2026-09-04 00:10:45 en6 無回應(gateway 192.168.225.1),正在 reset USB |
返轉頭睇
今次捉蟲,真正用嚟解決問題嘅時間唔使十分鐘,其餘時間全部花咗喺三個錯誤假設上面。佢哋有一個共通點:全部都係未驗證檢查方法之前,就用檢查結果去推斷。
- 信咗
ifconfig status,但對呢個 driver 嚟講,佢畀嘅係假 signal - 整好咗先至去測,check 緊一個已經唔存在嘅故障
- 用「唔見咗個 serial port」去推斷個 device 甩線,但個 serial port 根本從來冇出現過
嗰條 ioreg command 之所以成為轉機,唔係因為佢高級啲,而係因為佢係當時唯一一個直接睇到硬件實際狀態嘅方法。之前所有工具查嘅,都只係 macOS 對 hardware state 嘅 cache 同 abstraction;偏偏今次個 fault,就發生喺 abstraction 同現實脫節嘅位置。
排查 system issue 嗰陣,值得先問自己一句:我手上呢件工具,睇到嘅究竟係實際情況,定係某一層嘅 cache?
Source code 同完整嘅排錯記錄放咗喺 github.com/miacmo/dj4hub-deploy。