標籤: ESP8266

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 通道交給本站,再透過區網寫進板子。(2026-09-29 更正)改為由本站負責:喚醒成功、UAP 一上線,本站就經由 UAP 那條 reverse SSH 通道,借 UAP 在區網內把新 token 寫進板子。UAP 關機就是單純關機,不必再掛任何關機程式。token 只在喚醒那一刻出現在 MQTT 上,上線即換,外露的時間也更短。在成功喚醒之前,token 就算被人冒用也無妨,因為冒用的結果頂多也是把 UAP 叫醒,而這本來就是我們的目的。
  4. 為什麼 token 不讓 ESP8266 自己產生?板子能對外說話的管道只有 MQTT,而 MQTT 上的內容別人也看得到。區網和本站之間唯一的私密管道,是 UAP 開機時才存在的那條 reverse SSH 通道,所以換 token 要趁 UAP 關機前、通道還在的時候做。所以換 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 改版一起發布。

(2026-09-29 更正)完整程式直接附在這裡。它的定位是「以 codebase v1.3 為基礎的應用」,不是 codebase 的新版本;codebase 本身的改版另外再發布。下載後只有兩個檔案:

  • tmpc_esp8266.ino:codebase v1.3 原檔(cDataBase、cAlarm、Timeafter 等都在這一支裡),加上 WoL 中繼的部分。
  • tmpc_secrets.h:私密值(WiFi 帳密、broker、topic、client ID、目標網卡 MAC),附的是中性佔位文字,換成自己的值即可編譯。

相對於 codebase v1.3,異動只有這些(檔頭的修改記錄也逐條列出):

  • 新增 WOL_RELAY_SUPPORT 區塊(約 220 行,編譯開關包住):MQTT 連線與訂閱、token 比對、用 Timeafter() 連送 magic packet、大量訊息湧入時的處理。
  • 新增 TMPC_USE_SECRETS:私密值改由 tmpc_secrets.h 引入。
  • 開啟 WoL 中繼時,跳過「每開機 10 次強制進 AP 模式」的救援機制。
  • 檔頭加上這個應用的說明,以及尚未處理的 todo:OTA 沒有帳密、會寫入的 HTTP 指令沒有檢查金鑰、1883 是明文、WiFi 連線是阻塞式等待、同一 SSID 無法多組密碼輪替。

本站的兩支腳本

以下是精簡後的版本,邏輯與實際運行的相同;私密值以 <…> 佔位。先把目前的 token 存在 token、topic 存在 topic(權限 600)。喚醒:

#!/bin/bash
# wake_uap.sh -- 經 MQTT 叫 ESP8266 中繼板喚醒 UAP;UAP 一上線就換新 token
set -u
DIR=$HOME/wol
BROKER=<MQTT broker>
TOPIC=$(cat "$DIR/topic")        # public/ 底下一個不具描述性的隨機字串
UAP=<UAP 的 ssh 連線名稱>         # 走 UAP 自己連回本站的 reverse SSH 通道

# 連送 3 輪、間隔 5 秒;QoS 1、不設 retained
# token.new 是上次換 token 沒走完留下的,一併送(板子只認其中一組)
for round in 1 2 3; do
    for f in "$DIR/token" "$DIR/token.new"; do
        [ -r "$f" ] && mosquitto_pub -h "$BROKER" -p 1883 -q 1 -t "$TOPIC" -m "$(head -1 "$f")"
    done
    [ $round -lt 3 ] && sleep 5
done

# 等 UAP 上線(最多 150 秒),一上線就換 token
for i in $(seq 15); do
    timeout 8 ssh -o ConnectTimeout=6 "$UAP" true 2>/dev/null && exec "$DIR/rotate_token.sh"
    sleep 10
done
echo "150 秒內未上線,token 不換(可再跑一次)"
exit 2

換 token(喚醒腳本在 UAP 上線後會自動呼叫):

#!/bin/bash
# rotate_token.sh -- 產生新 token,經 UAP 在區網內用 codebase 的 /upload 寫進板子;
# 板子回 303 才正式換用,失敗時雙方續用舊 token
set -u
DIR=$HOME/wol
UAP=<UAP 的 ssh 連線名稱>
BOARD=<板子的區網位址或 mDNS 名稱>

umask 077
new=$(python3 -c 'import secrets; print(secrets.token_hex(16))')
printf '%s\n' "$new" > "$DIR/token.new"      # 先留底,萬一只寫進板子卻沒收到回應也不會失聯

code=$(printf '%s\n' "$new" | timeout 60 ssh "$UAP" \
  'f=$(mktemp); cat > $f
   curl -s -m 10 -o /dev/null -w "%{http_code}" -F "name=@$f;filename=/wol_nonce.txt" http://'"$BOARD"'/upload
   rm -f $f')

if [ "$code" = 303 ]; then
    mv "$DIR/token.new" "$DIR/token"
    echo "token 已更新"
else
    echo "寫入失敗(HTTP $code),續用舊 token"
    exit 1
fi

實測結果

  • 透過 MQTT 實際喚醒 UAP,30~40 秒內上線。
  • 原本打算讓板子吃 UAP 的 USB 電,實測才發現 UAP 關機後 USB 就斷電了,所以改插獨立的 USB 充電頭。
  • 長時間關機後的喚醒測試:UAP 關機 1、2、4、8 小時後都成功叫醒,更長時間的測試還在進行中。長時間關機後的喚醒測試:第一輪 UAP 關機 1、2、4、8、16 小時後都成功叫醒。改成「上線即換 token」之後重新測第二輪:10 分鐘成功(31 秒上線、2 秒換好 token),1.5、24、72 小時還在進行中。

踩到的坑

  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