標籤: MQTT

Wake-on-LAN 的另類玩法:WoL feat. MQTT、ESP8266、reverse SSH

No Comments

前言

Ken 又給了我一台閒置的小主機 UAP-server,讓我自由發揮。他唯一的要求是:平時關機,要用再開起來,用完再關掉;然後撂下一句:「怎麼處理,您自己看著辦。」

平時關機好辦,難在「要用再開起來」:人不在機器旁邊,得從遠端的本站伺服器(waterfalls)把它叫醒。這正是 Wake-on-LAN(WoL)的用途:對目標電腦的網卡送一個「magic packet」,把它從關機狀態叫醒。實際做下去,卻一路過了好幾關。這篇記錄最後的做法,以及過程中踩到的坑。

第一關:網卡與 BIOS

  • 無線網卡不支援喚醒。UAP 原本只連 WiFi,無線網卡是 Qualcomm Atheros AR9485,Linux 下用 ath9k 驅動,這個驅動不支援無線喚醒(WoWLAN)。主機板上另外有 6 個 Intel I211 有線網孔,這款網卡的 WoL 很穩,所以改接一條網路線。
  • BIOS 裡找不到 WoL 選項。這台機器是 AMI Aptio BIOS,翻遍了都沒看到相關設定,所以改在作業系統這一層處理。要注意的是,網卡的 WoL 設定在關機後會退回預設的「關閉」,每次開機都得重新開啟一次;因此寫了一個開機時自動執行的小服務,對每個有線網孔下指令開啟 WoL。
ethtool -s <網孔> wol g

實測在 BIOS 完全不設定的情況下,照樣叫得醒。

第二關:外面的封包根本進不來

UAP 接的是電信 4G 分享器。這類網路的電信商在上游多做了一層位址轉換,外面的連線根本到不了家裡的分享器,所以在分享器上設 port forward 也沒有用。magic packet 只能從同一個區網裡送出。

最早的解法,是借用區網裡另一台小主機當中繼:本站透過 reverse SSH 通道登入那台小主機,由它執行 wakeonlan。實測單發一次偶爾會掉,改成連送 5 次、間隔 3 秒就很穩,10~30 秒內會醒。

但那台小主機平常也會關機,變成「要叫醒 A,得先確定 B 開著」。

第三關:一塊常駐的 ESP8266

關鍵在於,外面連不進來,但裡面可以主動連出去。

所以在同一個區網放一塊 ESP8266。它開機後主動連上 MQTT broker,訂閱一個 topic。本站要喚醒 UAP 時,只要往這個 topic 發一則訊息;板子收到後,就在區網裡廣播 magic packet。板子耗電不到 1W,常駐開著也不心疼,那台小主機就可以照平常習慣關機了。

本站(waterfalls)
   │ ① 發佈喚醒訊息(帶 token)
   ▼
MQTT broker
   │ ② 轉送給訂閱者
   ▼
ESP8266 中繼板(開機後主動連出去訂閱)
   │ ③ 在區網廣播 magic packet
   ▼
UAP-server

只認正主:濾除假冒與非正規的喚醒訊息

本站藉由 MQTT 發佈,穿過這層「外面連不進來」的網路,讓區網裡的 ESP8266 收到「喚醒時機」。然而這則訊息是公開的,一旦被第三方照抄重送,真正的喚醒時機就可能淹沒在一堆假冒的喚醒時機裡。因此,要過濾出真正由本站發出的那一則,我們還需要一個只有雙方知道的憑據:token。

  1. token 怎麼放進板子。走區網內的 HTTP,不經過外網。直接用 codebase 原本就有的 /upload 指令,把一組 32 位的隨機 token 存進板子的檔案系統,本站同時留一份。
  2. 喚醒時。本站把 token 當成 MQTT 訊息的內容發出去。板子拿來和自己存的那組比對,完全相符才送出 magic packet,同樣連送 5 次、間隔 3 秒。
  3. token 只在成功喚醒後才換新。喚醒成功後,UAP 下次關機時會跑一支小程式:產生新 token,先經由 UAP 自己那條 reverse SSH 通道交給本站,再透過區網寫進板子。在成功喚醒之前,token 就算被人冒用也無妨,因為冒用的結果頂多也是把 UAP 叫醒,而這本來就是我們的目的。
  4. 為什麼 token 不讓 ESP8266 自己產生?板子能對外說話的管道只有 MQTT,而 MQTT 上的內容別人也看得到。區網和本站之間唯一的私密管道,是 UAP 開機時才存在的那條 reverse SSH 通道,所以換 token 要趁 UAP 關機前、通道還在的時候做。
  5. 交接順序:先交給本站,再寫進板子。萬一中途失敗,最壞也只是雙方繼續用舊的 token,不會發生「板子換了新的、本站卻不知道」而叫不醒的情況。

大量非正規訊息湧入時

  • 板子把 token 放在記憶體裡比對,每則訊息都比,一則都不漏,也不必每次都去讀檔案系統。
  • 成功喚醒後有 60 秒冷卻時間,同一組 token 重複送來也不會一直發 magic packet。
  • 被拒絕的訊息只計數,每分鐘彙整記錄一次。

實測中,每秒灌進上千則非正規訊息(連同一則正規訊息),板子都不會斷線,灌完 1 秒內就能正常喚醒。不過訊息量大到這個程度時,傳送途中本身就可能掉訊息,所以本站的喚醒腳本會連送 3 輪、每輪間隔 5 秒,這樣實測下看來就萬無一失了。

韌體:延伸既有的 codebase

韌體以 Ken 先前的〈The Most Practical Codebase for ESP8266〉為基礎,新增的功能都用編譯開關包起來,不影響 codebase 原本的行為:

  • MQTT 函式庫:用 AsyncMQTT_Generic 搭配 ESPAsyncTCP,也就是〈MQTT & Websocket〉那篇驗證過的組合。
  • 計時與排程:沿用 codebase 內建的 Timeafter()。
  • 中繼板模式:預設以 STA 模式連上分享器;並關掉 codebase「每開機 10 次強制進 AP 模式」的救援機制,因為沒人照顧的板子一旦進了 AP 模式,就會一直斷線到下次斷電為止。
  • 私密值另外存放:WiFi 帳密、topic 名稱等放在另一個不公開的標頭檔,原始碼本身不含任何祕密。
  • 更新方式:板子部署後沒有接電腦,當然就無法做 com port 有線更新,只能靠 codebase 內建的 OTA 更新。上線前已經實測過多次,更新後檔案系統裡的 token 都還在。

這些改動會累積到之後的 codebase 改版一起發布。

實測結果

  • 透過 MQTT 實際喚醒 UAP,30~40 秒內上線。
  • 原本打算讓板子吃 UAP 的 USB 電,實測才發現 UAP 關機後 USB 就斷電了,所以改插獨立的 USB 充電頭。
  • 長時間關機後的喚醒測試:UAP 關機 1、2、4、8 小時後都成功叫醒,更長時間的測試還在進行中。

踩到的坑

  1. 同一個 SSID 設兩組密碼,並不會換下一組。本想在更換 WiFi 密碼時順便測試看看,讓新舊兩組並存來過渡,翻了 ESP8266 core 裡 ESP8266WiFiMulti.cpp 的原始碼才知道:它挑網路時比的是訊號強度,而且要「嚴格大於」目前的最佳值才會換。同一個 SSID 的第二筆因為訊號強度相同,永遠不會被選上,實測也是一路拿第一組去連。只有 SSID 不同時,它才會換下一組。
  2. WiFi 連線是阻塞式等待。連不上的期間,整個主迴圈都停住,連 LED 都不會閃。所以 LED 不亮,不一定代表沒電。
  3. 打開序列埠,板子就被重開。即使先把 DTR、RTS 設成關閉再打開 port,還是會重開。改用下面的方式設定後,純讀取就不會:
stty -F /dev/ttyUSB0 115200 raw -echo -hupcl

各參數的意思:

  • -F /dev/ttyUSB0:指定要設定的序列埠裝置。
  • 115200:鮑率,要和板子程式裡 Serial.begin() 的設定一致。
  • raw:原始模式,收到什麼就交出什麼,不做換行轉換、也不把特殊字元當控制指令。
  • -echo:不把收到的字元回送給板子。
  • -hupcl:關鍵在這一項。關閉序列埠時不拉動 DTR/RTS 控制線;而這類開發板正是用這兩條線來觸發重開機(和進入燒錄模式)。

設定好之後,程式只以唯讀方式開啟序列埠、不去碰控制線,監看時就不會把板子重開。

  • 在訊息處理函式裡動態配置記憶體。早期版本每拒絕一則訊息,就排一個需要配置記憶體的工作。在大量訊息湧入時,記憶體很快被吃光,改成同一時間只排一個之後就解決了。

小結

這套 WoL 用 MQTT 把「喚醒時機」送進外面連不進來的區網,由 ESP8266 在區網裡喊醒 UAP,再用 reverse SSH 私下遞交 token,確保只有正主發出的訊息算數。各司其職,也算是 Wake-on-LAN 的另類玩法。

Categories: Arduino

Tags: , , ,

PHP Code Snippets Powered By : XYZScripts.com