Explorar o código

feature: router mode

Dew-OF-Aurora hai 1 semana
pai
achega
828a33ab07
Modificáronse 7 ficheiros con 157 adicións e 114 borrados
  1. 2 0
      .gitignore
  2. 1 1
      CLAUDE.md
  3. 85 17
      README.md
  4. 2 0
      router_local.conf
  5. 56 93
      scripts/router_local_http.sh
  6. 10 2
      scripts/router_local_update.sh
  7. 1 1
      workflow.md

+ 2 - 0
.gitignore

@@ -2,3 +2,5 @@ runtime/
 cfip_runtime/
 .claude/
 cfst*/
+busybox*/
+vmess/

+ 1 - 1
CLAUDE.md

@@ -152,7 +152,7 @@ There are two router-side entry styles:
   - parses CSV and writes `cfip_runtime/`
 - BusyBox shell mode via `router_local.conf`
   - `scripts/router_local_update.sh`
-  - `scripts/router_local_http.sh`
+  - `scripts/router_local_http.sh` prefers the configured full BusyBox binary (`BUSYBOX_BIN`, default `./busybox_armv7l`) and uses its `httpd` applet; ASUS/system `httpd` is not used automatically
   - intended for routers without Python
 
 ### 4) Sub-Store consumer

+ 85 - 17
README.md

@@ -3,7 +3,7 @@
 一个用于选择优选目标并写出运行时文件的工具集,当前支持两套独立模式:
 
 - 云服务器模式:调用远程 API,选择优选域名,写入 `runtime/`,可自动提交到 `runtime-state`
-- 本地路由器模式:调用本地 `cfst`,选择优选 IP,写入 `cfip_runtime/`,可通过 BusyBox `nc` 暴露到局域网
+- 本地路由器模式:调用本地 `cfst`,选择优选 IP,写入 `cfip_runtime/`;HTTP 暴露推荐使用项目目录下的完整 BusyBox `httpd`
 
 这两套模式已经彻底拆开:
 
@@ -28,7 +28,7 @@
 - `scripts/router_local_update.sh`
   BusyBox `sh` 路由器入口,执行 `cfst` 并写出 `cfip_runtime`
 - `scripts/router_local_http.sh`
-  BusyBox `sh` 路由器 HTTP 暴露入口,使用 `nc` 暴露 TXT/JSON
+  BusyBox `sh` 路由器 HTTP 暴露入口,优先使用 BusyBox `httpd` 暴露运行时文件
 - `scripts/update_vmess_links.py`
   可选工具,用运行时文件里的值批量替换 `vmess://` 节点
 
@@ -99,9 +99,8 @@
 适用场景:
 
 - 路由器没有 Python
-- 只有 BusyBox `sh` / `awk` / `sed` / `nc`
-- 直接在路由器上跑 `cfst`
-- 需要在局域网暴露 HTTP
+- 只有 BusyBox `sh` / `awk` / `sed` 等基础工具即可执行更新脚本
+- HTTP 暴露推荐使用项目目录下的完整 BusyBox,例如 `BUSYBOX_BIN="./busybox_armv7l"`,脚本会调用它的 `httpd` applet;系统自带精简 `nc` 不够
 
 入口:
 
@@ -335,25 +334,26 @@ python3 scripts/domain_updater.py --config config.router.json --print-output-set
 
 ### 6.1 适用环境
 
-- 路由器架构已确认
+- 路由器没有 Python
 - 已准备对应架构的 `cfst`
-- 路由器只有 BusyBox,没有 Python
+- 已准备完整 BusyBox,例如本项目默认使用 `busybox_armv7l`
+- HTTP 暴露使用完整 BusyBox 的 `httpd` applet,不使用 ASUS 固件自带 `/usr/sbin/httpd`
 
-你之前确认的路由器架构是
+架构以目标路由器实际输出为准
 
 ```bash
 uname -m
-# armv7
 ```
 
-所以你需要放入可在 `armv7` 上运行的 `cfst` 二进制
+`cfst` 和完整 BusyBox 都必须匹配路由器架构。ASUS RT-AC68U 示例里,完整 BusyBox 文件名使用 `busybox_armv7l`
 
 ### 6.2 路由器目录建议
 
-示例:
+示例(ASUS RT-AC68U 推荐放在 `/jffs/vmess`)
 
 ```text
-/tmp/home/root/vmess-domain-rotator/
+/jffs/vmess/
+├── busybox_armv7l
 ├── router_local.conf
 ├── scripts/
 │   ├── router_local_update.sh
@@ -366,7 +366,8 @@ uname -m
 
 其中:
 
-- `router_local.conf` 指定 `CFST_WORK_DIR`、输出目录、HTTP 端口等
+- `router_local.conf` 指定 `CFST_WORK_DIR`、输出目录、HTTP 端口、完整 BusyBox 路径等
+- `busybox_armv7l` 是你下载的完整 BusyBox,HTTP 服务脚本会优先调用它,而不是系统自带 BusyBox/ASUS `httpd`
 - `cfst/` 放路由器架构对应的 `cfst`
 
 ### 6.3 配置 router_local.conf
@@ -389,6 +390,8 @@ uname -m
   当前值文本文件,默认 `current_ip.txt`
 - `VALUE_JSON_FILE`
   当前值 JSON 文件,默认 `current_ip.json`
+- `BUSYBOX_BIN`
+  完整 BusyBox 二进制路径,默认 `./busybox_armv7l`;HTTP 脚本会优先调用它的 `httpd` applet,避免误用 ASUS `/usr/sbin/httpd`
 - `HTTP_PORT`
   局域网 HTTP 监听端口,默认 `8080`
 
@@ -407,6 +410,7 @@ sh scripts/router_local_update.sh ./router_local.conf
 生成文件默认在:
 
 - `cfip_runtime/current_ip.txt`
+- `cfip_runtime/index.html`(复制当前 IP 文本,供 `/` 访问)
 - `cfip_runtime/current_ip.json`
 - `cfip_runtime/state.json`
 - `cfip_runtime/substore_vars.json`
@@ -423,9 +427,17 @@ sh scripts/router_local_http.sh ./router_local.conf
 0.0.0.0:8080
 ```
 
+这个脚本不再使用系统自带的精简 `nc` 或 ASUS `/usr/sbin/httpd`。它会读取 `router_local.conf` 里的 `BUSYBOX_BIN`,优先调用项目目录下完整 BusyBox 的 `httpd` applet:
+
+```bash
+./busybox_armv7l httpd -f -p 8080 -h ./cfip_runtime
+```
+
+如果你的 BusyBox applet 列表里没有 `httpd`,当前脚本会明确报错。ASUS 固件自带的 `/usr/sbin/httpd` 通常是路由器管理后台服务,不是 BusyBox 静态文件服务器;即使支持 `-p` 端口参数,也不代表支持 `-h ./cfip_runtime` 这类目录发布。
+
 可访问路径:
 
-- `/`
+- `/`(由 `index.html` 返回当前 IP 文本)
 - `/current_ip.txt`
 - `/current_ip.json`
 - `/state.json`
@@ -438,7 +450,61 @@ curl http://192.168.50.1:8080/current_ip.json
 curl http://192.168.50.1:8080/current_ip.txt
 ```
 
-### 6.6 定时执行
+### 6.6 ASUS RT-AC68U services-start 自启动
+
+如果路由器使用 `/jffs/scripts/services-start` 统一启动任务,可以用 `cru` 注册定时更新和 watchdog。这个小节是 ASUS RT-AC68U / KoolShare 风格固件专用示例,假设项目放在 `/jffs/vmess`,完整 BusyBox 位于 `/jffs/vmess/busybox_armv7l`。
+
+部署前建议确认:
+
+```sh
+chmod +x /jffs/vmess/busybox_armv7l
+chmod +x /jffs/scripts/services-start
+/jffs/vmess/busybox_armv7l --list | grep '^httpd$'
+```
+
+`router_local.conf` 至少需要包含:
+
+```sh
+BUSYBOX_BIN="./busybox_armv7l"
+HTTP_PORT="8080"
+```
+
+把 `/jffs/scripts/services-start` 写成:
+
+```sh
+#!/bin/sh
+/koolshare/bin/ks-services-start.sh
+
+VMESS_DIR="/jffs/vmess"
+CONFIG="./router_local.conf"
+UPDATE_LOG="/tmp/router_local_update.log"
+HTTP_LOG="/tmp/router_http.log"
+
+start_vmess_http() {
+  if ps | grep -v grep | grep -q 'router_local_http.sh'; then
+    return 0
+  fi
+
+  cd "$VMESS_DIR" || exit 1
+  nohup sh scripts/router_local_http.sh "$CONFIG" >> "$HTTP_LOG" 2>&1 &
+}
+
+cd "$VMESS_DIR" || exit 1
+sh scripts/router_local_update.sh "$CONFIG" >> "$UPDATE_LOG" 2>&1
+
+cru d vmess_rotate
+cru a vmess_rotate "0 */3 * * * cd $VMESS_DIR && sh scripts/router_local_update.sh $CONFIG >> $UPDATE_LOG 2>&1"
+
+sleep 10
+start_vmess_http
+
+cru d vmess_watchdog
+cru a vmess_watchdog "*/5 * * * * if ! ps | grep -v grep | grep -q 'router_local_http.sh'; then cd $VMESS_DIR && nohup sh scripts/router_local_http.sh $CONFIG >> $HTTP_LOG 2>&1 & fi"
+```
+
+`router_local_http.sh` 会保留 wrapper 进程并等待子进程里的完整 BusyBox `httpd`,所以 watchdog 用 `router_local_http.sh` 作为进程关键字可以正常判断服务是否仍在运行。示例里先 `cru d` 再 `cru a`,避免 `services-start` 被重复触发后留下旧的定时任务定义;启动 HTTP 前也会先检查进程,避免重复监听同一个端口。
+
+### 6.7 定时执行
 
 可以用 BusyBox `crond` 定时更新,例如每 15 分钟执行一次:
 
@@ -446,7 +512,9 @@ curl http://192.168.50.1:8080/current_ip.txt
 */15 * * * * cd /tmp/home/root/vmess-domain-rotator && sh scripts/router_local_update.sh ./router_local.conf >> /tmp/router_local_update.log 2>&1
 ```
 
-HTTP 服务如果需要常驻,建议独立后台启动:
+### 6.8 手动常驻 HTTP
+
+HTTP 服务如果需要手动后台启动:
 
 ```bash
 cd /tmp/home/root/vmess-domain-rotator
@@ -512,5 +580,5 @@ cat cfip_runtime/current_ip.json
 2. `run_update_and_commit.sh` 设计目标是服务器模式;路由器模式默认不走 git 提交。
 3. 服务器模式下,service 用户和 git 凭证用户必须一致,否则会出现 `terminal prompts disabled`。
 4. `credential.helper store` 是明文存储,只适合可控服务器。
-5. BusyBox 路由器模式下,`nc`、`mkfifo`、`awk` 的行为依赖 BusyBox 版本,建议在目标设备上实测
+5. BusyBox 路由器模式下,`router_local_update.sh` 只依赖常见基础 applet;HTTP 暴露会优先调用 `BUSYBOX_BIN` 指向的完整 BusyBox `httpd`,不要依赖 ASUS `/usr/sbin/httpd` 或系统精简 `nc`
 6. `state.json` 需要持久化,否则 fallback 不可用。

+ 2 - 0
router_local.conf

@@ -31,4 +31,6 @@ VALUE_JSON_KEY="ip"
 STATE_LAST_GOOD_KEY="last_good_ip"
 EXPORT_VALUE_KEY="AUTO_CFIP"
 
+BUSYBOX_BIN="./busybox_armv7l"
+
 HTTP_PORT="8080"

+ 56 - 93
scripts/router_local_http.sh

@@ -13,114 +13,77 @@ fi
 # shellcheck disable=SC1090
 . "$CONFIG_PATH"
 
+BUSYBOX_BIN=${BUSYBOX_BIN:-"$APP_DIR/busybox_armv7l"}
 RUNTIME_DIR=${RUNTIME_DIR:-"$APP_DIR/cfip_runtime"}
 VALUE_TEXT_FILE=${VALUE_TEXT_FILE:-"current_ip.txt"}
-VALUE_JSON_FILE=${VALUE_JSON_FILE:-"current_ip.json"}
-STATE_FILE=${STATE_FILE:-"state.json"}
-EXPORT_VARS_FILE=${EXPORT_VARS_FILE:-"substore_vars.json"}
 HTTP_PORT=${HTTP_PORT:-8080}
 
 TEXT_PATH="$RUNTIME_DIR/$VALUE_TEXT_FILE"
-JSON_PATH="$RUNTIME_DIR/$VALUE_JSON_FILE"
-STATE_PATH="$RUNTIME_DIR/$STATE_FILE"
-EXPORT_PATH="$RUNTIME_DIR/$EXPORT_VARS_FILE"
-TMP_BASE=${TMPDIR:-/tmp}
-
-nc_listen() {
-  if nc -h 2>&1 | grep -qi 'busybox'; then
-    nc -l -p "$HTTP_PORT"
-  else
-    nc -l "$HTTP_PORT"
-  fi
-}
+INDEX_PATH="$RUNTIME_DIR/index.html"
 
-serve_once() {
-  req_fifo="$TMP_BASE/router_http_req.$$"
-  resp_fifo="$TMP_BASE/router_http_resp.$$"
+resolve_busybox() {
+  if [ -n "$BUSYBOX_BIN" ] && [ -x "$BUSYBOX_BIN" ]; then
+    printf '%s\n' "$BUSYBOX_BIN"
+    return 0
+  fi
 
-  rm -f "$req_fifo" "$resp_fifo"
-  mkfifo "$req_fifo" "$resp_fifo"
+  if [ -x "$APP_DIR/busybox_armv7l" ]; then
+    printf '%s\n' "$APP_DIR/busybox_armv7l"
+    return 0
+  fi
 
-  cat "$resp_fifo" | nc_listen > "$req_fifo" 2>/dev/null &
-  nc_pid=$!
-  sleep 1
-  if ! kill -0 "$nc_pid" 2>/dev/null; then
-    rm -f "$req_fifo" "$resp_fifo"
-    echo "[router-http] nc listen failed on port $HTTP_PORT" >&2
-    return 1
+  if command -v busybox >/dev/null 2>&1; then
+    command -v busybox
+    return 0
   fi
 
-  exec 3<"$req_fifo"
-  exec 4>"$resp_fifo"
+  return 1
+}
+
+busybox_has_applet() {
+  busybox_bin="$1"
+  applet="$2"
+  "$busybox_bin" --list 2>/dev/null | grep "^$applet$" >/dev/null
+}
 
-  if ! IFS= read -r request_line <&3; then
-    exec 3<&-
-    exec 4>&-
-    wait "$nc_pid" 2>/dev/null || true
-    rm -f "$req_fifo" "$resp_fifo"
+ensure_index() {
+  if [ ! -f "$TEXT_PATH" ]; then
     return 0
   fi
 
-  while IFS= read -r header_line <&3; do
-    [ "$header_line" = "$(printf '\r')" ] && break
-    [ -z "$header_line" ] && break
-  done
-
-  request_path=$(printf '%s' "$request_line" | awk '{print $2}')
-  status_line="HTTP/1.1 200 OK\r"
-  content_type="text/plain; charset=utf-8"
-  file_path="$TEXT_PATH"
-
-  case "$request_path" in
-    /|/current_ip.txt|"/$VALUE_TEXT_FILE")
-      content_type="text/plain; charset=utf-8"
-      file_path="$TEXT_PATH"
-      ;;
-    /current_ip.json|"/$VALUE_JSON_FILE")
-      content_type="application/json; charset=utf-8"
-      file_path="$JSON_PATH"
-      ;;
-    /state.json|"/$STATE_FILE")
-      content_type="application/json; charset=utf-8"
-      file_path="$STATE_PATH"
-      ;;
-    /substore_vars.json|"/$EXPORT_VARS_FILE")
-      content_type="application/json; charset=utf-8"
-      file_path="$EXPORT_PATH"
-      ;;
-    *)
-      status_line="HTTP/1.1 404 Not Found\r"
-      file_path=""
-      ;;
-  esac
-
-  if [ -n "$file_path" ] && [ -f "$file_path" ]; then
-    content_length=$(wc -c < "$file_path" | tr -d ' ')
-    printf '%b\n' "$status_line" >&4
-    printf 'Content-Type: %s\r\n' "$content_type" >&4
-    printf 'Content-Length: %s\r\n' "$content_length" >&4
-    printf 'Connection: close\r\n' >&4
-    printf '\r\n' >&4
-    cat "$file_path" >&4
-  else
-    body='not found'
-    printf '%b\n' "$status_line" >&4
-    printf 'Content-Type: text/plain; charset=utf-8\r\n' >&4
-    printf 'Content-Length: %s\r\n' "$(printf '%s' "$body" | wc -c | tr -d ' ')" >&4
-    printf 'Connection: close\r\n' >&4
-    printf '\r\n' >&4
-    printf '%s' "$body" >&4
+  rm -f "$INDEX_PATH"
+  cp "$TEXT_PATH" "$INDEX_PATH"
+}
+
+start_httpd() {
+  busybox_bin=$(resolve_busybox || true)
+  if [ -n "$busybox_bin" ] && busybox_has_applet "$busybox_bin" httpd; then
+    echo "[router-http] serving $RUNTIME_DIR on 0.0.0.0:$HTTP_PORT via $busybox_bin httpd"
+    "$busybox_bin" httpd -f -p "$HTTP_PORT" -h "$RUNTIME_DIR" &
+    httpd_pid=$!
+    trap 'kill "$httpd_pid" 2>/dev/null || true' INT TERM EXIT
+    wait "$httpd_pid"
+    exit $?
+  fi
+
+  if [ -n "$busybox_bin" ]; then
+    echo "[router-http] selected BusyBox has no httpd applet: $busybox_bin" >&2
+  fi
+  if command -v httpd >/dev/null 2>&1; then
+    echo "[router-http] found system httpd at $(command -v httpd), but it is not used" >&2
+    echo "[router-http] ASUS firmware httpd is usually the router admin web server and does not support serving an arbitrary -h directory" >&2
   fi
 
-  exec 3<&-
-  exec 4>&-
-  wait "$nc_pid" 2>/dev/null || true
-  rm -f "$req_fifo" "$resp_fifo"
+  echo "[router-http] usable BusyBox httpd applet not found" >&2
+  echo "[router-http] set BUSYBOX_BIN in router_local.conf to your full BusyBox binary, for example: BUSYBOX_BIN=\"./busybox_armv7l\"" >&2
+  exit 1
 }
 
-echo "[router-http] listening on 0.0.0.0:$HTTP_PORT"
-while true; do
-  if ! serve_once; then
-    exit 1
-  fi
-done
+if [ ! -d "$RUNTIME_DIR" ]; then
+  echo "[router-http] runtime dir not found: $RUNTIME_DIR" >&2
+  echo "[router-http] run: sh scripts/router_local_update.sh $CONFIG_PATH" >&2
+  exit 1
+fi
+
+ensure_index
+start_httpd

+ 10 - 2
scripts/router_local_update.sh

@@ -50,6 +50,7 @@ CURRENT_TEXT_PATH="$RUNTIME_DIR/$VALUE_TEXT_FILE"
 CURRENT_JSON_PATH="$RUNTIME_DIR/$VALUE_JSON_FILE"
 STATE_PATH="$RUNTIME_DIR/$STATE_FILE"
 EXPORT_VARS_PATH="$RUNTIME_DIR/$EXPORT_VARS_FILE"
+INDEX_PATH="$RUNTIME_DIR/index.html"
 RESULT_PATH="$CFST_WORK_DIR/$CFST_RESULT_FILE"
 UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
 
@@ -110,13 +111,20 @@ build_top_candidates_json() {
   ' "$RESULT_PATH"
 }
 
+write_text_value() {
+  selected_ip="$1"
+  printf '%s\n' "$selected_ip" > "$CURRENT_TEXT_PATH"
+  rm -f "$INDEX_PATH"
+  cp "$CURRENT_TEXT_PATH" "$INDEX_PATH"
+}
+
 write_success_outputs() {
   selected_ip="$1"
   top_candidates_json="$2"
   source_count="$3"
 
   escaped_ip=$(json_escape "$selected_ip")
-  printf '%s\n' "$selected_ip" > "$CURRENT_TEXT_PATH"
+  write_text_value "$selected_ip"
   cat > "$CURRENT_JSON_PATH" <<EOF
 {
   "$VALUE_JSON_KEY": "$escaped_ip",
@@ -155,7 +163,7 @@ write_error_with_fallback() {
   escaped_ip=$(json_escape "$last_good_ip")
   escaped_error=$(json_escape "$error_message")
 
-  printf '%s\n' "$last_good_ip" > "$CURRENT_TEXT_PATH"
+  write_text_value "$last_good_ip"
   cat > "$CURRENT_JSON_PATH" <<EOF
 {
   "$VALUE_JSON_KEY": "$escaped_ip",

+ 1 - 1
workflow.md

@@ -75,7 +75,7 @@ flowchart TD
 
     subgraph R2["router_local_http.sh"]
       direction TB
-      C1[read router_local.conf] --> C2[listen with nc]
+      C1[read router_local.conf] --> C2[start configured BusyBox httpd]
       C2 --> C3[serve current_ip.txt]
       C2 --> C4[serve current_ip.json]
       C2 --> C5[serve state.json]