本期主题:解决ipv6稳定性和DDNS同步问题 前两期回顾: 【超详细】手机搭建服务器 · 第 一 期 【超详细】手机搭建服务器 · 第 二 期 【超详细】手机搭建服务器 · 第 三 期 [!warning]第三期的内容可以不用管 第三期的内容主要是做一个记录,告诉大家在实现手机做服务器这个事情上,有这么一个环节需要去做,如果第三期没有实现成功,不用管,可以直接忽略,然后用本期的代码。因为这一期会有新的代码变更。用本期的代码就好了 前言: [!note]域名服务商推荐 在DDNS同步这个问题上,我推荐大家去华为购买服务器,因为我发现,只有华为的服务器能够做到给免费用户的TTL解析值降低到1s。阿里云是10分钟,腾讯云是5分钟。如果要设置1s,就需要成为付费用户。之前我一直用阿里云的服务器。后来因为这个原因改成华为了。 教程开始 一、如何让ipv6一直保持健 这里先说一下,当你的手机连接wifi以后,不是说你的ipv6就一直可以用了,因为ipv6是有有效期的,一旦过了有效期。ipv6就被废弃了,需要新的ipv6地址。所以ipv6不是大家想的,只要一直连着Wi-Fi,ipv6就是永远有效的。所以第一步,就是如何让手机的ipv6处于一直可用的状态。 1. 打开MT管理器,进入 /data/adb/service.d/ 文件夹,更新 99-phone-server.sh 代码 #!/system/bin/sh # # 99-wifi-keeper.sh # Magisk service.d 启动脚本 # BASE_DIR="/data/local/wifi-keeper" MAIN_SCRIPT="$BASE_DIR/wifi-keeper.sh" LAUNCH_LOG="$BASE_DIR/service.d-launcher.log" mkdir -p "$BASE_DIR" 2>/dev/null echo "============================================================" >> "$LAUNCH_LOG" echo "[$(date '+%Y-%m-%d %H:%M:%S')] service.d launcher started" >> "$LAUNCH_LOG" echo "MAIN_SCRIPT=$MAIN_SCRIPT" >> "$LAUNCH_LOG" if [ ! -f "$MAIN_SCRIPT" ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: main script not found" >> "$LAUNCH_LOG" exit 0 fi chmod 755 "$MAIN_SCRIPT" 2>/dev/null ( echo "[$(date '+%Y-%m-%d %H:%M:%S')] launching main script in background" >> "$LAUNCH_LOG" if command -v nohup >/dev/null 2>&1; then nohup sh "$MAIN_SCRIPT" >/dev/null 2>&1 & else sh "$MAIN_SCRIPT" >/dev/null 2>&1 & fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] launcher finished" >> "$LAUNCH_LOG" ) & exit 0 2 进入 /data/local/ 文件夹,新建 wifi-keeper 文件夹,然后进入 wifi-server 文件夹再新建 wifi-keeper.sh 文件;输入以下内容并保存: #!/system/bin/sh # # wifi-keeper.sh # # 作用: # 1. 开机后自动保障 Wi-Fi 连接。 # 2. 使用 cmd wifi connect-network 主动连接指定 Wi-Fi。 # 3. 不使用 wpa_cli / add-suggestion / add-request。 # 4. 获取 2 开头、非 temporary、带 mngtmpaddr 的长期 IPv6。 # 5. 每隔固定时间检查 IPv6 健康度。 # 6. 如果 IPv6 不健康,则重启 Wi-Fi,再重新连接。 # 7. 如果有效 IPv6 相比上一次发生变化,则执行 DDNS 脚本。 # # ============================================================ # 一、基础配置 # ============================================================ # 开机后等待多久再开始执行主逻辑。 # 原因: # Android 刚开机时,Wi-Fi 服务、网络服务、Magisk service.d 可能还没有完全稳定。 # 这里等待 10 秒,可以减少开机初期误判。 BOOT_DELAY_SECONDS=10 # IPv6 健康度检查间隔。 # 每隔多少秒检查一次是否仍然存在有效 IPv6。 # 你当前要求是每 10 秒检查一次。 IPV6_HEALTH_INTERVAL=10 # 打开 Wi-Fi 后,最多等待多少秒确认 Wi-Fi 已经开启。 # 如果超过这个时间仍然检测不到 Wi-Fi 开启,就认为本轮失败。 WAIT_WIFI_ON_SECONDS=40 # 扫描 Wi-Fi 后等待多少秒,让扫描结果刷新。 # Android 的扫描结果有时不是马上返回,适当等待更稳。 WAIT_SCAN_SECONDS=8 # 发起 Wi-Fi 连接后,最多等待多少秒确认已经连接成功。 # 如果第一个 Wi-Fi 在这个时间内没有连接成功,就尝试下一个 Wi-Fi。 WAIT_CONNECT_SECONDS=60 # Wi-Fi 连接成功后,最多等待多少秒获取有效 IPv6。 # 有些路由器下发 IPv6 会比 Wi-Fi 连接慢,所以这里不能太短。 WAIT_IPV6_SECONDS=80 # 各类等待循环中的检查间隔。 # 比如每 2 秒检查一次 Wi-Fi 是否开启、是否连接成功、IPv6 是否出现。 CHECK_INTERVAL=2 # 当 IPv6 不健康时,需要重启 Wi-Fi。 # 关闭 Wi-Fi 后等待多少秒再重新打开。 WIFI_RESTART_OFF_SECONDS=5 # 连接 Wi-Fi 时是否尝试使用固定 MAC,而不是随机 MAC。 # 1 = 优先使用 -r none # 0 = 不使用 -r none # # 对于手机做服务器,建议为 1。 # 好处: # 路由器里看到的 MAC 更稳定; # DHCP 绑定、设备识别、IPv6 分配可能更稳定。 PREFER_MAC_RANDOMIZATION_NONE=1 # 日志保留天数。 # 超过这个天数的动作日志会被自动删除。 LOG_RETENTION_DAYS=7 # 是否在首次获取到有效 IPv6 时执行 DDNS 脚本。 # 1 = 执行 # 0 = 只记录,不执行 # # 建议为 1。 # 因为手机重启后,即使 IPv6 和上次一样,也可能需要重新同步一次 DDNS。 DDNS_ON_FIRST_IP=1 # DDNS 脚本路径。 # 当检测到有效 IPv6 发生变化时,会执行这个脚本。 # # 执行方式: # sh /data/local/ipv6-ddns/ipv6-ddns.sh 当前IPv6地址 # # 同时也会传递环境变量: # CURRENT_IPV6 # VALID_IPV6 # WIFI_SSID DDNS_SCRIPT="/data/local/ipv6-ddns/ipv6-ddns.sh" # IPv6 历史记录最多保留多少行。 # 这个不是主日志,只是专门记录每次有效 IPv6 变化。 IPV6_HISTORY_MAX_LINES=500 # ============================================================ # 二、Wi-Fi 列表 # ============================================================ # # 格式: # WiFi名称|WiFi密码|加密方式 # # 加密方式: # WPA2 写 WPA2 即可,脚本内部会自动转成 cmd wifi 需要的 wpa2。 # 如果是开放网络,可以写 open。 # # 说明: # 1. 每一行代表一个 Wi-Fi。 # 2. 按照从上到下的顺序优先连接。 # 3. 第一个连接失败,才会尝试下一个。 # 4. 不管系统是否保存过 Wi-Fi,都会用这里写死的名称和密码主动连接。 # WIFI_LIST=' CMCC-Semmering|Semmering|WPA2 ' # ============================================================ # 三、路径配置 # ============================================================ SELF="$0" case "$SELF" in /*) SELF_PATH="$SELF" ;; *) SELF_PATH="$(pwd)/$SELF" ;; esac SCRIPT_DIR="$(cd "$(dirname "$SELF_PATH")" 2>/dev/null && pwd)" [ -n "$SCRIPT_DIR" ] || SCRIPT_DIR="$(pwd)" # 主日志。 # 最新动作在最上面。 LOG_FILE="$SCRIPT_DIR/wifi-keeper.log" # 临时目录。 # 用于临时存放单个动作日志,动作结束后再整体插入到 LOG_FILE 顶部。 # 这个目录不是业务数据,脚本停止后可以删除。 TEMP_DIR="$SCRIPT_DIR/wifi-keeper-temp" # 保存有效 IPv6 记录。 # 格式: # YYYY-MM-DD HH:mm:ss|IPv6地址|来源动作|previous=上一次IPv6 # # 说明: # 1. 只保留这一个 IPv6 记录文件,不再单独保存 last-valid-ipv6.txt。 # 2. 脚本会从这个文件尾部读取最新 IPv6,用于和本次 IPv6 做 diff。 # 3. 只有首次记录或者 IPv6 发生变化时,才会追加新记录。 IPV6_HISTORY_FILE="$SCRIPT_DIR/valid-ipv6-history.txt" mkdir -p "$TEMP_DIR" 2>/dev/null touch "$LOG_FILE" "$IPV6_HISTORY_FILE" 2>/dev/null if [ ! -w "$LOG_FILE" ]; then echo "ERROR: 无法写入日志文件:$LOG_FILE" echo "建议把脚本放到可写目录,例如:/data/local/phone-server/wifi-keeper.sh" exit 1 fi # ============================================================ # 四、全局变量 # ============================================================ WIFI_IFACE="wlan0" ACTION_FILE="" ACTION_NAME="" ACTION_SEQ=0 LAST_CONNECTED_SSID="" LAST_VALID_IPV6="" HEALTH_FAIL_REASON="" # ============================================================ # 五、基础工具函数 # ============================================================ now_ts() { date '+%Y-%m-%d %H:%M:%S' 2>/dev/null } now_epoch() { date '+%s' 2>/dev/null } print_line() { printf '%s\n' "$*" } get_wifi_iface() { IFACE="$(ip link 2>/dev/null \ | sed -n 's/^[0-9][0-9]*: \(wlan[0-9][^: ]*\).*/\1/p' \ | head -n 1 \ | sed 's/@.*//')" [ -n "$IFACE" ] || IFACE="wlan0" print_line "$IFACE" } normalize_security() { SEC="$(print_line "$1" | tr 'A-Z' 'a-z')" case "$SEC" in wap2|wpa-psk|psk) SEC="wpa2" ;; wpa2|wpa3|open|owe) ;; *) SEC="wpa2" ;; esac print_line "$SEC" } # ============================================================ # 六、日志函数 # ============================================================ start_action() { ACTION_NAME="$1" ACTION_SEQ=$((ACTION_SEQ + 1)) TS="$(now_ts)" EPOCH="$(now_epoch)" [ -n "$EPOCH" ] || EPOCH="0" ACTION_FILE="$TEMP_DIR/action.$$.${ACTION_SEQ}.log" { print_line "##### ACTION_BLOCK_BEGIN #####" print_line "============================================================" print_line "===== $TS | $ACTION_NAME | START =====" print_line "============================================================" print_line "LOG_EPOCH: $EPOCH" } > "$ACTION_FILE" } log_detail() { TS="$(now_ts)" if [ -n "$ACTION_FILE" ]; then print_line "[$TS] $*" >> "$ACTION_FILE" else print_line "[$TS] $*" >> "$LOG_FILE" fi } log_multiline() { PREFIX="$1" CONTENT="$2" if [ -n "$CONTENT" ]; then print_line "$CONTENT" | while IFS= read -r LINE || [ -n "$LINE" ]; do log_detail "$PREFIX$LINE" done else log_detail "${PREFIX}<empty>" fi } prune_log_by_days() { [ "$LOG_RETENTION_DAYS" -gt 0 ] 2>/dev/null || return 0 NOW="$(now_epoch)" [ -n "$NOW" ] || return 0 CUTOFF=$((NOW - LOG_RETENTION_DAYS * 86400)) PRUNED="$TEMP_DIR/pruned.$$.log" awk -v cutoff="$CUTOFF" ' BEGIN { inside = 0 block = "" keep = 1 } $0 == "##### ACTION_BLOCK_BEGIN #####" { if (inside == 1 && keep == 1) { printf "%s", block } inside = 1 block = $0 "\n" keep = 1 next } inside == 1 { block = block $0 "\n" if ($1 == "LOG_EPOCH:") { if (($2 + 0) < cutoff) { keep = 0 } } if ($0 == "##### ACTION_BLOCK_END #####") { if (keep == 1) { printf "%s", block } inside = 0 block = "" keep = 1 } next } { print } END { if (inside == 1 && keep == 1) { printf "%s", block } } ' "$LOG_FILE" > "$PRUNED" 2>/dev/null if [ -s "$PRUNED" ] || [ -f "$PRUNED" ]; then mv "$PRUNED" "$LOG_FILE" 2>/dev/null else rm -f "$PRUNED" 2>/dev/null fi } end_action() { RESULT="$1" TS="$(now_ts)" { print_line "[$TS] RESULT: $RESULT" print_line "============================================================" print_line "===== $TS | $ACTION_NAME | END: $RESULT =====" print_line "============================================================" print_line "##### ACTION_BLOCK_END #####" print_line "" } >> "$ACTION_FILE" MERGED="$TEMP_DIR/merged.$$.log" cat "$ACTION_FILE" "$LOG_FILE" > "$MERGED" 2>/dev/null && mv "$MERGED" "$LOG_FILE" rm -f "$ACTION_FILE" 2>/dev/null ACTION_FILE="" ACTION_NAME="" prune_log_by_days } run_cmd_args() { SHOW="$1" shift log_detail "CMD : $SHOW" OUT="$("$@" 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" return "$RC" } # ============================================================ # 七、Wi-Fi 状态判断 # ============================================================ is_wifi_enabled() { STATUS="$(cmd wifi status 2>/dev/null)" print_line "$STATUS" | grep -qi "Wifi is disabled" && return 1 print_line "$STATUS" | grep -qi "Wi-Fi is disabled" && return 1 print_line "$STATUS" | grep -qi "Wifi is enabled" && return 0 print_line "$STATUS" | grep -qi "Wi-Fi is enabled" && return 0 print_line "$STATUS" | grep -qi "Wifi is connected" && return 0 print_line "$STATUS" | grep -qi "Wi-Fi is connected" && return 0 return 1 } is_connected_to_ssid() { TARGET_SSID="$1" STATUS="$(cmd wifi status 2>/dev/null)" print_line "$STATUS" | grep -qi "Wifi is disabled" && return 1 print_line "$STATUS" | grep -qi "Wi-Fi is disabled" && return 1 if print_line "$STATUS" | grep -qi "Wifi is connected" \ && print_line "$STATUS" | grep -F "\"$TARGET_SSID\"" >/dev/null 2>&1; then return 0 fi if print_line "$STATUS" | grep -qi "Wi-Fi is connected" \ && print_line "$STATUS" | grep -F "\"$TARGET_SSID\"" >/dev/null 2>&1; then return 0 fi if command -v iw >/dev/null 2>&1; then IW_STATUS="$(iw dev "$WIFI_IFACE" link 2>/dev/null)" print_line "$IW_STATUS" | grep -q "^Connected to " || return 1 print_line "$IW_STATUS" | grep -F "SSID: $TARGET_SSID" >/dev/null 2>&1 && return 0 fi return 1 } get_connected_ssid() { STATUS="$(cmd wifi status 2>/dev/null)" SSID_NOW="$(print_line "$STATUS" | sed -n 's/.*connected to "\([^"]*\)".*/\1/p' | head -n 1)" if [ -n "$SSID_NOW" ]; then print_line "$SSID_NOW" return 0 fi if command -v iw >/dev/null 2>&1; then iw dev "$WIFI_IFACE" link 2>/dev/null | sed -n 's/^[[:space:]]*SSID: //p' | head -n 1 return 0 fi print_line "" } is_connected_to_any_configured_wifi() { LIST_FILE="$TEMP_DIR/wifi-list-any.$$.txt" printf '%s\n' "$WIFI_LIST" > "$LIST_FILE" while IFS='|' read -r SSID PASS SEC EXTRA || [ -n "$SSID" ]; do [ -z "$SSID" ] && continue case "$SSID" in \#*) continue ;; esac if is_connected_to_ssid "$SSID"; then rm -f "$LIST_FILE" 2>/dev/null return 0 fi done < "$LIST_FILE" rm -f "$LIST_FILE" 2>/dev/null return 1 } # ============================================================ # 八、Wi-Fi 操作函数 # ============================================================ log_status() { log_detail "---------- 当前 Wi-Fi / IP 状态 ----------" run_cmd_args "cmd wifi status" cmd wifi status if command -v iw >/dev/null 2>&1; then run_cmd_args "iw dev $WIFI_IFACE link" iw dev "$WIFI_IFACE" link else log_detail "INFO : 当前系统没有 iw 命令,跳过 iw dev link" fi run_cmd_args "ip addr show dev $WIFI_IFACE" ip addr show dev "$WIFI_IFACE" run_cmd_args "ip -6 addr show dev $WIFI_IFACE" ip -6 addr show dev "$WIFI_IFACE" log_detail "---------- 当前状态结束 ----------" } wait_wifi_enabled() { log_detail "INFO : 等待 Wi-Fi 开启,最多 ${WAIT_WIFI_ON_SECONDS}s" ELAPSED=0 while [ "$ELAPSED" -le "$WAIT_WIFI_ON_SECONDS" ]; do if is_wifi_enabled; then log_detail "OK : Wi-Fi 已开启" return 0 fi sleep "$CHECK_INTERVAL" ELAPSED=$((ELAPSED + CHECK_INTERVAL)) log_detail "WAIT : Wi-Fi 尚未确认开启,已等待 ${ELAPSED}s" done log_detail "ERROR: Wi-Fi 开启超时" return 1 } ensure_wifi_on() { if is_wifi_enabled; then log_detail "OK : Wi-Fi 当前已经开启,不重复打开" return 0 fi log_detail "INFO : Wi-Fi 当前关闭,开始打开" if command -v svc >/dev/null 2>&1; then run_cmd_args "svc wifi enable" svc wifi enable else log_detail "WARN : 当前系统没有 svc 命令,跳过 svc wifi enable" fi run_cmd_args "cmd wifi set-wifi-enabled enabled" cmd wifi set-wifi-enabled enabled wait_wifi_enabled } scan_wifi() { log_detail "INFO : 开始扫描周围 Wi-Fi" run_cmd_args "cmd wifi start-scan" cmd wifi start-scan log_detail "INFO : 等待扫描结果刷新 ${WAIT_SCAN_SECONDS}s" sleep "$WAIT_SCAN_SECONDS" SCAN_RESULT="$(cmd wifi list-scan-results 2>&1)" log_detail "CMD : cmd wifi list-scan-results" log_multiline "SCAN : " "$SCAN_RESULT" LIST_FILE="$TEMP_DIR/wifi-list-scan.$$.txt" printf '%s\n' "$WIFI_LIST" > "$LIST_FILE" while IFS='|' read -r SSID PASS SEC EXTRA || [ -n "$SSID" ]; do [ -z "$SSID" ] && continue case "$SSID" in \#*) continue ;; esac if print_line "$SCAN_RESULT" | grep -F "$SSID" >/dev/null 2>&1; then log_detail "OK : 扫描结果中发现配置 Wi-Fi:$SSID" else log_detail "WARN : 扫描结果中没有发现配置 Wi-Fi:$SSID;仍会继续尝试连接" fi done < "$LIST_FILE" rm -f "$LIST_FILE" 2>/dev/null } wait_connected_to_ssid() { TARGET_SSID="$1" log_detail "INFO : 等待连接到 Wi-Fi:$TARGET_SSID,最多 ${WAIT_CONNECT_SECONDS}s" ELAPSED=0 while [ "$ELAPSED" -le "$WAIT_CONNECT_SECONDS" ]; do if is_connected_to_ssid "$TARGET_SSID"; then LAST_CONNECTED_SSID="$TARGET_SSID" log_detail "OK : 已连接到目标 Wi-Fi:$TARGET_SSID" return 0 fi sleep "$CHECK_INTERVAL" ELAPSED=$((ELAPSED + CHECK_INTERVAL)) log_detail "WAIT : 尚未连接到 $TARGET_SSID,已等待 ${ELAPSED}s" done log_detail "ERROR: 连接 $TARGET_SSID 超时" return 1 } connect_one_wifi() { TARGET_SSID="$1" TARGET_PASS="$2" TARGET_SEC="$(normalize_security "$3")" log_detail "------------------------------------------------------------" log_detail "INFO : 尝试连接 Wi-Fi" log_detail "SSID : $TARGET_SSID" log_detail "SEC : $TARGET_SEC" log_detail "NOTE : 不管系统是否保存过该 Wi-Fi,都会使用 SSID + 密码主动连接" if [ "$TARGET_SEC" = "open" ] || [ "$TARGET_SEC" = "owe" ]; then if [ "$PREFER_MAC_RANDOMIZATION_NONE" = "1" ]; then log_detail "CMD : cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC -r none" OUT="$(cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" -r none 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" if [ "$RC" != "0" ]; then log_detail "WARN : 带 -r none 的 open/owe 连接失败,回退到不带 -r" run_cmd_args "cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC" \ cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" fi else run_cmd_args "cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC" \ cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" fi else if [ "$PREFER_MAC_RANDOMIZATION_NONE" = "1" ]; then log_detail "CMD : cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC \"******\" -r none" OUT="$(cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" "$TARGET_PASS" -r none 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" if [ "$RC" != "0" ]; then log_detail "WARN : 带 -r none 的连接失败,回退到不带 -r" log_detail "CMD : cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC \"******\"" OUT="$(cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" "$TARGET_PASS" 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" fi else log_detail "CMD : cmd wifi connect-network \"$TARGET_SSID\" $TARGET_SEC \"******\"" OUT="$(cmd wifi connect-network "$TARGET_SSID" "$TARGET_SEC" "$TARGET_PASS" 2>&1)" RC="$?" log_multiline "OUT : " "$OUT" log_detail "RC : $RC" fi fi wait_connected_to_ssid "$TARGET_SSID" } connect_by_wifi_list() { LIST_FILE="$TEMP_DIR/wifi-list-connect.$$.txt" printf '%s\n' "$WIFI_LIST" > "$LIST_FILE" INDEX=0 while IFS='|' read -r SSID PASS SEC EXTRA || [ -n "$SSID" ]; do [ -z "$SSID" ] && continue case "$SSID" in \#*) continue ;; esac INDEX=$((INDEX + 1)) log_detail "INFO : 开始尝试第 ${INDEX} 个 Wi-Fi:$SSID" if connect_one_wifi "$SSID" "$PASS" "$SEC"; then log_detail "OK : 第 ${INDEX} 个 Wi-Fi 连接成功:$SSID" rm -f "$LIST_FILE" 2>/dev/null return 0 fi log_detail "WARN : 第 ${INDEX} 个 Wi-Fi 连接失败:$SSID,准备尝试下一个" done < "$LIST_FILE" rm -f "$LIST_FILE" 2>/dev/null log_detail "ERROR: Wi-Fi 列表全部尝试失败" return 1 } # ============================================================ # 九、IPv6 相关函数 # ============================================================ get_valid_ipv6() { ip -6 addr show dev "$WIFI_IFACE" scope global 2>/dev/null | while IFS= read -r ADDR_LINE || [ -n "$ADDR_LINE" ]; do # 只处理 inet6 地址行 set -- $ADDR_LINE [ "$1" = "inet6" ] || continue ADDR_CIDR="$2" IPV6_ADDR="${ADDR_CIDR%%/*}" # 读取下一行 lifetime 信息 IFS= read -r LFT_LINE || LFT_LINE="" # 规则 1:必须是 2 开头 case "$IPV6_ADDR" in 2*) ;; *) continue ;; esac # 规则 2:必须是 mngtmpaddr echo "$ADDR_LINE" | grep -q "mngtmpaddr" || continue # 规则 3:不能是 temporary echo "$ADDR_LINE" | grep -q "temporary" && continue # 规则 4:preferred_lft 不能是 0sec echo "$LFT_LINE" | grep -q "preferred_lft 0sec" && continue echo "$IPV6_ADDR" exit 0 done | head -n 1 } log_ipv6_detail() { IPV6_ALL="$(ip -6 addr show dev "$WIFI_IFACE" 2>&1)" IPV6_GLOBAL="$(ip -6 addr show dev "$WIFI_IFACE" scope global 2>&1)" VALID_IPV6="$(get_valid_ipv6)" log_detail "---------- IPv6 地址详情 ----------" log_multiline "IPv6-ALL : " "$IPV6_ALL" log_multiline "IPv6-GLOBAL : " "$IPV6_GLOBAL" if [ -n "$VALID_IPV6" ]; then log_detail "IPv6-VALID : $VALID_IPV6" log_detail "IPv6-RULE : 命中规则:2 开头 + 非 temporary + mngtmpaddr + preferred_lft 非 0sec" else log_detail "IPv6-VALID : <none>" log_detail "IPv6-RULE : 未找到 2 开头 + 非 temporary + mngtmpaddr 的长期 IPv6" print_line "$IPV6_GLOBAL" | grep -q "temporary" && \ log_detail "IPv6-INFO : 存在 temporary IPv6,但不作为服务器长期地址" print_line "$IPV6_GLOBAL" | grep -E "inet6 f" >/dev/null 2>&1 && \ log_detail "IPv6-INFO : 存在 f 开头 IPv6,不符合要求" print_line "$IPV6_GLOBAL" | grep -q "inet6" || \ log_detail "IPv6-INFO : 当前没有 scope global IPv6" fi log_detail "---------- IPv6 地址详情结束 ----------" } wait_valid_ipv6() { log_detail "INFO : 等待获取有效 IPv6,最多 ${WAIT_IPV6_SECONDS}s" ELAPSED=0 while [ "$ELAPSED" -le "$WAIT_IPV6_SECONDS" ]; do VALID_IPV6="$(get_valid_ipv6)" if [ -n "$VALID_IPV6" ]; then LAST_VALID_IPV6="$VALID_IPV6" log_detail "OK : 已获取有效 IPv6:$VALID_IPV6" return 0 fi sleep "$CHECK_INTERVAL" ELAPSED=$((ELAPSED + CHECK_INTERVAL)) log_detail "WAIT : 尚未获取有效 IPv6,已等待 ${ELAPSED}s" done log_detail "ERROR: 获取有效 IPv6 超时" return 1 } trim_ipv6_history() { [ -f "$IPV6_HISTORY_FILE" ] || return 0 TMP_HISTORY="$TEMP_DIR/ipv6-history.$$.tmp" tail -n "$IPV6_HISTORY_MAX_LINES" "$IPV6_HISTORY_FILE" > "$TMP_HISTORY" 2>/dev/null \ && mv "$TMP_HISTORY" "$IPV6_HISTORY_FILE" 2>/dev/null rm -f "$TMP_HISTORY" 2>/dev/null } ipv6_change_action() { CURRENT_IPV6="$1" SOURCE_ACTION="$2" [ -n "$CURRENT_IPV6" ] || return 0 PREVIOUS_LINE="" PREVIOUS_IPV6="" # 只从 valid-ipv6-history.txt 的最后一行读取上一次 IPv6, # 不再使用独立的 last-valid-ipv6.txt 文件。 if [ -f "$IPV6_HISTORY_FILE" ]; then PREVIOUS_LINE="$(tail -n 1 "$IPV6_HISTORY_FILE" 2>/dev/null)" PREVIOUS_IPV6="$(print_line "$PREVIOUS_LINE" | awk -F'|' '{print $2}')" fi start_action "IPv6地址变更检查" TS="$(now_ts)" CONNECTED_SSID="$(get_connected_ssid)" log_detail "来源动作: $SOURCE_ACTION" log_detail "当前连接 Wi-Fi: ${CONNECTED_SSID:-<none>}" log_detail "有效 IPv6 记录文件: $IPV6_HISTORY_FILE" log_detail "上一条记录: ${PREVIOUS_LINE:-<none>}" log_detail "上一次 IPv6: ${PREVIOUS_IPV6:-<none>}" log_detail "本次 IPv6: $CURRENT_IPV6" if [ -n "$PREVIOUS_IPV6" ] && [ "$CURRENT_IPV6" = "$PREVIOUS_IPV6" ]; then log_detail "DIFF : IPv6 未变化:$CURRENT_IPV6" log_detail "SAVE : IPv6 未变化,不追加有效 IPv6 记录文件" log_detail "DDNS : IPv6 未变化,不执行 DDNS 脚本" end_action "unchanged" return 0 fi if [ -z "$PREVIOUS_IPV6" ]; then log_detail "DIFF : 这是首次记录有效 IPv6" if [ "$DDNS_ON_FIRST_IP" = "1" ]; then SHOULD_RUN_DDNS=1 log_detail "DDNS : DDNS_ON_FIRST_IP=1,首次获取 IPv6 也会执行 DDNS 脚本" else SHOULD_RUN_DDNS=0 log_detail "DDNS : DDNS_ON_FIRST_IP=0,首次获取 IPv6 只记录,不执行 DDNS 脚本" fi else SHOULD_RUN_DDNS=1 log_detail "DIFF : IPv6 已变化:$PREVIOUS_IPV6 -> $CURRENT_IPV6" fi if [ "$SHOULD_RUN_DDNS" != "1" ]; then print_line "$TS|$CURRENT_IPV6|$SOURCE_ACTION|previous=${PREVIOUS_IPV6:-none}|ddns=not_required" >> "$IPV6_HISTORY_FILE" 2>/dev/null log_detail "SAVE : DDNS 不需要执行,已追加有效 IPv6 记录文件:$IPV6_HISTORY_FILE" trim_ipv6_history end_action "done_without_ddns" return 0 fi if [ ! -f "$DDNS_SCRIPT" ]; then log_detail "DDNS : DDNS 脚本不存在,暂不执行:$DDNS_SCRIPT" log_detail "DDNS : 本次 IPv6 不会写入有效 IPv6 记录文件,等待下次健康检查继续重试" end_action "ddns_script_missing" return 1 fi log_detail "DDNS : 准备执行 DDNS 脚本" log_detail "CMD : CURRENT_IPV6=\"$CURRENT_IPV6\" VALID_IPV6=\"$CURRENT_IPV6\" WIFI_SSID=\"$CONNECTED_SSID\" sh \"$DDNS_SCRIPT\" \"$CURRENT_IPV6\"" DDNS_OUT="$( CURRENT_IPV6="$CURRENT_IPV6" \ VALID_IPV6="$CURRENT_IPV6" \ WIFI_SSID="$CONNECTED_SSID" \ sh "$DDNS_SCRIPT" "$CURRENT_IPV6" 2>&1 )" DDNS_RC="$?" log_multiline "DDNS-OUT : " "$DDNS_OUT" log_detail "DDNS-RC : $DDNS_RC" if [ "$DDNS_RC" = "0" ]; then log_detail "DDNS : DDNS 脚本执行成功" print_line "$TS|$CURRENT_IPV6|$SOURCE_ACTION|previous=${PREVIOUS_IPV6:-none}|ddns=success" >> "$IPV6_HISTORY_FILE" 2>/dev/null log_detail "SAVE : DDNS 成功后,已追加有效 IPv6 记录文件:$IPV6_HISTORY_FILE" trim_ipv6_history end_action "done" return 0 fi log_detail "DDNS : DDNS 脚本执行失败" log_detail "SAVE : DDNS 失败,本次 IPv6 不写入有效 IPv6 记录文件,下一轮会继续重试" end_action "ddns_failed" return 1 } # ============================================================ # 十、IPv6 健康检查 # ============================================================ check_ipv6_health() { HEALTH_FAIL_REASON="" CURRENT_SSID="$(get_connected_ssid)" log_detail "INFO : 当前连接 SSID:${CURRENT_SSID:-<none>}" if ! is_connected_to_any_configured_wifi; then HEALTH_FAIL_REASON="当前没有连接到脚本配置的 Wi-Fi" log_detail "ERROR: $HEALTH_FAIL_REASON" log_status log_ipv6_detail return 1 fi log_status log_ipv6_detail VALID_IPV6="$(get_valid_ipv6)" if [ -n "$VALID_IPV6" ]; then LAST_VALID_IPV6="$VALID_IPV6" log_detail "OK : IPv6 健康,有效 IPv6:$VALID_IPV6" return 0 fi HEALTH_FAIL_REASON="没有找到 2 开头、非 temporary、mngtmpaddr 的长期 IPv6 地址" log_detail "ERROR: IPv6 不健康:$HEALTH_FAIL_REASON" return 1 } # ============================================================ # 十一、恢复动作 # ============================================================ restart_wifi_switch() { start_action "Wi-Fi重启恢复" log_detail "INFO : 开始重启 Wi-Fi 开关" log_status if command -v svc >/dev/null 2>&1; then run_cmd_args "svc wifi disable" svc wifi disable else log_detail "WARN : 当前系统没有 svc 命令,跳过 svc wifi disable" fi run_cmd_args "cmd wifi set-wifi-enabled disabled" cmd wifi set-wifi-enabled disabled log_detail "INFO : Wi-Fi 关闭命令已发送,等待 ${WIFI_RESTART_OFF_SECONDS}s" sleep "$WIFI_RESTART_OFF_SECONDS" if command -v svc >/dev/null 2>&1; then run_cmd_args "svc wifi enable" svc wifi enable else log_detail "WARN : 当前系统没有 svc 命令,跳过 svc wifi enable" fi run_cmd_args "cmd wifi set-wifi-enabled enabled" cmd wifi set-wifi-enabled enabled if wait_wifi_enabled; then log_status end_action "success" return 0 fi log_status end_action "failed_wifi_not_enabled" return 1 } ensure_connection_and_ipv6() { REASON="$1" start_action "Wi-Fi连接保障" log_detail "触发原因: $REASON" log_detail "脚本目录: $SCRIPT_DIR" log_detail "主日志: $LOG_FILE" log_detail "临时目录: $TEMP_DIR" log_detail "有效 IPv6 记录文件: $IPV6_HISTORY_FILE" log_detail "Wi-Fi 网卡: $WIFI_IFACE" log_detail "Wi-Fi 列表:" log_multiline " - " "$WIFI_LIST" log_status if ! ensure_wifi_on; then log_status end_action "failed_wifi_not_enabled" return 1 fi scan_wifi if ! connect_by_wifi_list; then log_status end_action "failed_wifi_connect" return 1 fi log_status log_ipv6_detail if wait_valid_ipv6; then log_ipv6_detail CURRENT_VALID_IPV6="$LAST_VALID_IPV6" end_action "success" ipv6_change_action "$CURRENT_VALID_IPV6" "$REASON" return 0 fi log_ipv6_detail end_action "failed_ipv6_not_ready" return 1 } # ============================================================ # 十二、主流程 # ============================================================ # 清理上一次异常退出残留的临时文件。 rm -f "$TEMP_DIR"/action.*.log "$TEMP_DIR"/merged.*.log "$TEMP_DIR"/pruned.*.log 2>/dev/null WIFI_IFACE="$(get_wifi_iface)" start_action "脚本启动" log_detail "START: wifi-keeper" log_detail "PID : $$" log_detail "DIR : $SCRIPT_DIR" log_detail "LOG : $LOG_FILE" log_detail "IFACE: $WIFI_IFACE" log_detail "BOOT_DELAY_SECONDS: $BOOT_DELAY_SECONDS" log_detail "IPV6_HEALTH_INTERVAL: $IPV6_HEALTH_INTERVAL" log_detail "LOG_RETENTION_DAYS: $LOG_RETENTION_DAYS" if ! command -v cmd >/dev/null 2>&1; then log_detail "ERROR: 当前系统没有 cmd 命令,无法使用 cmd wifi" end_action "fatal_no_cmd" exit 1 fi run_cmd_args "id" id run_cmd_args "getprop ro.build.version.release" getprop ro.build.version.release run_cmd_args "getprop ro.build.version.sdk" getprop ro.build.version.sdk run_cmd_args "getprop ro.product.model" getprop ro.product.model run_cmd_args "getprop ro.product.device" getprop ro.product.device run_cmd_args "cmd wifi status" cmd wifi status log_detail "INFO : 开机后先等待 ${BOOT_DELAY_SECONDS}s" sleep "$BOOT_DELAY_SECONDS" log_detail "INFO : 开机等待结束,进入主流程" end_action "success" ensure_connection_and_ipv6 "开机初始化" while true; do sleep "$IPV6_HEALTH_INTERVAL" start_action "IPv6健康度检查" if check_ipv6_health; then CURRENT_VALID_IPV6="$LAST_VALID_IPV6" end_action "healthy" ipv6_change_action "$CURRENT_VALID_IPV6" "IPv6健康度检查" continue fi log_detail "处理策略: IPv6 不健康,准备重启 Wi-Fi 并重新连接" log_detail "失败原因: ${HEALTH_FAIL_REASON:-unknown}" end_action "unhealthy_need_recovery" restart_wifi_switch ensure_connection_and_ipv6 "IPv6健康度异常后的恢复连接" done [!note]重点说下wifi-keeper.sh 这一次wifi-keeper.sh相比较第三期,做出了极大的改变,可以说废弃了第三期的代码。因为我发现了一个重大的问题:在国产的系统中,控制wifi的命令,比如cmd wifi是无法使用的,根本不支持。只有类原生才支持。如果不支持cmd命令就无法做到这些事情: · 无法扫描wifi列表; · 无法查看Wi-Fi状态; · 无法通过wifi名称和Wi-Fi密码连接Wi-Fi(这个才是最致命的); 所以我把手里的MIUI 11刷成了 crDroid 。这里不推荐刷入 Pixel Experience ,因为 Pixel Experience 对cmd wifi的支持也有问题,当然,也可能是小米8SE的包原因,大家自行测试吧。 在wifi-keeper.sh这个脚本中, 大家要修改的地方是WIFI_LIST部分 ,这里需要把你家里的wifi名称和Wi-Fi密码和加密方式写到脚本中,这样不管是你手机重新开关机还是取消保存了wifi,还是开启了飞行模式,脚本都可以很好的自动连接上Wi-Fi。Wi-Fi是不会再断的,哪怕你手动断开wifi,wifi过10s都会自动连接上的。 大家不用担心脚本安全问题,因为脚本是公开透明的。 3.记得修改wifi-keeper.sh文件的权限,可以参考 上一期 如何修改权限,这里就不截图了 二、如何实现ipv6自动更新到DDNS,实现用域名访问 1. 自行去华为云购买服务器 。这里只拿华为云举例,原因开头已经说了。大家自行注册和购买域名。注册好域名后看下一步。 2. 配置AK和SK,后面脚本会用到 2.1 访问 统一身份认证服务IAM ,然后左边菜单找到 用户 ,在 用户 页面 右上角点击创建用户 2.2 如图操作,然后下一步 2.3 到这一步的时候,一定要点击确定下载密钥文件,因为密钥只能在这里下载一次,如果不小心点了取消,只能重新创建用户 3.进入 /data/local/ 文件夹,新建 ipv6-ddns 文件夹,然后进入 ipv6-ddns 文件夹再新建 ipv6-ddns.sh 文件;输入以下内容并保存: [!warning]提醒 记得务必把代码中的AK和SK替换成自己的。 文件同样记得修改权限。 图中域名填写时,小数点不能去掉 图中域名填写时,小数点不能去掉 图中域名填写时,小数点不能去掉 #!/system/bin/sh # # ipv6-ddns.sh # # 作用: # 接收 wifi-keeper.sh 传入的有效 IPv6,然后同步到华为云 DNS AAAA 记录。 # # 核心逻辑: # 1. 接收第一个参数作为新的 IPv6。 # 2. 查询华为云 DNS,获取 ZONE_ID。 # 3. 查询目标 AAAA 记录,读取华为云当前 IPv6。 # 4. 如果华为云当前 IPv6 与传入 IPv6 一致,则不更新。 # 5. 如果不一致,则 PUT 更新记录。 # 6. 每次执行作为一个动作写入日志,最新动作在日志最上方,动作内部细节正序。 # # 注意: # 这个脚本是给 wifi-keeper.sh 调用的: # sh /data/local/ipv6-ddns/ipv6-ddns.sh 2409:xxxx:xxxx::xxxx # # 因为 wifi-keeper.sh 使用 sh 调用 DDNS 脚本,所以本脚本尽量使用 /system/bin/sh 兼容写法, # 不使用 bash 数组。 # # ============================================================ # 一、Termux 工具路径 # ============================================================ # Termux 的安装目录。 # 因为 Magisk service.d 启动的脚本不一定有 Termux 的 PATH, # 所以这里主动把 Termux bin 目录加进去。 PREFIX="/data/data/com.termux/files/usr" export PATH="$PREFIX/bin:/system/bin:/system/xbin:/vendor/bin:$PATH" export LD_LIBRARY_PATH="$PREFIX/lib:${LD_LIBRARY_PATH:-}" # 华为云签名需要 openssl 和 xxd。 # HTTP 请求需要 curl。 OPENSSL_BIN="$PREFIX/bin/openssl" XXD_BIN="$PREFIX/bin/xxd" CURL_BIN="$PREFIX/bin/curl" # 常用命令。 DATE_BIN="/system/bin/date" [ -x "$PREFIX/bin/date" ] && DATE_BIN="$PREFIX/bin/date" MKDIR_BIN="/system/bin/mkdir" [ -x "$PREFIX/bin/mkdir" ] && MKDIR_BIN="$PREFIX/bin/mkdir" CAT_BIN="/system/bin/cat" [ -x "$PREFIX/bin/cat" ] && CAT_BIN="$PREFIX/bin/cat" MV_BIN="/system/bin/mv" [ -x "$PREFIX/bin/mv" ] && MV_BIN="$PREFIX/bin/mv" RM_BIN="/system/bin/rm" [ -x "$PREFIX/bin/rm" ] && RM_BIN="$PREFIX/bin/rm" AWK_BIN="/system/bin/awk" [ -x "$PREFIX/bin/awk" ] && AWK_BIN="$PREFIX/bin/awk" SED_BIN="/system/bin/sed" [ -x "$PREFIX/bin/sed" ] && SED_BIN="$PREFIX/bin/sed" GREP_BIN="/system/bin/grep" [ -x "$PREFIX/bin/grep" ] && GREP_BIN="$PREFIX/bin/grep" TAIL_BIN="/system/bin/tail" [ -x "$PREFIX/bin/tail" ] && TAIL_BIN="$PREFIX/bin/tail" # ============================================================ # 二、用户配置区 # ============================================================ # 华为云访问密钥。 # 为避免把你的 AK/SK 在聊天记录里再次明文展示, # 请把你参考脚本里的 AK 和 SK 填到下面两行。 # # 也可以通过环境变量传入: # HUAWEICLOUD_AK="你的AK" HUAWEICLOUD_SK="你的SK" sh ipv6-ddns.sh 2409:... AK="${填入AK}" SK="${填入SK}" # 华为云 DNS API Host。 HOST="dns.myhuaweicloud.com" # 主域名 Zone 名称。 # 注意:华为云 DNS API 返回的 zone name 通常带结尾点。 ZONE_NAME="自己的域名." # 后面的点不能去掉 # 需要同步的 AAAA 记录列表。 # 一行一个,必须带结尾点。 # 示例: # xxxx.top. # www.xxxx.top. # ssh.xxxx.top. RECORD_NAMES=' xxxx.top. ' # 记录类型,IPv6 固定使用 AAAA。 RECORD_TYPE="AAAA" # DNS TTL。 # 这里沿用你参考脚本里的 TTL=1。 TTL="1" # 调试开关: # 0 = 常规日志 # 1 = 写入更多签名和响应调试信息 # 注意:即使 DEBUG=1,也不会打印 SK。 DEBUG="0" # 日志保留天数。 # 超过这个天数的动作日志会被清理。 LOG_RETENTION_DAYS=7 # HTTP 请求最大重试次数。 # 主要用于处理刚开机、Wi-Fi 刚恢复、IPv6 刚出现时,网络栈还不稳定导致的 curl 56、HTTP 000、5xx 等临时错误。 HTTP_MAX_RETRIES=3 # HTTP 重试基础等待秒数。 # 第 1 次失败后等待 3 秒,第 2 次失败后等待 6 秒。 HTTP_RETRY_BASE_SLEEP_SECONDS=3 # ============================================================ # 三、路径配置 # ============================================================ BASE_DIR="/data/local/ipv6-ddns" LOG_FILE="$BASE_DIR/ipv6-ddns.log" RUNTIME_DIR="$BASE_DIR/.runtime" RUN_LOG="$RUNTIME_DIR/ipv6-ddns.run.$$.log" # ============================================================ # 四、运行参数 # ============================================================ NEW_IPV6="$1" # ============================================================ # 五、全局变量 # ============================================================ RUN_START_TIME="" RUN_START_EPOCH="" SCRIPT_NAME="ipv6-ddns.sh" FINAL_EXIT_CODE=0 LAST_HTTP_STATUS="" LAST_RESPONSE_BODY="" LAST_CURL_RC="" TOTAL_COUNT=0 UPDATED_COUNT=0 SKIPPED_COUNT=0 FAILED_COUNT=0 # ============================================================ # 六、基础函数 # ============================================================ now_ts() { "$DATE_BIN" '+%Y-%m-%d %H:%M:%S' 2>/dev/null } now_epoch() { "$DATE_BIN" '+%s' 2>/dev/null } print_line() { printf '%s\n' "$*" } log_line() { LEVEL="$1" shift TS="$(now_ts)" [ -n "$TS" ] || TS="unknown-time" printf '[%s] %-5s %s\n' "$TS" "$LEVEL" "$*" >> "$RUN_LOG" } log_info() { log_line "INFO" "$*" } log_warn() { log_line "WARN" "$*" } log_error() { log_line "ERROR" "$*" } log_debug() { if [ "$DEBUG" = "1" ]; then log_line "DEBUG" "$*" fi } log_multiline() { PREFIX="$1" CONTENT="$2" if [ -n "$CONTENT" ]; then print_line "$CONTENT" | while IFS= read -r L || [ -n "$L" ]; do log_info "$PREFIX$L" done else log_info "${PREFIX}<empty>" fi } is_placeholder_secret() { case "$1" in *请把参考脚本中的AK填到这里*|*请把参考脚本中的SK填到这里*|"") return 0 ;; *) return 1 ;; esac } mask_text() { VALUE="$1" LEN=${#VALUE} if [ "$LEN" -le 8 ]; then printf '******' else HEAD=$(printf '%s' "$VALUE" | cut -c 1-4) TAIL=$(printf '%s' "$VALUE" | cut -c "$((LEN - 3))"-"$LEN") printf '%s******%s' "$HEAD" "$TAIL" fi } # ============================================================ # 七、日志动作:插入主日志顶部 + 清理旧日志 # ============================================================ prune_log_by_days() { [ "$LOG_RETENTION_DAYS" -gt 0 ] 2>/dev/null || return 0 [ -f "$LOG_FILE" ] || return 0 NOW="$(now_epoch)" [ -n "$NOW" ] || return 0 CUTOFF=$((NOW - LOG_RETENTION_DAYS * 86400)) PRUNED="$RUNTIME_DIR/ipv6-ddns.pruned.$$.log" "$AWK_BIN" -v cutoff="$CUTOFF" ' BEGIN { inside = 0 block = "" keep = 1 } $0 == "##### ACTION_BLOCK_BEGIN #####" { if (inside == 1 && keep == 1) { printf "%s", block } inside = 1 block = $0 "\n" keep = 1 next } inside == 1 { block = block $0 "\n" if ($1 == "LOG_EPOCH:") { if (($2 + 0) < cutoff) { keep = 0 } } if ($0 == "##### ACTION_BLOCK_END #####") { if (keep == 1) { printf "%s", block } inside = 0 block = "" keep = 1 } next } { print } END { if (inside == 1 && keep == 1) { printf "%s", block } } ' "$LOG_FILE" > "$PRUNED" 2>/dev/null if [ -f "$PRUNED" ]; then "$MV_BIN" "$PRUNED" "$LOG_FILE" 2>/dev/null fi } prepend_run_log_to_main_log() { TMP_LOG="$RUNTIME_DIR/ipv6-ddns.merged.$$.log" END_TIME="$(now_ts)" { print_line "##### ACTION_BLOCK_BEGIN #####" print_line "============================================================" print_line "===== $RUN_START_TIME | IPv6-DDNS同步 | START =====" print_line "============================================================" print_line "LOG_EPOCH: $RUN_START_EPOCH" print_line "[$RUN_START_TIME] ACTION : IPv6-DDNS同步" print_line "[$RUN_START_TIME] SCRIPT : $0" print_line "[$RUN_START_TIME] LOG : $LOG_FILE" print_line "[$RUN_START_TIME] ZONE : $ZONE_NAME" print_line "[$RUN_START_TIME] TYPE : $RECORD_TYPE" print_line "[$RUN_START_TIME] TTL : $TTL" print_line "[$RUN_START_TIME] INPUT : $NEW_IPV6" print_line "[$RUN_START_TIME] RESULT : exit_code=$FINAL_EXIT_CODE total=$TOTAL_COUNT updated=$UPDATED_COUNT skipped=$SKIPPED_COUNT failed=$FAILED_COUNT" print_line "------------------------------------------------------------" [ -f "$RUN_LOG" ] && "$CAT_BIN" "$RUN_LOG" print_line "------------------------------------------------------------" print_line "[$END_TIME] RESULT: exit_code=$FINAL_EXIT_CODE total=$TOTAL_COUNT updated=$UPDATED_COUNT skipped=$SKIPPED_COUNT failed=$FAILED_COUNT" print_line "============================================================" print_line "===== $END_TIME | IPv6-DDNS同步 | END =====" print_line "============================================================" print_line "##### ACTION_BLOCK_END #####" print_line "" [ -f "$LOG_FILE" ] && "$CAT_BIN" "$LOG_FILE" } > "$TMP_LOG" "$MV_BIN" "$TMP_LOG" "$LOG_FILE" 2>/dev/null prune_log_by_days } finish() { EXIT_CODE=$? FINAL_EXIT_CODE="$EXIT_CODE" if [ -f "$RUN_LOG" ]; then prepend_run_log_to_main_log "$RM_BIN" -f "$RUN_LOG" 2>/dev/null fi exit "$EXIT_CODE" } # ============================================================ # 八、签名函数:只沿用参考脚本里的 AK/SK 使用方式 # ============================================================ sha256_hex() { DATA="$1" printf '%s' "$DATA" | "$OPENSSL_BIN" dgst -sha256 -binary | "$XXD_BIN" -p -c 256 } hmac_sha256_hex() { KEY="$1" DATA="$2" printf '%s' "$DATA" | "$OPENSSL_BIN" dgst -sha256 -mac HMAC -macopt "key:${KEY}" -binary | "$XXD_BIN" -p -c 256 } is_retryable_http_status() { case "$1" in 000|408|429|500|502|503|504) return 0 ;; *) return 1 ;; esac } signed_request() { METHOD="$1" REQUEST_PATH="$2" CANONICAL_URI="$3" CANONICAL_QUERY="$4" BODY="$5" CONTENT_TYPE="$6" URL="https://${HOST}${REQUEST_PATH}" if [ -n "$CANONICAL_QUERY" ]; then URL="${URL}?${CANONICAL_QUERY}" fi ATTEMPT=1 [ "$HTTP_MAX_RETRIES" -gt 0 ] 2>/dev/null || HTTP_MAX_RETRIES=1 while [ "$ATTEMPT" -le "$HTTP_MAX_RETRIES" ]; do X_SDK_DATE="$(TZ=UTC "$DATE_BIN" +%Y%m%dT%H%M%SZ 2>/dev/null)" PAYLOAD_HASH="$(sha256_hex "$BODY")" if [ -n "$CONTENT_TYPE" ]; then CANONICAL_HEADERS="content-type:${CONTENT_TYPE} host:${HOST} x-sdk-date:${X_SDK_DATE} " SIGNED_HEADERS="content-type;host;x-sdk-date" else CANONICAL_HEADERS="host:${HOST} x-sdk-date:${X_SDK_DATE} " SIGNED_HEADERS="host;x-sdk-date" fi CANONICAL_REQUEST="${METHOD} ${CANONICAL_URI} ${CANONICAL_QUERY} ${CANONICAL_HEADERS} ${SIGNED_HEADERS} ${PAYLOAD_HASH}" HASHED_CANONICAL_REQUEST="$(sha256_hex "$CANONICAL_REQUEST")" STRING_TO_SIGN="SDK-HMAC-SHA256 ${X_SDK_DATE} ${HASHED_CANONICAL_REQUEST}" SIGNATURE="$(hmac_sha256_hex "$SK" "$STRING_TO_SIGN")" AUTHORIZATION="SDK-HMAC-SHA256 Access=${AK}, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}" log_info "HTTP 请求开始: attempt=${ATTEMPT}/${HTTP_MAX_RETRIES} method=$METHOD url=$URL" log_debug "X-Sdk-Date=$X_SDK_DATE" log_debug "SignedHeaders=$SIGNED_HEADERS" log_debug "PayloadHash=$PAYLOAD_HASH" log_debug "CanonicalRequest=$(printf '%s\n' "$CANONICAL_REQUEST" | "$SED_BIN" ':a;N;$!ba;s/\n/|/g')" [ -n "$BODY" ] && log_debug "RequestBody=$BODY" if [ -n "$CONTENT_TYPE" ]; then RESPONSE="$( "$CURL_BIN" -sS \ --connect-timeout 10 \ --max-time 30 \ -w '\n__HTTP_STATUS__:%{http_code}' \ -X "$METHOD" \ "$URL" \ -H "Host: ${HOST}" \ -H "X-Sdk-Date: ${X_SDK_DATE}" \ -H "Authorization: ${AUTHORIZATION}" \ -H "Content-Type: ${CONTENT_TYPE}" \ --data "$BODY" 2>&1 )" CURL_RC="$?" else RESPONSE="$( "$CURL_BIN" -sS \ --connect-timeout 10 \ --max-time 30 \ -w '\n__HTTP_STATUS__:%{http_code}' \ -X "$METHOD" \ "$URL" \ -H "Host: ${HOST}" \ -H "X-Sdk-Date: ${X_SDK_DATE}" \ -H "Authorization: ${AUTHORIZATION}" 2>&1 )" CURL_RC="$?" fi LAST_CURL_RC="$CURL_RC" if [ "$CURL_RC" != "0" ]; then LAST_HTTP_STATUS="CURL_ERROR" LAST_RESPONSE_BODY="$RESPONSE" log_error "HTTP 请求失败: attempt=${ATTEMPT}/${HTTP_MAX_RETRIES} curl_rc=$CURL_RC method=$METHOD url=$URL" log_multiline "curl输出: " "$RESPONSE" if [ "$ATTEMPT" -lt "$HTTP_MAX_RETRIES" ]; then SLEEP_SECONDS=$((HTTP_RETRY_BASE_SLEEP_SECONDS * ATTEMPT)) log_warn "HTTP 请求将重试: ${SLEEP_SECONDS}s 后进行第 $((ATTEMPT + 1)) 次尝试" sleep "$SLEEP_SECONDS" ATTEMPT=$((ATTEMPT + 1)) continue fi log_error "HTTP 请求失败且已达到最大重试次数: $HTTP_MAX_RETRIES" return 1 fi LAST_HTTP_STATUS="$(printf '%s' "$RESPONSE" | "$SED_BIN" -n 's/^__HTTP_STATUS__://p' | "$TAIL_BIN" -n 1)" LAST_RESPONSE_BODY="$(printf '%s' "$RESPONSE" | "$SED_BIN" '/^__HTTP_STATUS__:/d')" [ -n "$LAST_HTTP_STATUS" ] || LAST_HTTP_STATUS="000" log_info "HTTP 请求结束: attempt=${ATTEMPT}/${HTTP_MAX_RETRIES} status=${LAST_HTTP_STATUS} method=$METHOD url=$URL" log_debug "ResponseBody=$LAST_RESPONSE_BODY" if is_retryable_http_status "$LAST_HTTP_STATUS" && [ "$ATTEMPT" -lt "$HTTP_MAX_RETRIES" ]; then SLEEP_SECONDS=$((HTTP_RETRY_BASE_SLEEP_SECONDS * ATTEMPT)) log_warn "HTTP 状态可能是临时错误: status=${LAST_HTTP_STATUS},${SLEEP_SECONDS}s 后进行第 $((ATTEMPT + 1)) 次尝试" sleep "$SLEEP_SECONDS" ATTEMPT=$((ATTEMPT + 1)) continue fi return 0 done return 1 } # ============================================================ # 九、华为云 DNS API 操作函数 # ============================================================ get_zone_id() { signed_request \ "GET" \ "/v2/zones" \ "/v2/zones/" \ "limit=500&type=public" \ "" \ "" if [ "$LAST_HTTP_STATUS" != "200" ]; then log_error "查询 ZONE_ID 失败: HTTP=$LAST_HTTP_STATUS" log_multiline "响应内容: " "$LAST_RESPONSE_BODY" return 1 fi ZONE_ID="$(printf '%s' "$LAST_RESPONSE_BODY" \ | "$SED_BIN" 's/},{/},\ {/g' \ | "$GREP_BIN" -F "\"name\":\"${ZONE_NAME}\"" \ | "$SED_BIN" -n 's/.*"id":"\([^"]*\)".*/\1/p' \ | "$TAIL_BIN" -n 1)" if [ -n "$ZONE_ID" ]; then log_info "解析到 ZONE_ID: $ZONE_ID" return 0 fi log_error "没有从华为云 zones 响应中找到 ZONE_NAME=$ZONE_NAME" return 1 } get_record_line() { G_ZONE_ID="$1" G_RECORD_NAME="$2" signed_request \ "GET" \ "/v2.1/zones/${G_ZONE_ID}/recordsets" \ "/v2.1/zones/${G_ZONE_ID}/recordsets/" \ "limit=500" \ "" \ "" if [ "$LAST_HTTP_STATUS" != "200" ]; then log_error "查询 recordsets 失败: record=$G_RECORD_NAME HTTP=$LAST_HTTP_STATUS" log_multiline "响应内容: " "$LAST_RESPONSE_BODY" return 1 fi MATCH_LINE="$(printf '%s' "$LAST_RESPONSE_BODY" \ | "$SED_BIN" 's/},{/},\ {/g' \ | "$GREP_BIN" -F "\"name\":\"${G_RECORD_NAME}\"" \ | "$GREP_BIN" -F "\"type\":\"${RECORD_TYPE}\"" \ | "$TAIL_BIN" -n 1)" printf '%s' "$MATCH_LINE" return 0 } parse_recordset_id() { printf '%s' "$1" | "$SED_BIN" -n 's/.*"id":"\([^"]*\)".*/\1/p' | "$TAIL_BIN" -n 1 } parse_record_ipv6() { printf '%s' "$1" | "$SED_BIN" -n 's/.*"records":\["\([^"]*\)"\].*/\1/p' | "$TAIL_BIN" -n 1 } parse_record_ttl() { printf '%s' "$1" | "$SED_BIN" -n 's/.*"ttl":\([0-9]*\).*/\1/p' | "$TAIL_BIN" -n 1 } update_recordset() { U_ZONE_ID="$1" U_RECORDSET_ID="$2" U_RECORD_NAME="$3" U_NEW_IPV6="$4" BODY="{\"name\":\"${U_RECORD_NAME}\",\"type\":\"${RECORD_TYPE}\",\"ttl\":${TTL},\"records\":[\"${U_NEW_IPV6}\"]}" log_info "准备更新华为云记录: record=$U_RECORD_NAME recordset_id=$U_RECORDSET_ID" log_info "更新目标 IPv6: $U_NEW_IPV6" log_debug "更新请求 body=$BODY" signed_request \ "PUT" \ "/v2.1/zones/${U_ZONE_ID}/recordsets/${U_RECORDSET_ID}" \ "/v2.1/zones/${U_ZONE_ID}/recordsets/${U_RECORDSET_ID}/" \ "" \ "$BODY" \ "application/json" if [ "$LAST_HTTP_STATUS" = "202" ] || [ "$LAST_HTTP_STATUS" = "200" ]; then log_info "更新请求已被华为云接受: record=$U_RECORD_NAME HTTP=$LAST_HTTP_STATUS" log_debug "更新响应: $LAST_RESPONSE_BODY" return 0 fi log_error "更新记录失败: record=$U_RECORD_NAME HTTP=$LAST_HTTP_STATUS" log_multiline "响应内容: " "$LAST_RESPONSE_BODY" return 1 } # ============================================================ # 十、环境检查与参数检查 # ============================================================ prepare_runtime() { "$MKDIR_BIN" -p "$RUNTIME_DIR" 2>/dev/null touch "$RUN_LOG" 2>/dev/null touch "$LOG_FILE" 2>/dev/null RUN_START_TIME="$(now_ts)" RUN_START_EPOCH="$(now_epoch)" [ -n "$RUN_START_TIME" ] || RUN_START_TIME="unknown-time" [ -n "$RUN_START_EPOCH" ] || RUN_START_EPOCH="0" } check_environment() { log_info "脚本开始执行" log_info "接收到的 IPv6 参数: ${NEW_IPV6:-<empty>}" log_info "脚本路径: $0" log_info "日志文件: $LOG_FILE" log_info "运行目录: $RUNTIME_DIR" log_info "华为云 HOST: $HOST" log_info "主域名 ZONE_NAME: $ZONE_NAME" log_info "记录类型: $RECORD_TYPE" log_info "目标 TTL: $TTL" log_info "AK: $(mask_text "$AK")" log_info "SK: ******" if is_placeholder_secret "$AK" || is_placeholder_secret "$SK"; then log_error "AK/SK 尚未配置。请把参考脚本中的 AK/SK 填入本脚本,或通过 HUAWEICLOUD_AK/HUAWEICLOUD_SK 环境变量传入。" return 1 fi if [ ! -x "$OPENSSL_BIN" ]; then log_error "找不到 openssl: $OPENSSL_BIN" return 1 fi if [ ! -x "$XXD_BIN" ]; then log_error "找不到 xxd: $XXD_BIN" return 1 fi if [ ! -x "$CURL_BIN" ]; then log_error "找不到 curl: $CURL_BIN" return 1 fi log_info "工具检查通过: openssl=$OPENSSL_BIN" log_info "工具检查通过: xxd=$XXD_BIN" log_info "工具检查通过: curl=$CURL_BIN" if [ -z "$NEW_IPV6" ]; then log_error "没有收到 IPv6 参数。正确用法: sh $0 2409:xxxx:xxxx::xxxx" return 1 fi if ! printf '%s' "$NEW_IPV6" | "$GREP_BIN" -qiE '^[0-9a-f:]+$'; then log_error "IPv6 格式看起来不正确: $NEW_IPV6" return 1 fi case "$NEW_IPV6" in 2*) log_info "IPv6 参数基础校验通过: 以 2 开头" ;; *) log_warn "IPv6 参数不是 2 开头: $NEW_IPV6" log_warn "wifi-keeper.sh 正常情况下只会传入 2 开头的长期 IPv6,这里仍继续执行。" ;; esac return 0 } # ============================================================ # 十一、主流程 # ============================================================ process_one_record() { P_ZONE_ID="$1" P_RECORD_NAME="$2" TOTAL_COUNT=$((TOTAL_COUNT + 1)) log_info "------------------------------------------------------------" log_info "开始处理记录: $P_RECORD_NAME" RECORD_LINE="$(get_record_line "$P_ZONE_ID" "$P_RECORD_NAME")" GET_RECORD_RC="$?" if [ "$GET_RECORD_RC" != "0" ]; then log_error "查询目标 AAAA 记录失败,无法判断记录是否存在: $P_RECORD_NAME" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi if [ -z "$RECORD_LINE" ]; then log_error "华为云没有找到目标 AAAA 记录: $P_RECORD_NAME" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi log_info "华为云记录原始信息: $RECORD_LINE" RECORDSET_ID="$(parse_recordset_id "$RECORD_LINE")" REMOTE_IPV6="$(parse_record_ipv6 "$RECORD_LINE")" REMOTE_TTL="$(parse_record_ttl "$RECORD_LINE")" log_info "解析到 RECORDSET_ID: ${RECORDSET_ID:-<empty>}" log_info "从华为云读取到的原 IPv6: ${REMOTE_IPV6:-<empty>}" log_info "从华为云读取到的 TTL: ${REMOTE_TTL:-<unknown>}" log_info "wifi-keeper.sh 传入的新 IPv6: $NEW_IPV6" if [ -z "$RECORDSET_ID" ]; then log_error "无法解析 RECORDSET_ID,跳过记录: $P_RECORD_NAME" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi if [ "$REMOTE_IPV6" = "$NEW_IPV6" ]; then log_info "对比结果: IPv6 一致,不需要更新" log_info "跳过更新: $P_RECORD_NAME" SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) return 0 fi log_info "对比结果: IPv6 不一致,需要更新" log_info "变化详情: ${REMOTE_IPV6:-<empty>} -> $NEW_IPV6" if ! update_recordset "$P_ZONE_ID" "$RECORDSET_ID" "$P_RECORD_NAME" "$NEW_IPV6"; then FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi log_info "开始二次查询,验证华为云是否已经更新为新 IPv6" VERIFY_LINE="$(get_record_line "$P_ZONE_ID" "$P_RECORD_NAME")" VERIFY_RC="$?" if [ "$VERIFY_RC" != "0" ]; then log_error "二次查询失败,无法验证华为云是否已经更新: $P_RECORD_NAME" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 fi VERIFY_IPV6="$(parse_record_ipv6 "$VERIFY_LINE")" log_info "二次查询记录原始信息: ${VERIFY_LINE:-<empty>}" log_info "二次查询解析到的 IPv6: ${VERIFY_IPV6:-<empty>}" if [ "$VERIFY_IPV6" = "$NEW_IPV6" ]; then log_info "更新验证成功: $P_RECORD_NAME 已经是 $NEW_IPV6" UPDATED_COUNT=$((UPDATED_COUNT + 1)) return 0 fi log_error "更新验证失败: $P_RECORD_NAME 当前仍是 ${VERIFY_IPV6:-<empty>},期望 $NEW_IPV6" FAILED_COUNT=$((FAILED_COUNT + 1)) return 1 } main() { prepare_runtime trap finish EXIT if ! check_environment; then exit 1 fi log_info "开始查询华为云 ZONE_ID" if ! get_zone_id; then exit 1 fi if [ -z "$ZONE_ID" ]; then log_error "ZONE_ID 为空,无法继续" exit 1 fi log_info "ZONE_ID 查询成功: $ZONE_ID" log_info "开始处理 RECORD_NAMES 列表" log_multiline "目标记录: " "$RECORD_NAMES" RECORD_LIST_FILE="$RUNTIME_DIR/record-list.$$.txt" printf '%s\n' "$RECORD_NAMES" > "$RECORD_LIST_FILE" while IFS= read -r RECORD_NAME || [ -n "$RECORD_NAME" ]; do [ -z "$RECORD_NAME" ] && continue case "$RECORD_NAME" in \#*) continue ;; esac process_one_record "$ZONE_ID" "$RECORD_NAME" done < "$RECORD_LIST_FILE" "$RM_BIN" -f "$RECORD_LIST_FILE" 2>/dev/null log_info "全部记录处理完成: total=$TOTAL_COUNT updated=$UPDATED_COUNT skipped=$SKIPPED_COUNT failed=$FAILED_COUNT" if [ "$FAILED_COUNT" -gt 0 ]; then log_error "存在失败记录,脚本以失败状态退出" exit 1 fi log_info "脚本执行成功" exit 0 } main 4. 同步DDNS时,需要用到openssl,所以先安装termux。 4.1 执行命令 pkg update 可能会提示: No mirror or mirror group selected. You might want to select one by running 'termux-change-repo' Testing the available mirrors: [*] (10) https://packages-cf.termux.dev/apt/termux-main: ok [*] (1) https://mirror.freedif.org/termux/termux-main: ok [*] (1) https://mirror.nevacloud.com/applications/termux/termux-main: ok [*] (1) https://mirrors.krnk.org/apt/termux/termux-main: ok [*] (1) https://mirrors.saswata.cc/termux/termux-main: bad [*] (1) https://tmx.xvx.my.id/apt/termux-main: bad [*] (1) https://mirror.rinarin.dev/termux/termux-main: ok [*] (1) https://mirror.albony.in/termux/termux-main: ok [*] (1) https://mirror.meowsmp.net/termux/termux-main: bad [*] (1) https://mirrors.in.sahilister.net/termux/termux-main/: ok [*] (1) https://mirrors.ravidwivedi.in/termux/termux-main: ok [*] (1) https://linux.domainesia.com/applications/termux/termux-main: ok [*] (1) https://mirror.jeonnam.school/termux/termux-main: ok [*] (1) https://termux.niranjan.co/termux-main: ok [*] (1) https://mirrors.cbrx.io/apt/termux/termux-main: ok [*] (1) https://mirror.bardia.tech/termux/termux-main: bad [*] (1) https://mirror.textcord.xyz/termux/termux-main: ok [*] (1) https://mirror.twds.com.tw/termux/termux-main: ok [*] (1) https://mirrors.nguyenhoang.cloud/termux/termux-main: ok [*] (1) https://mirrors.zju.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.pku.edu.cn/termux/termux-main/: ok [*] (1) https://mirrors.cernet.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.hust.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.bfsu.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.cqupt.edu.cn/termux/termux-main: ok [*] (1) https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirror.nyist.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirror.sjtu.edu.cn/termux/termux-main/: ok [*] (1) https://mirrors.sau.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.sdu.edu.cn/termux/termux-main: ok [*] (1) https://mirrors.nju.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.aliyun.com/termux/termux-main: ok [*] (1) https://mirror.iscas.ac.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.sustech.edu.cn/termux/apt/termux-main: ok [*] (1) https://mirrors.ustc.edu.cn/termux/termux-main: ok [*] (1) https://mirrors.de.sahilister.net/termux/termux-main: ok [*] (1) https://ro.mirror.flokinet.net/termux/termux-main: ok [*] (1) https://mirrors.medzik.dev/termux/termux-main: bad [*] (1) https://mirror.leitecastro.com/termux/termux-main: bad [*] (1) https://mirror.mwt.me/termux/main: ok [*] (1) https://termux.librehat.com/apt/termux-main: ok [*] (1) https://ftp.fau.de/termux/termux-main: ok [*] (1) https://packages.termux.dev/apt/termux-main: ok [*] (1) https://termux.mentality.rip/termux-main: ok [*] (1) https://md.mirrors.hacktegic.com/termux/termux-main: bad [*] (1) https://mirror.termux.dev/termux-main: bad [*] (1) https://mirror.bouwhuis.network/termux/termux-main: ok [*] (1) https://ftp.agdsn.de/termux/termux-main: ok [*] (1) https://mirrors.cfe.re/termux/termux-main: bad [*] (1) https://is.mirror.flokinet.net/termux/termux-main: ok [*] (1) https://termux.3san.dev/termux/termux-main: ok [*] (1) https://mirror.accum.se/mirror/termux.dev/termux-main: ok [*] (1) https://mirror.sunred.org/termux/termux-main: ok [*] (4) https://grimler.se/termux/termux-main: ok [*] (1) https://mirror.autkin.net/termux/termux-main: ok [*] (1) https://mirror.polido.pt/termux/termux-main: bad [*] (1) https://nl.mirror.flokinet.net/termux/termux-main: ok [*] (1) https://termux.cdn.lumito.net/termux-main: ok [*] (1) https://mirrors.utermux.dev/termux/termux-main: ok [*] (1) https://mirror.quantum5.ca/termux/termux-main: ok [*] (1) https://gnlug.org/pub/termux/termux-main: ok [*] (1) https://mirror.mwt.me/termux/main: ok [*] (1) https://plug-mirror.rcac.purdue.edu/termux/termux-main: bad [*] (1) https://termux.danyael.xyz/termux/termux-main: ok [*] (1) https://mirror.vern.cc/termux/termux-main: bad [*] (1) https://mirror.csclub.uwaterloo.ca/termux/termux-main: ok [*] (1) https://dl.kcubeterm.com/termux-main: bad [*] (1) https://mirror.fcix.net/termux/termux-main: bad [*] (1) https://mirrors.middlendian.com/termux/termux-main: bad [*] (1) http://mirror.mephi.ru/termux/termux-main: ok [*] (1) https://repository.su/termux/termux-main/: bad Picking mirror: (40) /data/data/com.termux/files/usr/etc/termux/mirrors/europe/mirrors.de.sahilister.net Get:1 https://mirrors.de.sahilister.net/termux/termux-main stable InRelease [14.0 kB] Get:2 https://mirrors.de.sahilister.net/termux/termux-main stable/main aarch64 Packages [547 kB] Fetched 561 kB in 6s (91.4 kB/s) Reading package lists... Done Building dependency tree... Done 73 packages can be upgraded. Run 'apt list --upgradable' to see them. ~ $ 意思是:你当前 Termux 还没有固定选择一个软件源镜像,所以它自动测试了一堆可用镜像。 不用管,termux会自动选择可用源帮你更新。 4.2然后执行命令安装openssl: pkg install openssl openssl-tool curl vim -y 安装过程中,如果提示输入内容,默认输入Y就行。 如果安装过程缓慢,可以搭载全局节点,也可以切换源地址后重新执行命令安装。 [!success] 到此,ipv6的健康度检查和ddns自动同步问题就解决了。手机重启以后,wifi会自动连接,然后ipv6会做健康度检查,然后自动同步DDNS。 [!warning] 由于论坛的文本编辑框,内容输入过多的时候,就会变得非常卡,由于本期代码太多了,所以帖子写的很慢。有很多细节上的东西不太重要我就暂时不补充了。不管是 ipv6 的检查脚本还是 DDNS 的同步脚本,都有非常详细的日志记录,如果有什么问题,大家可以把日志发出来,到时候我看看 4 个帖子 - 4 位参与者 阅读完整话题
前两期回顾: 【超详细】手机搭建服务器 · 第 一 期 【超详细】手机搭建服务器 · 第 二 期 前言 第一期讲了必备条件;第二期讲了如何实现旁路供电,解决供电问题; 本期要实现的是:如何让手机的wifi永不断连: 不管是手机意外重启,还是重新开关机; 不管是开启了飞行模式; 不管是不是主动关闭wifi。 [!warning]建议 在开始之前,建议先把手机的这两个开关关闭,不是必须,但是建议,减少干扰: 系统设置 - 开发者选项 - WLAN扫描调节 - 关闭 系统设置 - WLAN - WLAN助理 - 智能选网和网速模式 - 关闭 补充说明: · 机型不同,打开开发者选项的方式也不同,这个自己百度 · 不是所有手机都有【WLAN助理】这个选项,我是MIUI 11,所有有,其他手机可能是其他名字,或者没有这个功能,自己看自己系统的功能开关。 主要是把能影响WIFI稳定的开关关闭,不管是信号调节还是省电方面的设置。 教程开始 1.新建开机自启脚本 1.1 打开MT管理器,进入 /data/adb/service.d/ 文件夹,新建 99-phone-server.sh 文件 1.2 打开文件,在文件中输入以下代码并保存: #!/system/bin/sh # ============================================================ # Phone Server 启动器 # 放在 Magisk service.d 中 # 注意: # 这个脚本不写其他的复杂业务逻辑,只负责启动真正的脚本。 # 以后 IPv6、DDNS、Debian、SSH 等脚本,也会这么做。 # ============================================================ WIFI_KEEPER="/data/local/phone-server/wifi-keeper/wifi-keeper.sh" WIFI_LOG="/data/local/phone-server/wifi-keeper/wifi-keeper.log" mkdir -p /data/local/phone-server/wifi-keeper echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] [启动器] 99-phone-server.sh 开始执行" >> "$WIFI_LOG" if [ -x "$WIFI_KEEPER" ]; then nohup "$WIFI_KEEPER" >/dev/null 2>&1 & echo "$(date '+%Y-%m-%d %H:%M:%S') [OK] [启动器] 已后台启动 Wi-Fi Keeper" >> "$WIFI_LOG" else echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] [启动器] Wi-Fi Keeper 不存在或没有执行权限:$WIFI_KEEPER" >> "$WIFI_LOG" fi 1.3给脚本设置权限 2.新建wifi核心脚本 2.1 打开MT管理器,进入 /data/local/ 文件夹,新建 phone-server 文件夹,然后进入 phone-server 文件夹再新建 wifi-keeper 文件夹; 2.2 在 /data/local/phone-server/wifi-keeper/ 文件夹下面新建 wifi-keeper.sh 文件,并且输入下面的代码进行保存: #!/system/bin/sh # ============================================================ # Wi-Fi Keeper # ============================================================ # 功能: # 1. 开机后自动检查 wifi 开关 # 2. 如果 wifi 关闭,自动打开 wifi # 3. 等待系统自动连接“已保存”的 wifi # 4. 如果系统没有自动连上,则按 WIFI_LIST 优先级选择已保存 wifi # 5. wifi 断开后,自动重新连接 # 6. 输出详细日志,方便排查 # # 适用环境: # Android Root + Magisk service.d # # 重要说明: # 1. 需要提前手动连接一次 wifi,让系统保存 wifi。 # 2. WIFI_LIST 里只写 Wi-Fi 名称,不用写密码、不用写验证方式。 # 3. 如果一个WI-FI没连接成功,系统会连接下一个wifi # 4. 这是常驻守护脚本,不是执行一次就退出的脚本。 # ============================================================ # ============================================================ # 基础路径配置 # ============================================================ BASE_DIR="/data/local/phone-server/wifi-keeper" LOG_FILE="$BASE_DIR/wifi-keeper.log" # 临时锁目录,放在 /dev,重启后自动消失 # 作用:防止同一个 wifi-keeper 被重复启动 LOCK_DIR="/dev/phone-server-wifi-keeper.lock" WIFI_IFACE="wlan0" # ============================================================ # Wi-Fi 列表配置 # ============================================================ # 格式: # 一行一个 Wi-Fi 名称。 # # 顺序就是优先级: # 第一行优先级最高,连接失败才尝试下一行。 # ============================================================ WIFI_LIST=" CMCC-Semmering " # ============================================================ # 行为参数 # ============================================================ # 开机后等待系统完全启动,最多等待多少秒,单位:秒 BOOT_WAIT_MAX=60 # 系统启动完成后,额外等待多少秒,单位:秒 # 有些 MIUI 设备 sys.boot_completed=1 后,Wi-Fi 服务还没完全准备好。 AFTER_BOOT_EXTRA_WAIT=10 # 打开 Wi-Fi 后,最多等待多久确认 Wi-Fi 进入可连接状态,单位:秒 ENABLE_WIFI_WAIT=10 # 打开 Wi-Fi 后,等待系统自动连接已保存 Wi-Fi 的时间,单位:秒 AUTO_CONNECT_WAIT=10 # 使用 wpa_cli 选择已保存 Wi-Fi 后,最多等待多久拿到 IP,单位:秒 SELECT_WIFI_WAIT=30 # 正常情况下,每隔多少秒检查一次 Wi-Fi,单位:秒 CHECK_INTERVAL=60 # 本轮修复失败后,等待多少秒再重试,单位:秒 FAIL_COOLDOWN=30 # 日志最大大小,单位:字节 # 1048576 字节 = 1MB # 超过后只保留最后 300 行,避免日志无限增长 MAX_LOG_SIZE=1048576 # ============================================================ # 全局变量 # ============================================================ WPA_CLI_BIN="" WPA_CTRL_DIR="" WPA_LAST_OUT="" # ============================================================ # 日志函数 # ============================================================ now() { date '+%Y-%m-%d %H:%M:%S' } # 避免日志里输出 MAC 地址 / BSSID mask_mac() { echo "$1" | sed -E 's/([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}/[MAC已隐藏]/g' } # 取 WIFI_LIST 中第一个有效 wifi名称,用于“尚未解析到当前 SSID”时的日志提示。 # 这样日志不会把 MIUI 的 BSSID / state 状态误当成 Wi-Fi 名称。 get_first_wifi_name_from_list() { printf '%s\n' "$WIFI_LIST" | while IFS= read -r line; do [ -z "$line" ] && continue case "$line" in \#*) continue ;; esac echo "$line" return 0 done } # 清洗从 dumpsys / wpa_cli 解析出来的 SSID。 clean_ssid_value() { raw="$1" ssid="$(echo "$raw" | sed 's/^ *//;s/ *$//')" case "$ssid" in ""|"<unknown ssid>"|"unknown"|"null"|"0x"|"WifiSsid.NONE"|"none"|"<none>") echo "" return 0 ;; esac echo "$ssid" | grep -Eiq '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}|(^|[[:space:]])nid:|(^|[[:space:]])state:|bssid|scanning|associating|disconnected|completed' && { echo "" return 0 } echo "$ssid" } rotate_log_if_needed() { [ -f "$LOG_FILE" ] || return 0 size="$(wc -c < "$LOG_FILE" 2>/dev/null)" [ -n "$size" ] || return 0 if [ "$size" -gt "$MAX_LOG_SIZE" ]; then tail -n 300 "$LOG_FILE" > "$LOG_FILE.tmp" 2>/dev/null mv "$LOG_FILE.tmp" "$LOG_FILE" 2>/dev/null fi } log() { level="$1" action="$2" msg="$(mask_mac "$3")" mkdir -p "$BASE_DIR" rotate_log_if_needed echo "$(now) [$level] [$action] $msg" >> "$LOG_FILE" } log_info() { log "INFO" "$1" "$2" } log_ok() { log "OK" "$1" "$2" } log_error() { log "ERROR" "$1" "$2" } # ============================================================ # 防止重复运行 # ============================================================ acquire_lock() { if mkdir "$LOCK_DIR" 2>/dev/null; then trap 'rm -rf "$LOCK_DIR"; log_info "退出脚本" "Wi-Fi Keeper 已退出,释放 lock"' EXIT log_ok "启动脚本" "获取 lock 成功,当前实例开始运行" return 0 fi log_error "启动脚本" "检测到已有 Wi-Fi Keeper 正在运行,当前实例退出" exit 0 } # ============================================================ # 环境检查 # ============================================================ check_env() { mkdir -p "$BASE_DIR" if ! command -v svc >/dev/null 2>&1; then log_error "检查环境" "系统不支持 svc 命令,无法打开 Wi-Fi" exit 1 fi if ! command -v ip >/dev/null 2>&1; then log_error "检查环境" "系统不支持 ip 命令,无法检测 wlan0 地址" exit 1 fi log_ok "检查环境" "环境检查完成" } wait_boot_completed() { log_info "等待系统启动" "开始等待 sys.boot_completed=1" i=0 while [ "$i" -lt "$BOOT_WAIT_MAX" ]; do boot_completed="$(getprop sys.boot_completed 2>/dev/null)" if [ "$boot_completed" = "1" ]; then log_ok "等待系统启动" "系统启动完成" return 0 fi sleep 2 i=$((i + 2)) done log_error "等待系统启动" "等待系统启动完成超时,继续执行后续逻辑" return 1 } # ============================================================ # Wi-Fi 状态判断 # ============================================================ wifi_on_value() { settings get global wifi_on 2>/dev/null } wifi_on_text() { v="$(wifi_on_value)" case "$v" in 0) echo "关闭" ;; 1) echo "已开启" ;; 2) echo "正在开启" ;; 3) echo "正在关闭" ;; *) echo "未知状态:$v" ;; esac } has_wifi_ip() { ip -4 addr show dev "$WIFI_IFACE" 2>/dev/null | grep -q "inet " && return 0 ip -6 addr show dev "$WIFI_IFACE" scope global 2>/dev/null | grep -q "inet6 " && return 0 return 1 } wifi_iface_up() { ip link show "$WIFI_IFACE" 2>/dev/null | grep -q "UP" } wifi_ready_for_connect() { # 已经拿到 IP,说明 Wi-Fi 已连接 has_wifi_ip && return 0 # settings 显示已开启 [ "$(wifi_on_value)" = "1" ] && return 0 # wlan0 已经 UP,也说明可以等待系统自动连接或尝试选择已保存网络 wifi_iface_up && return 0 return 1 } # ============================================================ # 当前 Wi-Fi 名称解析 # ============================================================ find_wpa_cli_bin_quiet() { [ -n "$WPA_CLI_BIN" ] && [ -x "$WPA_CLI_BIN" ] && return 0 for b in \ "$(command -v wpa_cli 2>/dev/null)" \ "/system/bin/wpa_cli" \ "/vendor/bin/wpa_cli" \ "/system/xbin/wpa_cli" \ "/system_ext/bin/wpa_cli" do [ -z "$b" ] && continue if [ -x "$b" ]; then WPA_CLI_BIN="$b" return 0 fi done return 1 } wpa_cli_raw() { if [ -z "$WPA_CTRL_DIR" ]; then "$WPA_CLI_BIN" -i "$WIFI_IFACE" "$@" 2>&1 else "$WPA_CLI_BIN" -p "$WPA_CTRL_DIR" -i "$WIFI_IFACE" "$@" 2>&1 fi } get_current_ssid_from_wpa() { find_wpa_cli_bin_quiet || return 0 # 如果已经检测过控制目录,就直接用 if [ -n "$WPA_CTRL_DIR" ]; then out="$(wpa_cli_raw status 2>/dev/null)" ssid="$(echo "$out" | sed -n 's/^ssid=//p' | head -n 1)" ssid="$(clean_ssid_value "$ssid")" [ -n "$ssid" ] && echo "$ssid" return 0 fi # 尝试几个常见控制目录 for p in \ "" \ "/data/vendor/wifi/wpa/sockets" \ "/data/misc/wifi/sockets" \ "/dev/socket" do WPA_CTRL_DIR="$p" out="$(wpa_cli_raw status 2>/dev/null)" echo "$out" | grep -q "wpa_state" || continue ssid="$(echo "$out" | sed -n 's/^ssid=//p' | head -n 1)" ssid="$(clean_ssid_value "$ssid")" if [ -n "$ssid" ]; then echo "$ssid" return 0 fi done WPA_CTRL_DIR="" return 0 } get_current_ssid_from_dumpsys() { # 只提取 SSID,避免输出 BSSID/MAC。 line="$(dumpsys wifi 2>/dev/null | grep -m 1 "SSID:" 2>/dev/null)" [ -z "$line" ] && return 0 ssid="$(echo "$line" | sed -n 's/.*SSID: *"\([^"]*\)".*/\1/p')" if [ -z "$ssid" ]; then ssid="$(echo "$line" | sed -n 's/.*SSID: *\([^,]*\).*/\1/p')" fi ssid="$(clean_ssid_value "$ssid")" [ -n "$ssid" ] && echo "$ssid" } get_current_ssid() { ssid="$(get_current_ssid_from_wpa)" if [ -n "$ssid" ]; then echo "$ssid" return 0 fi ssid="$(get_current_ssid_from_dumpsys)" if [ -n "$ssid" ]; then echo "$ssid" return 0 fi echo "" } # 获取日志里应该显示的 Wi-Fi 名称。 # 说明: # MIUI 11 / Android 10 有时不允许从 wpa_cli / dumpsys 稳定解析当前 SSID。 # 如果已经拿到 Wi-Fi IP,但解析不到 SSID,并且 WIFI_LIST 里配置了目标 Wi-Fi, # 就在日志中显示“目标 WiFi + 推断说明”,避免反复出现“未解析到当前WiFi名称”。 get_display_wifi_name() { real_ssid="$(get_current_ssid)" if [ -n "$real_ssid" ]; then echo "$real_ssid" return 0 fi target_ssid="$(get_first_wifi_name_from_list)" if has_wifi_ip && [ -n "$target_ssid" ]; then echo "$target_ssid(根据WIFI_LIST推断)" return 0 fi if [ -n "$target_ssid" ]; then echo "未连接;目标WiFi=$target_ssid" else echo "未连接" fi } log_wifi_basic_status() { wifi_value="$(wifi_on_value)" wifi_text="$(wifi_on_text)" ssid="$(get_display_wifi_name)" ip_info="$(ip addr show dev "$WIFI_IFACE" 2>/dev/null | grep -E 'inet |inet6 ' | tr '\n' ' ')" [ -z "$ip_info" ] && ip_info="未拿到IP" log_info "WiFi状态" "WiFi开关=$wifi_value($wifi_text);当前WiFi=$ssid;IP=$ip_info" } enable_wifi() { if wifi_ready_for_connect; then log_ok "打开WiFi开关" "Wi-Fi 已经处于可用或可连接状态,无需重复打开" log_wifi_basic_status return 0 fi log_info "打开WiFi开关" "检测到 Wi-Fi 关闭,开始打开 Wi-Fi" out="$(svc wifi enable 2>&1)" rc="$?" if [ "$rc" -ne 0 ]; then log_error "打开WiFi开关" "svc wifi enable 执行失败,rc=$rc,输出:$out" return 1 fi log_info "打开WiFi开关" "svc wifi enable 执行成功,开始等待 Wi-Fi 进入可连接状态" i=0 while [ "$i" -lt "$ENABLE_WIFI_WAIT" ]; do if wifi_ready_for_connect; then log_ok "打开WiFi开关" "Wi-Fi 已进入可用或可连接状态" log_wifi_basic_status return 0 fi if [ $((i % 10)) -eq 0 ]; then log_info "打开WiFi开关" "等待 Wi-Fi 开启中,已等待 ${i} 秒" log_wifi_basic_status fi sleep 2 i=$((i + 2)) done log_error "打开WiFi开关" "已执行打开 Wi-Fi,但 ${ENABLE_WIFI_WAIT} 秒内没有确认进入可连接状态" log_wifi_basic_status return 1 } wifi_connected_ok() { if ! has_wifi_ip; then return 1 fi ssid="$(get_display_wifi_name)" log_ok "检查WiFi状态" "Wi-Fi 已连接,当前WiFi=$ssid,已拿到 IP" return 0 } wait_wifi_connected() { action="$1" max_wait="$2" i=0 while [ "$i" -lt "$max_wait" ]; do if wifi_connected_ok; then log_ok "$action" "连接判断成功" return 0 fi if [ $((i % 10)) -eq 0 ]; then log_info "$action" "等待 Wi-Fi 获取 IP,已等待 ${i} 秒" log_wifi_basic_status fi sleep 2 i=$((i + 2)) done log_error "$action" "等待 ${max_wait} 秒后仍未拿到 Wi-Fi IP" log_wifi_basic_status return 1 } wait_auto_connect() { log_info "自动连接WIFI" "等待系统自动连接已保存 Wi-Fi,最长等待 ${AUTO_CONNECT_WAIT} 秒" if wait_wifi_connected "自动连接WIFI" "$AUTO_CONNECT_WAIT"; then return 0 fi log_error "自动连接WIFI" "系统自动连接超时,准备按 Wi-Fi 名称选择已保存网络" return 1 } # ============================================================ # 使用 wpa_cli 选择已保存 wifi # 注意:这里只选择已保存网络,不写入密码,不创建新网络 # ============================================================ find_wpa_cli_bin() { if find_wpa_cli_bin_quiet; then log_ok "查找wpa_cli" "找到 wpa_cli:$WPA_CLI_BIN" return 0 fi log_error "查找wpa_cli" "没有找到 wpa_cli,无法主动选择已保存 Wi-Fi,只能依赖系统自动连接" return 1 } wpa_exec() { action="$1" shift WPA_LAST_OUT="$(wpa_cli_raw "$@")" rc="$?" safe_out="$(mask_mac "$WPA_LAST_OUT")" log_info "$action" "执行:$WPA_CLI_BIN ${WPA_CTRL_DIR:+-p $WPA_CTRL_DIR} -i $WIFI_IFACE $*;rc=$rc;输出:$safe_out" return "$rc" } find_wpa_cli_control() { if ! find_wpa_cli_bin; then return 1 fi for p in \ "" \ "/data/vendor/wifi/wpa/sockets" \ "/data/misc/wifi/sockets" \ "/dev/socket" do WPA_CTRL_DIR="$p" out="$(wpa_cli_raw status 2>&1)" rc="$?" safe_out="$(mask_mac "$out")" log_info "检测wpa_cli" "尝试控制目录='${p:-默认}',rc=$rc,输出:$safe_out" echo "$out" | grep -q "wpa_state" && { log_ok "检测wpa_cli" "wpa_cli 可用,控制目录='${p:-默认}'" return 0 } done WPA_CTRL_DIR="" log_error "检测wpa_cli" "wpa_cli 存在,但无法连接到 Wi-Fi 控制接口,只能依赖系统自动连接" return 1 } find_saved_network_id_by_ssid() { target_ssid="$1" wpa_exec "查找已保存WIFI" list_networks network_id="$(printf '%s\n' "$WPA_LAST_OUT" \ | awk -F '\t' -v target="$target_ssid" 'NR > 1 && $2 == target {print $1; exit}')" if [ -z "$network_id" ]; then log_error "查找已保存WIFI" "没有找到已保存的 Wi-Fi:$target_ssid。请先手动连接一次并保存" return 1 fi log_ok "查找已保存WIFI" "找到已保存 Wi-Fi:$target_ssid,network_id=$network_id" echo "$network_id" return 0 } select_saved_wifi_by_ssid() { ssid="$1" log_info "选择WIFI" "准备选择已保存 Wi-Fi:$ssid" if ! find_wpa_cli_control; then return 1 fi network_id="$(find_saved_network_id_by_ssid "$ssid")" if [ -z "$network_id" ]; then return 1 fi wpa_exec "启用WIFI" enable_network "$network_id" wpa_exec "选择WIFI" select_network "$network_id" wpa_exec "重连WIFI" reconnect if wait_wifi_connected "选择WIFI" "$SELECT_WIFI_WAIT"; then current_ssid="$(get_display_wifi_name)" log_ok "选择WIFI" "已连接 Wi-Fi:$current_ssid" return 0 fi log_error "选择WIFI" "已尝试选择 Wi-Fi:$ssid,但最终没有拿到 IP" return 1 } connect_by_priority() { log_info "连接WIFI" "开始按 WIFI_LIST 顺序选择已保存 Wi-Fi" tmp_list="/dev/phone-server-wifi-list.tmp" printf '%s\n' "$WIFI_LIST" > "$tmp_list" while IFS= read -r ssid; do [ -z "$ssid" ] && continue case "$ssid" in \#*) continue ;; esac log_info "连接WIFI" "按优先级尝试选择 Wi-Fi:$ssid" if select_saved_wifi_by_ssid "$ssid"; then rm -f "$tmp_list" 2>/dev/null return 0 fi log_error "连接WIFI" "Wi-Fi=$ssid 连接失败,继续尝试下一个" done < "$tmp_list" rm -f "$tmp_list" 2>/dev/null log_error "连接WIFI" "WIFI_LIST 中的已保存 Wi-Fi 都没有连接成功" return 1 } # ============================================================ # 主循环 # ============================================================ main_loop() { log_info "启动脚本" "Wi-Fi Keeper 启动" wait_boot_completed if [ "$AFTER_BOOT_EXTRA_WAIT" -gt 0 ]; then log_info "等待系统服务" "系统已启动,额外等待 ${AFTER_BOOT_EXTRA_WAIT} 秒,让 Wi-Fi 服务准备完成" sleep "$AFTER_BOOT_EXTRA_WAIT" fi log_wifi_basic_status enable_wifi if wait_auto_connect; then log_ok "启动连接" "系统自动连接 Wi-Fi 成功" else connect_by_priority fi while true; do if wifi_connected_ok; then sleep "$CHECK_INTERVAL" continue fi log_error "守护检测" "检测到 Wi-Fi 不可用,开始自动修复" enable_wifi if wait_auto_connect; then log_ok "守护检测" "系统自动恢复 Wi-Fi 成功" else if connect_by_priority; then log_ok "守护检测" "按 Wi-Fi 名称选择已保存网络成功" else log_error "守护检测" "本轮 Wi-Fi 修复失败,${FAIL_COOLDOWN} 秒后重试" sleep "$FAIL_COOLDOWN" fi fi done } # ============================================================ # 程序入口 # ============================================================ acquire_lock check_env main_loop 2.3 按照1.3的过程给脚本设置权限。 [!question] 如何验证脚本是否成功 1. 手机先正常连接上你选择Wi-Fi,建议选择一个即可,避免手机因为某些原因在多个wifi之间来回切换,这个对后面的ipv6的解析影响非常大,然后关闭wifi的 自动连接 开关。 2. 正常连接上一个wifi以后,关闭wifi开关,把手机重启。 3.验证手机重启wifi是否能正常连接: 重启以后,等几秒钟(具体看配置你写的是几秒),然后看看wifi开关是否会自动打开,并且wifi是否会自动连接上。 4.验证手动断开wifi是否能正常连接: 如果wifi能自动连接上,可以手动关闭wifi开关,看看wifi是否能过几秒(具体还是要看配置你写的是几秒)后自动打开并且连接上wifi。 我用的脚本在米8SE上多轮测试没问题,如果你们有问题,记得联系我,我到时候拍查一下 [!question] 如何关闭脚本 方法一: 取消/data/adb/service.d/99-phone-server.sh脚本的执行权限 方法二: 删除/data/adb/service.d/99-phone-server.sh脚本 [!success]结束语 不得不说,AI是真的好用。不仅可以优化脚本,还能把注释写的清清楚楚。按照它的逻辑和日志,就算出问题了也能很好的定位和解决,这一次确实是省了不少时间。可惜两天撸的business 2个月翻车了。 另外,给佬友们送上一份可直接食用的prompt: 我现在正在把一台 Android 手机改造成 7×24 小时运行的家庭小服务器。 目前我已经完成了 ACC 充电控制,解决了长期插电和旁路供电相关的问题。下一步我要解决的是:手机与 Wi-Fi 之间的连接稳定性。 我的目标是: 手机开机后,或者 Wi-Fi 因为路由器重启、信号波动、系统休眠、网络异常等原因断开后,脚本能够自动恢复 Wi-Fi 连接,让手机尽可能长期稳定在线。 我的手机环境大概如下: - Android 手机,已 Root; - 使用 Magisk; - 开机自启动脚本可以通过 Magisk 的 `/data/adb/service.d/` 实现; - 手机会长期作为服务器运行; - 后续还会继续做 IPv6 公网地址检测、DDNS 同步、Debian/SSH 服务守护等脚本; - 所以我不希望所有业务脚本都直接堆在 `/data/adb/service.d/` 里。 --- # 我希望你帮我实现的事情 请你作为一名专业的 Android / Linux / Shell 脚本开发人员,帮我设计并编写一套 Wi-Fi 连接守护方案。 我希望它能实现这些能力: ## 1. 开机自动执行 手机开机后,Magisk 自动执行启动脚本。 但是我不希望把真正复杂的 Wi-Fi 守护逻辑直接写在 `/data/adb/service.d/` 里面。 我希望 `/data/adb/service.d/` 里只放一个“启动器脚本”,比如: ```text /data/adb/service.d/99-phone-server.sh ``` 这个启动器只负责启动真正的 Wi-Fi 守护脚本。 真正的 Wi-Fi 守护脚本和日志文件,请你帮我设计一个合理的目录,比如放在: ```text /data/local/phone-server/wifi-keeper/ ``` 或者你认为更合理的目录也可以,但请你说明为什么这样设计。 --- ## 2. 自动打开 Wi-Fi 脚本启动后,需要检查 Wi-Fi 开关状态。 如果 Wi-Fi 是关闭状态,就自动打开 Wi-Fi。 如果 Wi-Fi 已经打开,就不要重复操作。 注意: 不同 Android 版本、不同 ROM 对 Wi-Fi 命令支持可能不一样,所以请你不要想当然地只使用某一种命令。 请你先分析有哪些可能的实现方式,比如: * `svc wifi enable` * `settings get global wifi_on` * `ip link show wlan0` * `ip addr show wlan0` * `wpa_cli` * `cmd wifi` * `dumpsys wifi` 然后根据兼容性和稳定性,选择更适合 Magisk Root 环境的实现方式。 如果某些命令在部分系统上可能不可用,请在脚本里做好兼容、兜底和日志记录,而不是让脚本直接崩溃。 --- ## 3. Wi-Fi 列表和优先级 我希望脚本支持提前配置一个 Wi-Fi 列表。 比如: ```text 主 Wi-Fi 备用 Wi-Fi 手机热点 ``` 列表顺序就是优先级。 脚本应该优先连接列表中靠前的 Wi-Fi。 如果当前 Wi-Fi 可用,就不要频繁切换,避免影响 SSH、网页服务、IPv6、DDNS 等后续服务。 只有当当前 Wi-Fi 不可用、断开、没有拿到 IP,或者长时间无法恢复时,才尝试切换到下一个可用 Wi-Fi。 --- ## 4. 已保存 Wi-Fi 和新 Wi-Fi 的处理 请你根据 Android 实际情况,设计合理逻辑: * 如果 Wi-Fi 已经在系统中保存过,优先让系统自动连接; * 如果系统自动连接失败,再尝试根据 Wi-Fi 名称选择已保存网络; * 如果脚本能够可靠地支持“通过密码添加新 Wi-Fi”,可以设计为可选能力; * 如果某些 Android 版本或 ROM 不适合脚本直接写入 Wi-Fi 密码,也请你明确说明原因,并给出更稳定的替代方案。 我不希望脚本为了追求“看起来功能完整”,强行使用不可靠的命令。 请你优先保证稳定性。 --- ## 5. 不要频繁扫描和切换 手机是作为服务器使用的,所以稳定性比“永远连接最高优先级 Wi-Fi”更重要。 请你设计逻辑时注意: * 如果当前 Wi-Fi 已经连接成功,并且已经拿到 IP,就不要乱动; * 不要频繁扫描 Wi-Fi; * 不要频繁断开当前 Wi-Fi 再切换; * 不要在信号短暂波动时马上切换; * 需要有合理的重试间隔和冷却时间; * 避免脚本疯狂循环导致耗电、发热、刷屏日志。 --- ## 6. Wi-Fi 是否连接成功的判断 我希望脚本不要只看 Wi-Fi 开关是否打开。 真正有意义的是: ```text 手机是否已经通过 wlan0 拿到了 IP 地址 ``` 请你设计连接成功判断逻辑。 比如: * wlan0 拿到 IPv4; * 或者 wlan0 拿到全局 IPv6; * 或者两者都可以。 暂时不需要做外网连通性检测,比如 ping 某个公网 IP。 因为公网 ping 可能受 DNS、运营商、防火墙、网络环境影响,不适合作为 Wi-Fi 是否连接成功的唯一依据。 --- ## 7. 日志要求 脚本必须输出清晰日志,方便我排查问题。 日志中需要包含: * 时间,格式为 `YYYY-MM-DD HH:mm:ss`; * 日志级别,比如 `INFO`、`OK`、`ERROR`; * 当前操作名称,比如: * 启动脚本 * 等待系统启动 * 打开 Wi-Fi 开关 * 检查 Wi-Fi 状态 * 等待系统自动连接 * 选择已保存 Wi-Fi * 重连 Wi-Fi * 进入守护循环 * 错误原因; * 关键命令的返回值和输出,方便排查。 日志示例可以类似: ```text 2026-05-15 22:01:23 [OK] [启动脚本] 获取 lock 成功,当前实例开始运行 2026-05-15 22:01:40 [OK] [等待系统启动] 系统启动完成 2026-05-15 22:01:52 [INFO] [打开WiFi开关] 检测到 Wi-Fi 关闭,开始打开 Wi-Fi 2026-05-15 22:01:55 [OK] [打开WiFi开关] Wi-Fi 已进入可连接状态 2026-05-15 22:02:36 [OK] [检查WiFi状态] Wi-Fi 已连接,当前WiFi=xxx,已拿到 IP ``` 日志文件也要限制大小,避免长期运行后日志无限变大。 请你在脚本里设计日志轮转,例如超过 1MB 后只保留最后若干行,并且在注释中写清楚单位。 --- ## 8. 日志中的 Wi-Fi 名称和隐私 如果能解析当前 Wi-Fi 名称,日志中应该显示 Wi-Fi 名称,而不是 MAC 地址。 如果某些系统状态中出现 BSSID / MAC 地址,请隐藏或过滤掉。 例如不要输出: ```text 00:00:00:00:00:00 nid: 0 state: ASSOCIATING ``` 这不是 Wi-Fi 名称,而是系统连接状态。 如果暂时解析不到当前 Wi-Fi 名称,但已经拿到 wlan0 的 IP,请不要误判为失败。 这种情况下可以写: ```text Wi-Fi 已连接,已拿到 IP,但当前 Wi-Fi 名称暂时无法解析 ``` 或者如果配置列表里只有一个目标 Wi-Fi,也可以说明是根据配置推断的名称。 --- ## 9. 防止重复运行 这个脚本是常驻守护脚本,不是一次性脚本。 所以请你加入 lock 机制,防止多个相同脚本同时运行。 我希望你使用适合 Android / Magisk 环境的方式,比如: * 使用目录锁; * lock 放在 `/dev` 这种重启后会自动清理的临时目录; * 不要用容易残留的持久 PID 文件; * 如果脚本正常退出,要自动释放 lock; * 如果检测到已有实例运行,当前脚本应该退出,并写入日志。 请你解释一下 lock 的作用和为什么这样设计。 --- ## 10. 守护循环 脚本不应该只在开机时执行一次。 它应该是常驻守护脚本。 大概逻辑应该是: ```text 等待系统启动完成 ↓ 打开 Wi-Fi ↓ 等待系统自动连接已保存 Wi-Fi ↓ 如果连接成功,进入守护循环 ↓ 每隔一段时间检查 Wi-Fi 是否仍然连接 ↓ 如果 Wi-Fi 正常,什么都不做 ↓ 如果 Wi-Fi 不正常,开始自动修复 ↓ 重新打开 Wi-Fi / 等待自动连接 / 必要时选择已保存 Wi-Fi ↓ 失败则等待冷却时间后重试 ``` --- # 请你特别注意 我上面的想法可能并不成熟,所以请你不要只是机械照着我的描述写。 请你先站在专业 Android 开发人员和 Linux 运维人员的角度,帮我完善整体逻辑。 在正式给代码之前,请你先做一次简短但准确的设计分析,包括: 1. 推荐目录结构; 2. 为什么 service.d 里只放启动器; 3. Wi-Fi 守护脚本应该承担哪些职责; 4. 哪些事情不应该放在 Wi-Fi 守护脚本里,比如 IPv6 检测、DDNS 同步; 5. 如何判断 Wi-Fi 是否真的可用; 6. 如何避免重复运行; 7. 哪些命令在 Android 上可能存在兼容性问题,脚本应该如何处理。 然后再给我完整代码。 --- # 输出要求 请输出: ## 第一部分:设计说明 用通俗易懂的话解释整体方案。 ## 第二部分:目录结构 给出推荐目录结构,并说明每个文件的作用。 ## 第三部分:Magisk 启动器脚本 给出完整代码,可以直接复制使用。 ## 第四部分:Wi-Fi 守护脚本 给出完整代码,可以直接复制使用。 ## 第五部分:安装和测试命令 包括: * 创建目录; * 写入脚本; * 添加执行权限; * 手动启动测试; * 查看日志; * 停止脚本; * 清理 lock; * 如何确认脚本是否正在运行。 --- # 我的最终目标 我希望得到的是一套适合 Android Root + Magisk + 手机服务器场景的 Wi-Fi 稳定连接方案。 重点是: * 稳定; * 易排查; * 日志清晰; * 不重复运行; * 不乱切 Wi-Fi; * 不把所有功能写成一个超级大脚本; * 后续方便扩展 IPv6 检测、DDNS 更新、Debian / SSH 守护等功能。 请你根据这些目标,帮我设计并实现一个更专业、更稳定、更容易维护的版本。 1 个帖子 - 1 位参与者 阅读完整话题
36氪获悉,中国人民银行于本周三(5月27日)通过香港金融管理局债务工具中央结算系统(CMU)债券投标平台,以利率招标方式发行了2026年第四期和第五期央行票据。2026年第四期央行票据(香港)的发行量为150亿元人民币,期限为91天,中标利率为1.13%。2026年第五期央行票据(香港)的发行量为150亿元人民币,期限为1年,中标利率为1.32%
36氪获悉,为丰富香港高信用等级人民币金融产品,完善香港人民币收益率曲线,今天(27日)中国人民银行将通过香港金融管理局债务工具中央结算系统(CMU)债券投标平台,招标发行2026年第四期和第五期中央银行票据。发行量均为人民币150亿元。