fix: harden the bin/ snapshot-and-restore against the review round on #6152

- Replace the mktemp+cp snapshot with a same-filesystem mv of bin/ aside:
  an unchecked mktemp failure previously made the very next line copy
  bin/'s contents into "/" (empty custom_bin_backup + trailing slash),
  and a silently-ignored cp failure (stderr redirected, exit code never
  checked) could leave a truncated custom geo file that gets "restored"
  as if it were intact. A rename is atomic and needs no extra disk space,
  removing both failure modes at once; if it fails, back off cleanly and
  say so instead of proceeding as if a backup exists.
- Add a trap so an interrupted update (Ctrl-C, signal) between the
  backup and the restore doesn't leave the snapshot (which contains
  bin/config.json and every mtproto client's FakeTLS secret) sitting
  around indefinitely; the two exit-path cleanups this replaces are gone
  since the trap now covers those exits too.
- Move the restore below the arm arch-rename/chmod block instead of
  before it, so xray-linux-arm32/mtg-linux-arm already exist under their
  final names and don't get needlessly restored-then-overwritten and
  misreported as "custom".
- Exclude bin/config.json and bin/mtproto/*.toml from the restore: those
  are the panel's own generated runtime state (internal/xray/process.go,
  internal/mtproto/manager.go), not admin-placed files, and restoring a
  stale one only resurrects dead state or recreates bin/mtproto/ with the
  wrong (more permissive) directory mode.
- Match symlinks in the restore's find, not just plain files -- cp -a
  already preserves them in the snapshot, but the restore loop was
  silently dropping them, which is exactly the failure mode (a geo file
  symlinked in from elsewhere) this PR set out to fix.
- Quote the two new xui_folder expansions.
- Extend the non-interactive smoke test to reinstall over an existing
  install with a sentinel file in bin/, asserting it survives and that
  the bundled geoip.dat is still the release's own copy -- the update
  path this PR touches had no CI coverage at all before this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kuzz007
2026-07-30 10:45:02 +03:00
parent 9e42ec82c6
commit 8daf1d844d
2 changed files with 67 additions and 27 deletions
+18
View File
@@ -87,6 +87,24 @@ docker run --rm \
*) echo "FAIL: panel did not serve (status ${code:-none})"; tail -n 30 /tmp/xui.log; exit 1 ;;
esac
echo "--- verifying a second install preserves custom bin/ files ---"
echo "custom-sentinel" > /usr/local/x-ui/bin/geoip_custom.dat
geoip_sum_before=$(sha256sum /usr/local/x-ui/bin/geoip.dat | cut -d" " -f1)
if [ -n "${XUI_SMOKE_VERSION:-}" ]; then
cat /root/install.sh | bash -s -- "$XUI_SMOKE_VERSION"
else
cat /root/install.sh | bash
fi
test -f /usr/local/x-ui/bin/geoip_custom.dat \
|| { echo "FAIL: custom bin/ file did not survive a second install"; exit 1; }
[ "$(cat /usr/local/x-ui/bin/geoip_custom.dat)" = "custom-sentinel" ] \
|| { echo "FAIL: custom bin/ file content changed across a second install"; exit 1; }
geoip_sum_after=$(sha256sum /usr/local/x-ui/bin/geoip.dat | cut -d" " -f1)
[ "$geoip_sum_after" = "$geoip_sum_before" ] \
|| { echo "FAIL: bundled geoip.dat changed across a same-version reinstall"; exit 1; }
echo "SMOKE_PASS: user=$XUI_USERNAME port=$XUI_PANEL_PORT path=$XUI_WEB_BASE_PATH"
'
+49 -27
View File
@@ -1727,11 +1727,23 @@ install_x-ui() {
# from a routing rule via "ext:<file>:<code>") and would otherwise be
# silently deleted on every update, breaking Xray at next start with
# "failed to open <file>: no such file or directory" for any routing
# rule that references it.
if [[ -d ${xui_folder}/bin ]]; then
custom_bin_backup=$(mktemp -d)
cp -a ${xui_folder}/bin/. "${custom_bin_backup}/" 2> /dev/null
# rule that references it. Moved aside rather than copied: a rename
# on the same filesystem is atomic (no truncated file if disk space
# runs out mid-copy, unlike `cp`) and keeps the snapshot under
# /usr/local rather than a separate, possibly small/tmpfs $TMPDIR.
if [[ -d "${xui_folder}/bin" ]]; then
custom_bin_backup="${xui_folder%/x-ui}/x-ui-bin-backup.$$"
rm -rf "${custom_bin_backup}"
if ! mv "${xui_folder}/bin" "${custom_bin_backup}"; then
custom_bin_backup=""
echo -e "${yellow}Could not back up bin/ -- custom files there will not be preserved across this update${plain}"
fi
fi
# Sole cleanup path for the backup from here on -- covers both the
# two `exit 1`s below (extraction/binary-missing failures) and an
# interrupted update (Ctrl-C, signal) before the restore runs.
# Cleared once the restore below finishes normally.
trap '[[ -n "${custom_bin_backup}" ]] && rm -rf "${custom_bin_backup}"' EXIT INT TERM
rm ${xui_folder}/ -rf
fi
@@ -1740,7 +1752,6 @@ install_x-ui() {
if [[ $? -ne 0 ]]; then
rm x-ui-linux-$(arch).tar.gz -f
rm -f "${xui_script_temp}"
[[ -n "${custom_bin_backup}" ]] && rm -rf "${custom_bin_backup}"
echo -e "${red}Failed to extract the x-ui release archive -- the previous installation has already been removed, so the panel will not start until this is fixed; try running the installer again${plain}"
exit 1
fi
@@ -1749,34 +1760,12 @@ install_x-ui() {
cd x-ui
if [[ $? -ne 0 || ! -s x-ui ]]; then
rm -f "${xui_script_temp}"
[[ -n "${custom_bin_backup}" ]] && rm -rf "${custom_bin_backup}"
echo -e "${red}Extracted x-ui archive is missing the x-ui binary -- the previous installation has already been removed, so the panel will not start until this is fixed; try running the installer again${plain}"
exit 1
fi
chmod +x x-ui
chmod +x x-ui.sh
# Restore anything from the old bin/ that the fresh release doesn't ship
# (custom geoip/geosite files, or anything else an admin hand-placed
# there) -- never overwrites a same-named file the new release provides,
# so bundled assets (geoip.dat, geoip_RU.dat, ...) still get the fresh
# per-release copy.
if [[ -n "${custom_bin_backup}" ]]; then
local restored_custom_bin=()
while IFS= read -r -d '' f; do
local rel="${f#"${custom_bin_backup}"/}"
if [[ ! -e "bin/${rel}" ]]; then
mkdir -p "bin/$(dirname "${rel}")"
cp -a "${f}" "bin/${rel}"
restored_custom_bin+=("${rel}")
fi
done < <(find "${custom_bin_backup}" -type f -print0)
rm -rf "${custom_bin_backup}"
if [[ ${#restored_custom_bin[@]} -gt 0 ]]; then
echo -e "${green}Restored custom file(s) in bin/ not shipped by this release: ${restored_custom_bin[*]}${plain}"
fi
fi
# Check the system's architecture and rename the file accordingly.
# The panel binary maps GOARCH=arm to "arm32" (internal/xray/process.go),
# so the Xray binary must be named xray-linux-arm32; mtg keeps plain "arm".
@@ -1795,6 +1784,39 @@ install_x-ui() {
chmod +x bin/mtg-linux-$(arch)
fi
# Restore anything from the old bin/ that the fresh release doesn't ship
# (custom geoip/geosite files, or anything else an admin hand-placed
# there) -- never overwrites a same-named file the new release provides,
# so bundled assets (geoip.dat, geoip_RU.dat, ...) still get the fresh
# per-release copy. Runs after the arch-rename above so xray-linux-arm32/
# mtg-linux-arm already exist under their final names there and aren't
# mistaken for custom files needing a restore. Skips paths the panel
# itself regenerates at runtime (config.json, mtproto/*.toml -- see
# internal/xray/process.go, internal/mtproto/manager.go): those aren't
# admin-placed, and restoring a stale one only resurrects dead state (an
# orphaned mtg config for a since-deleted inbound) or the wrong
# directory permissions.
if [[ -n "${custom_bin_backup}" ]]; then
local restored_custom_bin=()
while IFS= read -r -d '' f; do
local rel="${f#"${custom_bin_backup}"/}"
case "${rel}" in
config.json | mtproto | mtproto/*) continue ;;
esac
if [[ ! -e "bin/${rel}" ]]; then
mkdir -p "bin/$(dirname "${rel}")"
cp -a "${f}" "bin/${rel}"
restored_custom_bin+=("${rel}")
fi
done < <(find "${custom_bin_backup}" \( -type f -o -type l \) -print0)
rm -rf "${custom_bin_backup}"
custom_bin_backup=""
if [[ ${#restored_custom_bin[@]} -gt 0 ]]; then
echo -e "${green}Restored custom file(s) in bin/ not shipped by this release: ${restored_custom_bin[*]}${plain}"
fi
fi
trap - EXIT INT TERM
# Update x-ui cli and se set permission
mv -f "${xui_script_temp}" /usr/bin/x-ui
if [[ $? -ne 0 ]]; then