這是篇冷門文章,我選了常人不會選的路由器玩法,遇到全球或許只有個位數人口需要面對的 Bug,最後用了在 Vibe Coding 時代才有的方法解決。

想在路由器開發整合應用,我選了一條另類路線,沒用 OpenWRT 跑軟路由,因型號問題沒法改刷魔改韌體,不能用 Docker 領域展開,最後是與官方版韌體鬥智門勇,找可用工具拼湊解決方案。

前幾天將路由器溫度、CPU 及流量數據整合到 Grafana 儀表板時,我發現 BE58 雖然內建 curl 工具,但連外部網站 OK,連 Intranet IP 網站就會閃退,原因不明,吊詭的是沒顯示任何訊息,加了 --verbose 也沒用,怎麼看都像程式有 Bug。原本還想著,在路由器用 cron 設排程定期靠 curl 呼叫 WebAPI 將效能資料上傳 Prometheus Pushgateway,多美妙啊,如意算盤當被摔個粉碎 Orz 後來我的解法是將數據輸出到 syslog,由 ng-syslog 接收 syslog 識別解析再上傳 Prometheus Pushgateway,繞了一大圈,至少問題是解了。

心有不甘,少了 curl 路由器腳本無法直接與外部溝通,當場少掉一條簡潔又高效的整合通道,程式像被毒啞了沒法說話。在舊路由器 AC66U-B1 跑 curl 對照,不論內外部網站都正常,查資料也找到相關討論(會這樣用的人應該很少),加上程式閃退無訊息,加 -v 也沒用,推測 BE58 現行版本內建的 curl 有 Bug 吧。

我先研究了這個議題的背景,AsusWRT 是華碩路由器使用的韌體架構(核心為 Linux),由於路由器的 Flash 儲存空間與 RAM 非常有限,故加入了有「嵌入式 Linux 的瑞士刀」之稱的 BusyBox,以便在 ssh 登入的 Shell 環境提供 ls, cp, mkdir, grep ... 等這些指令。BusyBox 的做法是將數百個指令功能打包在一個僅約數百 KB 到幾 MB 的執行檔,包含大多數標準 Unix 工具的精簡版本,雖然功能不像完整版那麼強大,但足以應付系統管理與腳本執行。系統中看起來像獨立指令的檔案 (例如 /bin/ls),實際上通常只是指向 /bin/busybox 的符號連結。當你執行 ls 時,BusyBox 會根據被呼叫的名稱來決定執行哪項功能,用 ls -l /bin/ls 可以證明。

但 curl 並不屬於 BusyBox,是另外安裝的執行檔,其版本為 7.84.0 (arm-buildroot-linux-gnueabi):

curl 7.84.0 (arm-buildroot-linux-gnueabi) libcurl/7.84.0 OpenSSL/1.1.1t
Release-Date: 2022-06-27
Protocols: file ftp ftps http https mqtt
Features: alt-svc HSTS IPv6 Largefile NTLM SSL threadsafe TLS-SRP UnixSockets

而爬文結果,可能原因是 curl 或其依賴庫(如 OpenSSL、mbedTLS)在編譯時啟用了 -O3 優化,遇到存取地址錯誤會觸發 SIGBUS 或 SIGSEGV,在嵌入式環境的表現便是直接退出且不列印錯誤訊息。另一個可能是 arm-buildroot-linux-gnueabi 通常為 Soft-Foat,若目標機硬體是 Hard-Float 或者連結到 Hard-Float 的動態庫,程式會在進入浮點運算時崩潰。總之,結論是 curl 程式在嵌入環境崩潰閃退是常態,我只用過 PC 跟伺服器等級 Linux 才會大驚小怪。

以我的技能與知識儲備,要修正 curl 如登陸火星,機會渺茫。但在調查過程,我找到一絲希望,BusyBox 沒有 telnet 但有極簡版 nc,原本的 Netcat 是個萬用工具,甚至可以模擬簡單的 HTTP Server,BusyBox 的版本只有開 Socket 傳送跟接收內容的功能,對我這種學過用 TELNET 模擬 HTTP 請求的老人來說,足以成為救命稻草。

BusyBox v1.24.1 (2026-03-03 11:41:07 CST) multi-call binary.

Usage: nc [IPADDR PORT]

Open a pipe to IP:PORT

於是我請 Github Copilot 幫我寫一段 Shell 腳本 (不能用 Bash 語法,AsusWRT 是用 BusyBox 內建的 ash,Almquist Shell) 模擬 curl 的功能(當然,不支援 HTTPS,但能連 HTTP 在我的內網應用已是功德無量),Shell 真是強大又深奧的語言,功能是做出來了,但在我這個麻瓜眼中,根本是一堆去去武器走、阿咯哈姆拉、啊哇咀喀咀啦...

#!/bin/sh
# nc-curl.sh - A simple HTTP client which simulates curl using netcat (nc)
# Usage: ./nc-curl.sh <URL>
# nc-curl.sh http://www.example.com
# nc-curl.sh -X POST -d "a=1&b=2" http://www.example.com/form
# nc-curl.sh -X POST -H "Content-Type: application/json" -d '{"key":"value"}' http://www.example.com/api

METHOD="GET"
DATA=""
HEADERS=""
HAS_CT=0
URL=""

# Parse arguments
while [ $# -gt 0 ]; do
    case "$1" in
        -X) METHOD="$2"; shift 2 ;;
        -d) case "$2" in
                # @- read data from stdin, use sentinel x to keep trailing newlines
                @-) DATA=$(cat; printf x); DATA="${DATA%x}" ;;
                *)  DATA="$2"   ;;
            esac
            shift 2
            ;;
        -H)
            # Check if Content-Type is being set
            case "$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]')" in
                content-type:*) HAS_CT=1 ;;
            esac
            HEADERS="${HEADERS}${2}\r\n"
            shift 2
            ;;
        *)  URL="$1"; shift ;;
    esac
done

if [ -z "$URL" ]; then
    echo "Usage: $0 [-X METHOD] [-H 'Header: Value'] [-d data] <URL>" >&2
    exit 1
fi

case "$URL" in
    https://*)
        echo "Error: HTTPS is not supported." >&2
        exit 1
        ;;
esac

# Parse URL: http://host[:port]/path
URL_NO_SCHEME="${URL#http://}"
HOST_PORT="${URL_NO_SCHEME%%/*}"

case "$URL_NO_SCHEME" in
    */*) PATH_PART="/${URL_NO_SCHEME#*/}" ;;
    *)   PATH_PART="/" ;;
esac

case "$HOST_PORT" in
    *:*) HOST="${HOST_PORT%%:*}"; PORT="${HOST_PORT##*:}" ;;
    *)   HOST="$HOST_PORT";       PORT="80" ;;
esac

# Build HTTP request
REQUEST="${METHOD} ${PATH_PART} HTTP/1.0\r\nHost: ${HOST}\r\n${HEADERS}"

if [ -n "$DATA" ]; then
    CONTENT_LENGTH=$(printf '%s' "$DATA" | wc -c | tr -d ' ')
    [ "$HAS_CT" -eq 0 ] && REQUEST="${REQUEST}Content-Type: application/x-www-form-urlencoded\r\n"
    REQUEST="${REQUEST}Content-Length: ${CONTENT_LENGTH}\r\n\r\n${DATA}"
else
    REQUEST="${REQUEST}\r\n"
fi

# Send request via nc and print response
printf "%b" "$REQUEST" | nc "$HOST" "$PORT"

使用方式模仿 curl:

./nc-curl.sh http://some-web:3150/test
printf 'a=1&b=2' | ./nc-curl.sh -X POST -d @- http://some-web:3150/form
./nc-curl.sh -X POST -d "a=1&b=2" http://some-web:3150/form
./nc-curl.sh -X POST -H "Content-Type: application/json" -d '{"key":"value"}' http://some-web:3150/api
./nc-curl.sh http://some-web:3150/not-found

實際丟上路由器測試 OK,我成功找到 curl 的替代品,未來排程可直接跟內網服務 API 溝通,有種豁然開朗的舒暢~ (灑花)

嵌入裝置 Linux 經驗值 + 5。

最後附上路由器排程直飛 Prometheus Pushgateway 的 CPU、溫度、流量數據更新排程腳本範例,告別經 syslog 轉機的笨做法,爽!

#!/bin/sh

# 一次讀取四個核心的 /proc/stat 快照,回傳格式: "total idle"
snapshot_cpu() {
    cat /proc/stat | awk '/^cpu[0-3] / {
        total = $2+$3+$4+$5+$6+$7+$8
        idle  = $5
        print total, idle
    }'
}
snapshot_net() {
    # /proc/net/dev 欄位: face rx_bytes ... (9 欄) tx_bytes ...
    # 取出 ppp0, wl0.1, wl1.1, wgs1 的 rx_bytes(col2) 及 tx_bytes(col10)
    awk '/^ *(ppp0|wl0\.1|wl1\.1|wgs1):/ {
        gsub(/:/, " ")
        print $1, $2, $10
    }' /proc/net/dev
}

# 第一次快照
cpu_snap1=$(snapshot_cpu)
net_snap1=$(snapshot_net)
sleep 1

# 第二次快照
cpu_snap2=$(snapshot_cpu)
net_snap2=$(snapshot_net)

# --- 收集所有輸出至 $DATA ---
DATA=$(

    # CPU 使用率
    echo "# TYPE router_cpu gauge"
    core=0
    echo "$cpu_snap1" | while IFS= read -r line1; do
        line2=$(echo "$cpu_snap2" | sed -n "$((core + 1))p")
        total1=$(echo "$line1" | awk '{print $1}')
        idle1=$(echo  "$line1" | awk '{print $2}')
        total2=$(echo "$line2" | awk '{print $1}')
        idle2=$(echo  "$line2" | awk '{print $2}')
        usage=$(( 100 * ( (total2 - total1) - (idle2 - idle1) ) / (total2 - total1) ))
        echo "router_cpu{core=\"cpu$((core + 1))\"} $usage"
        core=$((core + 1))
    done

    # 網路每秒 RX / TX bytes
    echo "# TYPE router_network gauge"
    # Use awk for arithmetic to avoid 32-bit integer overflow in shell $(( ))
    echo "$net_snap1" | while IFS= read -r line1; do
        iface=$(echo "$line1" | awk '{print $1}')
        line2=$(echo "$net_snap2" | awk -v iface="$iface" '$1 == iface {print}')
        echo "$line1 $line2" | awk '{printf "router_network{if=\"%s\",dir=\"rx\"} %.0f\nrouter_network{if=\"%s\",dir=\"tx\"} %.0f\n", $1, $5-$2, $1, $6-$3}'
    done

    # --- 溫度 ---
    temp=$(cat /sys/class/thermal/thermal_zone0/temp)
    echo "# TYPE router_temperature gauge"
    echo "router_temperature $((temp / 1000))"

)

printf '%s\n' "$DATA" | ./nc-curl.sh -X POST -d @- http://<pushgateway-ip>:9091/metrics/job/router


Comments

Be the first to post a comment

Post a comment