【超详细】手机搭建服务器 · 第 四 期

本期主题:解决ipv6稳定性和DDNS同步问题 前两期回顾: 【超详细】手机搭建服务器 · 第 一 期 【超详细】手机搭建服务器 · 第 二 期 【超详细】手机搭建服务器 · 第 三 期 [!warning]第三期的内容可以不用管 第三期的内容主要是做一个记录,告诉大家在实现手机做服务器这个事情上,...
【超详细】手机搭建服务器 · 第 四 期
【超详细手机搭建服务器 · 第 四 期

本期主题:解决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

  1. 这一次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的包原因,大家自行测试吧。

  2. 在wifi-keeper.sh这个脚本中,大家要修改的地方是WIFI_LIST部分,这里需要把你家里的wifi名称和Wi-Fi密码和加密方式写到脚本中,这样不管是你手机重新开关机还是取消保存了wifi,还是开启了飞行模式,脚本都可以很好的自动连接上Wi-Fi。Wi-Fi是不会再断的,哪怕你手动断开wifi,wifi过10s都会自动连接上的。
    image


    大家不用担心脚本安全问题,因为脚本是公开透明的。

3.记得修改wifi-keeper.sh文件的权限,可以参考上一期如何修改权限,这里就不截图了

二、如何实现ipv6自动更新到DDNS,实现用域名访问

1. 自行去华为云购买服务器。这里只拿华为云举例,原因开头已经说了。大家自行注册和购买域名。注册好域名后看下一步。

2. 配置AK和SK,后面脚本会用到

  2.1 访问统一身份认证服务IAM,然后左边菜单找到用户,在用户页面右上角点击创建用户

image
  2.2 如图操作,然后下一步

image
image

  
2.3 到这一步的时候,一定要点击确定下载密钥文件,因为密钥只能在这里下载一次,如果不小心点了取消,只能重新创建用户
image

3.进入/data/local/文件夹,新建ipv6-ddns文件夹,然后进入ipv6-ddns文件夹再新建ipv6-ddns.sh文件;输入以下内容并保存:

[!warning]提醒

  1. 记得务必把代码中的AK和SK替换成自己的。
  2. 文件同样记得修改权限。
    image

    图中域名填写时,小数点不能去掉

    图中域名填写时,小数点不能去掉

    图中域名填写时,小数点不能去掉
#!/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 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文