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:
+97
-16
@@ -138,7 +138,11 @@ resolve_restart_config() {
|
||||
error "XRAY_RESTART_EVERY_DAYS 必须是非负整数,当前为 '${days}'"
|
||||
return 1
|
||||
fi
|
||||
RESTART_EVERY_DAYS="$days"
|
||||
# 10# 前缀去掉前导零:这个值会原样写进 guard 的 DAYS=,
|
||||
# 而 guard 里 $(( DAYS * 86400 - 300 )) 是算术展开——
|
||||
# 010 会被当八进制解析成 8 天,08 / 09 直接报 "value too great for base"
|
||||
# 从而让间隔判断求值为假,退化成每天重启。
|
||||
RESTART_EVERY_DAYS=$((10#$days))
|
||||
|
||||
local t
|
||||
if ! t=$(normalize_restart_time "${XRAY_RESTART_TIME:-04:00}"); then
|
||||
@@ -163,16 +167,18 @@ update_env_field() {
|
||||
local file="$1" key="$2" value="$3" esc
|
||||
[ -f "$file" ] || return 1
|
||||
|
||||
# 每一步写入都检查返回值:磁盘满或只读时静默失败会让用户以为改动已落盘,
|
||||
# 下次完整部署又被 .env 里的旧值覆盖回去
|
||||
if grep -q "^${key}=" "$file"; then
|
||||
# 用 | 作分隔符,并转义 value 中的 | & \,避免值里的 / 破坏 sed
|
||||
esc=$(printf '%s' "$value" | sed 's/[|&\\]/\\&/g')
|
||||
sed -i "s|^${key}=.*|${key}=${esc}|" "$file"
|
||||
sed -i "s|^${key}=.*|${key}=${esc}|" "$file" || return 1
|
||||
else
|
||||
# 文件末尾可能缺换行(用户手工编辑过),直接追加会把新字段黏到上一行
|
||||
if [ -n "$(tail -c 1 "$file")" ]; then
|
||||
printf '\n' >> "$file"
|
||||
printf '\n' >> "$file" || return 1
|
||||
fi
|
||||
printf '%s=%s\n' "$key" "$value" >> "$file"
|
||||
printf '%s=%s\n' "$key" "$value" >> "$file" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
@@ -199,6 +205,10 @@ if [ -f "\$STATE" ]; then
|
||||
last=\$(cat "\$STATE" 2>/dev/null || echo 0)
|
||||
# 内容被写坏时按 0 处理,即允许立即重启
|
||||
[[ "\$last" =~ ^[0-9]+\$ ]] || last=0
|
||||
# 时间戳落在未来(RTC 错乱或 NTP 大幅向后校正时写下的)同样按 0 处理。
|
||||
# 否则时钟校回之后 now - last 恒为负,间隔判断恒成立,此后永远跳过,
|
||||
# 而状态文件只在真正重启时才被覆盖——没有任何自愈路径。
|
||||
[ "\$last" -gt "\$now" ] && last=0
|
||||
fi
|
||||
|
||||
# 300 秒容差:systemd timer 默认 AccuracySec=1min,第 N 次触发的实际间隔
|
||||
@@ -208,11 +218,20 @@ if [ "\$last" -gt 0 ] && [ \$(( now - last )) -lt \$(( DAYS * 86400 - 300 )) ];
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "\$(dirname "\$STATE")"
|
||||
# 必须先写时间戳:restart 会中断本脚本所在的 systemd 事务,
|
||||
# 顺序反了会丢记录,退化成每天都重启。
|
||||
# 代价是重启失败时本轮仍被记为已执行——这比陷入每日重启循环可接受。
|
||||
echo "\$now" > "\$STATE"
|
||||
#
|
||||
# 写不进去(/var 满了、只读挂载)时必须跳过本轮:照常重启会让下一轮
|
||||
# 依旧读不到时间戳,从而变成每天硬重启且无限持续——保护完全失效。
|
||||
if ! mkdir -p "\$(dirname "\$STATE")" 2>/dev/null; then
|
||||
echo "无法创建 \$(dirname "\$STATE"),跳过本轮重启以免退化成每日重启" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "\$now" > "\$STATE" 2>/dev/null; then
|
||||
echo "无法写入 \$STATE,跳过本轮重启以免退化成每日重启" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "距上次重启已满 \${DAYS} 天,执行重启"
|
||||
exec systemctl restart xray
|
||||
EOF
|
||||
@@ -234,8 +253,11 @@ EOF
|
||||
Description=Wake xray-restart daily at ${time} ${tz}; the guard enforces the ${days}-day interval
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* ${time}:00
|
||||
TimeZone=${tz}
|
||||
# 时区必须写在日历表达式里(systemd >= 242 支持)。
|
||||
# [Timer] 段没有 TimeZone= 这个 key——写了只会被忽略并留下一行
|
||||
# "Unknown key name 'TimeZone'",然后按系统本地时区解析。
|
||||
# VPS 出厂多为 UTC,那样「04:00 Asia/Shanghai」会在北京时间 12:00 触发。
|
||||
OnCalendar=*-*-* ${time}:00 ${tz}
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
@@ -249,7 +271,11 @@ remove_restart_units() {
|
||||
if [ ! -f "${SYSTEMD_DIR}/xray-restart.timer" ] && [ ! -f "$GUARD_BIN" ]; then
|
||||
return 1
|
||||
fi
|
||||
systemctl disable --now xray-restart.timer >/dev/null 2>&1 || true
|
||||
# 失败不阻断删除(单元可能本就没被 enable 过),但要留一句可见的提示,
|
||||
# 而不是把结果彻底吞掉
|
||||
if ! systemctl disable --now xray-restart.timer >/dev/null 2>&1; then
|
||||
warn "禁用 xray-restart.timer 未成功(可能本就未启用),继续删除单元文件"
|
||||
fi
|
||||
rm -f "${SYSTEMD_DIR}/xray-restart.timer" \
|
||||
"${SYSTEMD_DIR}/xray-restart.service" \
|
||||
"$GUARD_BIN"
|
||||
@@ -258,21 +284,66 @@ remove_restart_units() {
|
||||
|
||||
# 依据 RESTART_EVERY_DAYS / RESTART_TIME / RESTART_TZ 建立或移除定时重启,
|
||||
# 并完成 daemon-reload 与 enable/disable。完整部署与 --restart 共用。
|
||||
# 本函数被 harden_system 裸调用、harden_system 又被 main 裸调用,
|
||||
# 全程不在条件位置。deploy.sh 开头是 set -euo pipefail,所以这里任何一条
|
||||
# systemctl 返回非 0 都会让整份部署在「8/8 加固」之后无声退出——
|
||||
# start_service / save_env / print_result 全都不执行。
|
||||
# 因此下面每条 systemctl 都放在条件位置,失败只告警不中断。
|
||||
configure_restart_timer() {
|
||||
if [ "$RESTART_EVERY_DAYS" -eq 0 ]; then
|
||||
if remove_restart_units; then
|
||||
warn "已移除定时重启(XRAY_RESTART_EVERY_DAYS=0)"
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
if ! systemctl daemon-reload; then
|
||||
warn "systemctl daemon-reload 失败,已删除的单元可能仍残留在 systemd 内存中"
|
||||
fi
|
||||
log "定时重启: 关闭(崩溃恢复已由 Restart=always 秒级覆盖)"
|
||||
return
|
||||
return 0
|
||||
fi
|
||||
|
||||
render_restart_units "$RESTART_EVERY_DAYS" "$RESTART_TIME" "$RESTART_TZ"
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now xray-restart.timer >/dev/null 2>&1
|
||||
if ! systemctl daemon-reload; then
|
||||
warn "systemctl daemon-reload 失败,新的 timer 可能不会立即生效"
|
||||
fi
|
||||
# 不吞 stderr:规格明确「不预校验时区字符串,靠 systemctl 报错」,
|
||||
# 把报错藏起来就等于既不校验也不提示
|
||||
if ! systemctl enable --now xray-restart.timer; then
|
||||
error "定时重启 timer 启用失败,请检查 XRAY_RESTART_TIMEZONE 是否为合法时区"
|
||||
error "部署的其余部分不受影响,可用 systemd-analyze verify ${SYSTEMD_DIR}/xray-restart.timer 排查"
|
||||
return 0
|
||||
fi
|
||||
log "定时重启: 每 ${RESTART_EVERY_DAYS} 天 ${RESTART_TIME} (${RESTART_TZ})"
|
||||
warn "重启会切断所有活动连接;崩溃恢复本已由 Restart=always 覆盖,按需启用"
|
||||
return 0
|
||||
}
|
||||
|
||||
# list-timers 里的 NEXT 是 timer 的「下次唤醒」——每天一次,恒为次日的
|
||||
# ${RESTART_TIME};真正是否重启由 guard 按间隔判断。设了「每 7 天」且 5 天前
|
||||
# 刚重启过的用户,看到 NEXT 是明天凌晨会以为明天就重启,实际还差 2 天。
|
||||
# 所以这里直接把「下次实际重启」算出来一并打印。
|
||||
print_next_restart() {
|
||||
local last next now
|
||||
now=$(date +%s)
|
||||
|
||||
if [ ! -f "$RESTART_STATE_FILE" ]; then
|
||||
echo " 下次实际重启: 下次唤醒时即会重启(尚无重启记录)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
last=$(cat "$RESTART_STATE_FILE" 2>/dev/null || echo "")
|
||||
if ! [[ "$last" =~ ^[0-9]+$ ]] || [ "$last" -gt "$now" ]; then
|
||||
echo " 下次实际重启: 下次唤醒时即会重启(状态文件内容不可用,按无记录处理)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
next=$(( last + RESTART_EVERY_DAYS * 86400 ))
|
||||
echo " 上次实际重启: $(date -d "@${last}" '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo "$last")"
|
||||
if [ "$next" -le "$now" ]; then
|
||||
echo " 下次实际重启: 下次唤醒时即会重启(距上次已满 ${RESTART_EVERY_DAYS} 天)"
|
||||
else
|
||||
echo " 下次实际重启: $(date -d "@${next}" '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo "$next") 之后的首次唤醒"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===== --restart:只改重启计划 =====
|
||||
@@ -302,10 +373,16 @@ apply_restart_only() {
|
||||
|
||||
# 只把命令行显式给过的字段落盘,没给的不动 .env
|
||||
if [ -n "$RESTART_ARG_DAYS" ]; then
|
||||
update_env_field "$env_file" XRAY_RESTART_EVERY_DAYS "$RESTART_EVERY_DAYS"
|
||||
if ! update_env_field "$env_file" XRAY_RESTART_EVERY_DAYS "$RESTART_EVERY_DAYS"; then
|
||||
error "写入 ${env_file} 失败,未做任何改动"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ -n "$RESTART_ARG_TIME" ]; then
|
||||
update_env_field "$env_file" XRAY_RESTART_TIME "$RESTART_TIME"
|
||||
if ! update_env_field "$env_file" XRAY_RESTART_TIME "$RESTART_TIME"; then
|
||||
error "写入 ${env_file} 失败,未做任何改动"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
configure_restart_timer
|
||||
@@ -316,12 +393,16 @@ apply_restart_only() {
|
||||
echo " 定时重启: 已关闭"
|
||||
else
|
||||
echo " 定时重启: 每 ${RESTART_EVERY_DAYS} 天 ${RESTART_TIME} (${RESTART_TZ})"
|
||||
print_next_restart
|
||||
fi
|
||||
echo " xray 服务未重启,vless 链接与现有连接均未受影响。"
|
||||
echo "=========================================================="
|
||||
if [ "$RESTART_EVERY_DAYS" -ne 0 ]; then
|
||||
echo ""
|
||||
systemctl list-timers xray-restart.timer --no-pager 2>/dev/null || true
|
||||
echo ""
|
||||
echo "注意:上表的 NEXT 是 timer 的下次「唤醒」时间(每天一次),"
|
||||
echo " 不是下次重启时间。是否真的重启由守卫脚本按上面的间隔判断。"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -972,7 +1053,7 @@ SERVER_IP=${SERVER_IP:-}
|
||||
# 定时重启。0 = 关闭(默认);N = 每 N 天在 XRAY_RESTART_TIME 重启一次。
|
||||
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而硬重启会切断
|
||||
# 全部活动连接。仅在确实观察到长期运行后性能退化时才启用。
|
||||
# 改这两项无需完整重新部署: bash deploy.sh --restart 7 04:00
|
||||
# 改这几项无需完整重新部署: bash deploy.sh --restart 7 04:00
|
||||
XRAY_RESTART_EVERY_DAYS=${RESTART_EVERY_DAYS}
|
||||
XRAY_RESTART_TIME=${RESTART_TIME}
|
||||
XRAY_RESTART_TIMEZONE=${RESTART_TZ}
|
||||
|
||||
Reference in New Issue
Block a user