fix(vps-xray): 修复定时重启的四类静默失效并补齐安全不变式测试
Critical-1 timer 时区:[Timer] 段没有 TimeZone= 这个 key,systemd 只会记一条 "Unknown key name" 然后按系统本地时区解析 OnCalendar。VPS 出厂多为 UTC, 用户设「04:00 Asia/Shanghai」会在北京时间 12:00 触发且毫无提示。 改为把时区写进日历表达式本身:OnCalendar=*-*-* HH:MM:00 <TZ>(systemd >= 242)。 Critical-2 前导零:^[0-9]+$ 放行 08/09/010,该值原样写进 guard 的 DAYS=, 而 guard 里是算术展开——010 被当八进制解析成 8 天,08/09 直接报 "value too great for base",使间隔判断求值为假,落进重启分支变成每天重启。 校验通过后统一 $((10#$days)) 归一化。 Important-3 状态文件写入失败:mkdir 与写时间戳都不检查返回值,/var 写满或 只读时写入失败却照样重启,「先写时间戳」的保护完全失效,退化成每天硬重启且 无限持续。改为 fail-safe:写不进去就报错并跳过本轮。 update_env_field 的三处写入同样补上返回值检查,调用方改在条件位置。 Important-4 未来时间戳:RTC 错乱或 NTP 大幅向后校正时会写下未来的时间戳, 时钟校回后 now-last 恒为负,此后永远跳过且无自愈路径。与「内容非法按 0 处理」 同策略,读到 last > now 时按 0 处理。 Important-5 静默中止:configure_restart_timer 被 harden_system、main 一路裸调用, set -e 下 systemctl enable 失败会让部署在「8/8 加固」之后无声退出—— start_service / save_env / print_result 全不执行,config.json 已换、防火墙已改, 但 xray 没重启、.env 没更新、链接没打印。三处 systemctl 全部改到条件位置, enable 不再吞 stderr(规格本就要求靠 systemctl 报错暴露非法时区)。 Important-6 NEXT 语义:list-timers 的 NEXT 是 timer 的「下次唤醒」,每天一次, 不是下次重启时间。新增 print_next_restart,--restart 输出里直接算出并显示 「下次实际重启时间」,README 同步说明差异。 Important-7 安全不变式测试:configure_restart_timer 与 apply_restart_only 原本 零覆盖,「不重启 xray、不碰 vless 链接」这个最重要的承诺恰恰没有回归保护。 补上启用/关闭两个方向的断言,以及 .env 逐字段 diff 与 systemctl 调用日志断言。 Minor-9 README 补充取舍:完整部署的 systemctl restart xray 不更新状态文件。 Minor-11 save_env 注释「这两项」→「这几项」。 测试 61 → 101 项,全部通过。
This commit is contained in:
@@ -89,6 +89,20 @@ reset_env; XRAY_DAILY_RESTART=true; XRAY_RESTART_EVERY_DAYS=0
|
||||
resolve_restart_config
|
||||
is "新字段显式 0 覆盖旧 true" "$RESTART_EVERY_DAYS" "0"
|
||||
|
||||
# 前导零必须按十进制归一化:这个值会原样写进 guard 的 DAYS=,
|
||||
# 而 guard 里是算术展开——010 会变成 8 天,08/09 直接报错导致每天重启
|
||||
reset_env; XRAY_RESTART_EVERY_DAYS=08
|
||||
resolve_restart_config
|
||||
is "08 归一化为 8(不当八进制)" "$RESTART_EVERY_DAYS" "8"
|
||||
|
||||
reset_env; XRAY_RESTART_EVERY_DAYS=09
|
||||
resolve_restart_config
|
||||
is "09 归一化为 9(不当八进制)" "$RESTART_EVERY_DAYS" "9"
|
||||
|
||||
reset_env; XRAY_RESTART_EVERY_DAYS=010
|
||||
resolve_restart_config
|
||||
is "010 归一化为 10(不当八进制)" "$RESTART_EVERY_DAYS" "10"
|
||||
|
||||
reset_env; XRAY_RESTART_EVERY_DAYS=abc
|
||||
resolve_restart_config 2>/dev/null
|
||||
is "天数非数字报错" "$?" "1"
|
||||
@@ -169,10 +183,12 @@ is "timer 文件存在" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo
|
||||
is "service 文件存在" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes)" "yes"
|
||||
is "guard 可执行" "$([ -x "$GUARD_BIN" ] && echo yes)" "yes"
|
||||
|
||||
is "OnCalendar 每天触发" \
|
||||
"$(grep -c '^OnCalendar=\*-\*-\* 04:00:00$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||
is "TimeZone 写入" \
|
||||
"$(grep -c '^TimeZone=Asia/Shanghai$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||
# 时区必须写在日历表达式里:[Timer] 段没有 TimeZone= 这个 key,
|
||||
# 写了会被 systemd 忽略并按系统本地时区(VPS 多为 UTC)解析,静默偏移 8 小时
|
||||
is "OnCalendar 每天触发且带时区" \
|
||||
"$(grep -c '^OnCalendar=\*-\*-\* 04:00:00 Asia/Shanghai$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||
is "不再使用非法的 TimeZone= 指令" \
|
||||
"$(grep -c '^TimeZone=' "${SYSTEMD_DIR}/xray-restart.timer")" "0"
|
||||
is "Persistent 开启" \
|
||||
"$(grep -c '^Persistent=true$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||
is "service 指向 guard" \
|
||||
@@ -186,8 +202,8 @@ is "guard 用 PATH 查找 systemctl 而非绝对路径" \
|
||||
|
||||
render_restart_units 3 23:05 Europe/Berlin
|
||||
is "重复渲染覆盖天数" "$(grep -c '^DAYS=3$' "$GUARD_BIN")" "1"
|
||||
is "重复渲染覆盖时间" \
|
||||
"$(grep -c '^OnCalendar=\*-\*-\* 23:05:00$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||
is "重复渲染覆盖时间与时区" \
|
||||
"$(grep -c '^OnCalendar=\*-\*-\* 23:05:00 Europe/Berlin$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||
|
||||
remove_restart_units
|
||||
is "remove 后 timer 消失" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
|
||||
@@ -246,6 +262,37 @@ guard_err=$("$GUARD_BIN" 2>&1 >/dev/null)
|
||||
is "状态文件非法时不产生算术求值报错" \
|
||||
"$(printf '%s' "$guard_err" | grep -c 'integer expression')" "0"
|
||||
|
||||
# 时间戳落在未来(RTC 错乱 / NTP 大幅向后校正)→ 必须按 0 处理立即执行,
|
||||
# 否则时钟校回后 now-last 恒为负,此后永远跳过且无自愈路径
|
||||
echo "$(( $(date +%s) + 30 * 86400 ))" > "$RESTART_STATE_FILE"
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
"$GUARD_BIN" > /dev/null 2>&1
|
||||
is "未来时间戳不导致永久跳过" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
|
||||
is "未来时间戳被覆盖为当前时间" \
|
||||
"$([ "$(cat "$RESTART_STATE_FILE")" -le "$(date +%s)" ] && echo yes || echo no)" "yes"
|
||||
|
||||
# 状态文件写不进去 → 必须跳过本轮。照常重启会让下一轮依旧读不到时间戳,
|
||||
# 退化成每天硬重启且无限持续
|
||||
guard_saved_state="$RESTART_STATE_FILE"
|
||||
|
||||
# (a) 目录建不出来:拿一个普通文件占住父目录的位置
|
||||
: > "${TMP}/blocked"
|
||||
RESTART_STATE_FILE="${TMP}/blocked/last-restart"
|
||||
render_restart_units 7 04:00 Asia/Shanghai
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
"$GUARD_BIN" > /dev/null 2>&1
|
||||
is "状态目录建不出来时跳过重启" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||
|
||||
# (b) 目录能建但文件写不进去:让状态文件路径本身是个目录
|
||||
mkdir -p "${TMP}/asdir/last-restart"
|
||||
RESTART_STATE_FILE="${TMP}/asdir/last-restart"
|
||||
render_restart_units 7 04:00 Asia/Shanghai
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
"$GUARD_BIN" > /dev/null 2>&1
|
||||
is "时间戳写不进去时跳过重启" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||
|
||||
RESTART_STATE_FILE="$guard_saved_state"
|
||||
|
||||
# 每日模式:距上次 1 天即执行
|
||||
render_restart_units 1 04:00 Asia/Shanghai
|
||||
echo "$(( $(date +%s) - 86400 ))" > "$RESTART_STATE_FILE"
|
||||
@@ -272,6 +319,150 @@ echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
|
||||
STUBEOF
|
||||
chmod 755 "${STUB}/systemctl"
|
||||
|
||||
echo "== configure_restart_timer =="
|
||||
|
||||
# 启用方向
|
||||
RESTART_EVERY_DAYS=7
|
||||
RESTART_TIME="04:00"
|
||||
RESTART_TZ="Asia/Shanghai"
|
||||
rm -f "${SYSTEMD_DIR}/xray-restart.timer" "${SYSTEMD_DIR}/xray-restart.service" "$GUARD_BIN"
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
configure_restart_timer > /dev/null 2>&1
|
||||
is "启用时渲染出 timer" \
|
||||
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "yes"
|
||||
is "启用时渲染出 guard" "$([ -x "$GUARD_BIN" ] && echo yes || echo no)" "yes"
|
||||
is "启用时执行 daemon-reload" \
|
||||
"$(grep -c '^systemctl daemon-reload$' "$SYSTEMCTL_LOG")" "1"
|
||||
is "启用时执行 enable --now" \
|
||||
"$(grep -c '^systemctl enable --now xray-restart.timer$' "$SYSTEMCTL_LOG")" "1"
|
||||
is "启用时不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||
|
||||
# enable 失败时不得中断调用方:configure_restart_timer 在部署主干上被裸调用
|
||||
cat > "${STUB}/systemctl" << 'STUBEOF'
|
||||
#!/bin/sh
|
||||
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
|
||||
case "$1" in
|
||||
enable) exit 1 ;;
|
||||
esac
|
||||
STUBEOF
|
||||
chmod 755 "${STUB}/systemctl"
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
configure_restart_timer > /dev/null 2>&1
|
||||
is "enable 失败时仍返回 0(不中断部署主干)" "$?" "0"
|
||||
cat > "${STUB}/systemctl" << 'STUBEOF'
|
||||
#!/bin/sh
|
||||
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
|
||||
STUBEOF
|
||||
chmod 755 "${STUB}/systemctl"
|
||||
|
||||
# 关闭方向
|
||||
echo "12345" > "$RESTART_STATE_FILE"
|
||||
RESTART_EVERY_DAYS=0
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
configure_restart_timer > /dev/null 2>&1
|
||||
is "关闭时删除 timer" \
|
||||
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
|
||||
is "关闭时删除 service" \
|
||||
"$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
|
||||
is "关闭时删除 guard" "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"
|
||||
is "关闭时保留状态文件" "$(cat "$RESTART_STATE_FILE")" "12345"
|
||||
is "关闭时执行 disable --now" \
|
||||
"$(grep -c '^systemctl disable --now xray-restart.timer$' "$SYSTEMCTL_LOG")" "1"
|
||||
is "关闭时不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||
|
||||
echo "== apply_restart_only 安全不变式 =="
|
||||
|
||||
# 这一节守护本特性最核心的承诺:--restart 不重启 xray、不碰 vless 链接。
|
||||
# 缺了它,谁在 apply_restart_only 里加一行 systemctl restart xray 都不会被发现。
|
||||
APP_DIR="${TMP}/app"
|
||||
mkdir -p "$APP_DIR"
|
||||
SCRIPT_DIR="$APP_DIR"
|
||||
|
||||
make_app_env() {
|
||||
cat > "${APP_DIR}/.env" << 'ENVEOF'
|
||||
# Xray 部署配置(由 deploy.sh 自动生成)
|
||||
|
||||
XRAY_MODE=reality
|
||||
XRAY_PORT=443
|
||||
XRAY_UUID=11111111-2222-3333-4444-555555555555
|
||||
# 这是用户自己加的注释,不许动
|
||||
SERVER_IP=
|
||||
XRAY_RESTART_EVERY_DAYS=0
|
||||
XRAY_RESTART_TIME=04:00
|
||||
XRAY_RESTART_TIMEZONE=Asia/Shanghai
|
||||
XRAY_PUBLIC_KEY=pubkeyzzz
|
||||
XRAY_SHORT_ID=abcdef0123456789
|
||||
REALITY_SNI=www.apple.com
|
||||
ENVEOF
|
||||
}
|
||||
|
||||
# --- 启用:--restart 7 5:30 ---
|
||||
reset_env
|
||||
make_app_env
|
||||
cp "${APP_DIR}/.env" "${TMP}/env.before"
|
||||
RESTART_ARG_DAYS=7
|
||||
RESTART_ARG_TIME=5:30
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
apply_restart_only > /dev/null 2>&1
|
||||
|
||||
is "apply_restart_only 绝不重启 xray" \
|
||||
"$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||
is "天数写回 .env" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${APP_DIR}/.env")" "XRAY_RESTART_EVERY_DAYS=7"
|
||||
is "时间归一化后写回" "$(grep '^XRAY_RESTART_TIME=' "${APP_DIR}/.env")" "XRAY_RESTART_TIME=05:30"
|
||||
is ".env 只有这两行发生变化" \
|
||||
"$(diff "${TMP}/env.before" "${APP_DIR}/.env" | grep -c '^[<>]')" "4"
|
||||
is "用户注释原样保留" "$(grep -c '这是用户自己加的注释,不许动' "${APP_DIR}/.env")" "1"
|
||||
is "UUID 未被改动" "$(grep -c '^XRAY_UUID=11111111-2222-3333-4444-555555555555$' "${APP_DIR}/.env")" "1"
|
||||
is "SNI 未被改动" "$(grep -c '^REALITY_SNI=www\.apple\.com$' "${APP_DIR}/.env")" "1"
|
||||
is "PublicKey 未被改动" "$(grep -c '^XRAY_PUBLIC_KEY=pubkeyzzz$' "${APP_DIR}/.env")" "1"
|
||||
is "启用后 timer 已就位" \
|
||||
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "yes"
|
||||
|
||||
# --- 不带位置参数:.env 必须完全不变 ---
|
||||
reset_env
|
||||
make_app_env
|
||||
XRAY_RESTART_EVERY_DAYS=7
|
||||
XRAY_RESTART_TIME=04:00
|
||||
cp "${APP_DIR}/.env" "${TMP}/env.before2"
|
||||
RESTART_ARG_DAYS=""
|
||||
RESTART_ARG_TIME=""
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
apply_restart_only > /dev/null 2>&1
|
||||
is "不带位置参数时 .env 一字不改" \
|
||||
"$(diff "${TMP}/env.before2" "${APP_DIR}/.env" > /dev/null && echo same || echo differ)" "same"
|
||||
is "不带位置参数时也不重启 xray" \
|
||||
"$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||
|
||||
# --- 关闭:--restart 0 ---
|
||||
reset_env
|
||||
echo "12345" > "$RESTART_STATE_FILE"
|
||||
RESTART_ARG_DAYS=0
|
||||
RESTART_ARG_TIME=""
|
||||
: > "$SYSTEMCTL_LOG"
|
||||
apply_restart_only > /dev/null 2>&1
|
||||
is "关闭后 timer 被删" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
|
||||
is "关闭后 service 被删" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
|
||||
is "关闭后 guard 被删" "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"
|
||||
is "关闭后状态文件保留" "$(cat "$RESTART_STATE_FILE")" "12345"
|
||||
is "关闭时天数写回 .env" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${APP_DIR}/.env")" "XRAY_RESTART_EVERY_DAYS=0"
|
||||
is "关闭时也不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||
|
||||
echo "== print_next_restart =="
|
||||
|
||||
RESTART_EVERY_DAYS=7
|
||||
rm -f "$RESTART_STATE_FILE"
|
||||
is "无状态文件时提示下次唤醒即重启" \
|
||||
"$(print_next_restart | grep -c '下次唤醒时即会重启')" "1"
|
||||
|
||||
now_ts=$(date +%s)
|
||||
echo "$(( now_ts - 6 * 86400 ))" > "$RESTART_STATE_FILE"
|
||||
is "未满 N 天时给出的是真实重启日期而非明天的唤醒时间" \
|
||||
"$(print_next_restart | grep -c "下次实际重启: $(date -d "@$(( now_ts + 86400 ))" '+%Y-%m-%d')")" "1"
|
||||
|
||||
echo "$(( $(date +%s) - 9 * 86400 ))" > "$RESTART_STATE_FILE"
|
||||
is "已满 N 天时提示下次唤醒即重启" \
|
||||
"$(print_next_restart | grep -c '下次唤醒时即会重启')" "1"
|
||||
|
||||
echo ""
|
||||
echo "通过 ${PASS},失败 ${FAIL}"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
|
||||
Reference in New Issue
Block a user