update.sh 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. #!/bin/bash
  2. red='\033[0;31m'
  3. green='\033[0;32m'
  4. blue='\033[0;34m'
  5. yellow='\033[0;33m'
  6. plain='\033[0m'
  7. xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
  8. xui_service="${XUI_SERVICE:=/etc/systemd/system}"
  9. # Don't edit this config
  10. b_source="${BASH_SOURCE[0]}"
  11. while [ -h "$b_source" ]; do
  12. b_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)"
  13. b_source="$(readlink "$b_source")"
  14. [[ $b_source != /* ]] && b_source="$b_dir/$b_source"
  15. done
  16. cur_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)"
  17. script_name=$(basename "$0")
  18. # Check command exist function
  19. _command_exists() {
  20. type "$1" &> /dev/null
  21. }
  22. # Fail, log and exit script function
  23. _fail() {
  24. local msg=${1}
  25. echo -e "${red}${msg}${plain}"
  26. exit 2
  27. }
  28. # Records this run's outcome for the panel's web updater to poll, since it
  29. # launches this script detached and has no other way to learn whether it
  30. # finished. Written to a fixed path outside XUI_MAIN_FOLDER so it survives
  31. # the update regardless of what happens to that folder. The EXIT trap below
  32. # covers every exit path in this file, including the bare `exit 1`/`exit 2`
  33. # calls that don't go through _fail.
  34. xui_update_run_id="${XUI_UPDATE_RUN_ID:-0}"
  35. [[ "${xui_update_run_id}" =~ ^[0-9]+$ ]] || xui_update_run_id="0"
  36. xui_update_status_file="${XUI_UPDATE_STATUS_FILE:-/etc/x-ui/update-status.json}"
  37. _write_update_status() {
  38. local state="$1"
  39. local exit_code="$2"
  40. local status_dir
  41. status_dir="$(dirname "${xui_update_status_file}")"
  42. mkdir -p "${status_dir}" > /dev/null 2>&1
  43. local tmp_file="${xui_update_status_file}.tmp.$$"
  44. printf '{"runId":"%s","state":"%s","exitCode":%s,"finishedAt":%s}\n' \
  45. "${xui_update_run_id}" "${state}" "${exit_code}" "$(date +%s)" > "${tmp_file}" 2> /dev/null
  46. mv -f "${tmp_file}" "${xui_update_status_file}" > /dev/null 2>&1
  47. }
  48. _report_update_exit() {
  49. local code=$?
  50. if [[ "${code}" -eq 0 ]]; then
  51. _write_update_status "success" "0"
  52. else
  53. _write_update_status "failed" "${code}"
  54. fi
  55. }
  56. trap _report_update_exit EXIT
  57. trap 'exit 143' TERM
  58. trap 'exit 130' INT
  59. # check root
  60. [[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege."
  61. if _command_exists curl; then
  62. curl_bin=$(which curl)
  63. else
  64. _fail "ERROR: Command 'curl' not found."
  65. fi
  66. # Check OS and set release variable
  67. if [[ -f /etc/os-release ]]; then
  68. source /etc/os-release
  69. release=$ID
  70. elif [[ -f /usr/lib/os-release ]]; then
  71. source /usr/lib/os-release
  72. release=$ID
  73. else
  74. _fail "Failed to check the system OS, please contact the author!"
  75. fi
  76. echo "The OS release is: $release"
  77. arch() {
  78. case "$(uname -m)" in
  79. x86_64 | x64 | amd64) echo 'amd64' ;;
  80. i*86 | x86) echo '386' ;;
  81. armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
  82. armv7* | armv7 | arm) echo 'armv7' ;;
  83. armv6* | armv6) echo 'armv6' ;;
  84. armv5* | armv5) echo 'armv5' ;;
  85. s390x) echo 's390x' ;;
  86. *) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" > /dev/null 2>&1 && exit 2 ;;
  87. esac
  88. }
  89. echo "Arch: $(arch)"
  90. gen_random_string() {
  91. local length="$1"
  92. openssl rand -base64 $((length * 2)) \
  93. | tr -dc 'a-zA-Z0-9' \
  94. | head -c "$length"
  95. }
  96. xui_env_file_path() {
  97. case "${release}" in
  98. ubuntu | debian | armbian)
  99. echo "/etc/default/x-ui"
  100. ;;
  101. arch | manjaro | parch | alpine)
  102. echo "/etc/conf.d/x-ui"
  103. ;;
  104. *)
  105. echo "/etc/sysconfig/x-ui"
  106. ;;
  107. esac
  108. }
  109. load_xui_env() {
  110. local env_file
  111. env_file="$(xui_env_file_path)"
  112. if [[ -r "$env_file" ]]; then
  113. set -a
  114. # shellcheck disable=SC1090
  115. source "$env_file"
  116. set +a
  117. fi
  118. }
  119. install_base() {
  120. echo -e "${green}Updating and install dependency packages...${plain}"
  121. case "${release}" in
  122. ubuntu | debian | armbian)
  123. apt-get update > /dev/null 2>&1 && apt-get install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
  124. ;;
  125. fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
  126. dnf makecache -y > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
  127. ;;
  128. centos)
  129. if [[ "${VERSION_ID}" =~ ^7 ]]; then
  130. yum makecache -y > /dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
  131. else
  132. dnf makecache -y > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
  133. fi
  134. ;;
  135. arch | manjaro | parch)
  136. pacman -Sy --noconfirm cronie curl tar tzdata socat openssl > /dev/null 2>&1
  137. ;;
  138. opensuse-tumbleweed | opensuse-leap)
  139. zypper refresh > /dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl > /dev/null 2>&1
  140. ;;
  141. alpine)
  142. apk update > /dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl > /dev/null 2>&1
  143. ;;
  144. *)
  145. apt-get update > /dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
  146. ;;
  147. esac
  148. }
  149. config_after_update() {
  150. local panel_needs_restart=0
  151. echo -e "${yellow}x-ui settings:${plain}"
  152. ${xui_folder}/x-ui setting -show true
  153. ${xui_folder}/x-ui migrate
  154. # Properly detect empty cert by checking if cert: line exists and has content after it
  155. local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2> /dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
  156. local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
  157. local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
  158. # Get server IP
  159. local URL_lists=(
  160. "https://api4.ipify.org"
  161. "https://ipv4.icanhazip.com"
  162. "https://v4.api.ipinfo.io/ip"
  163. "https://ipv4.myexternalip.com/raw"
  164. "https://4.ident.me"
  165. "https://check-host.net/ip"
  166. )
  167. local server_ip=""
  168. for ip_address in "${URL_lists[@]}"; do
  169. local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null)
  170. local http_code=$(echo "$response" | tail -n1)
  171. local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]"')
  172. if [[ "${http_code}" == "200" && "${ip_result}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  173. server_ip="${ip_result}"
  174. break
  175. fi
  176. done
  177. # Only used to print the access URL; never prompt, the web updater has no TTY.
  178. if [[ -z "$server_ip" ]]; then
  179. server_ip=$(hostname -I 2> /dev/null | awk '{print $1}')
  180. server_ip="${server_ip:-<server-ip>}"
  181. fi
  182. # Handle missing/short webBasePath
  183. if [[ ${#existing_webBasePath} -lt 4 ]]; then
  184. echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
  185. local config_webBasePath=$(gen_random_string 18)
  186. ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
  187. existing_webBasePath="${config_webBasePath}"
  188. panel_needs_restart=1
  189. echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
  190. fi
  191. # An update only updates the panel: TLS stays exactly as configured. Set it up
  192. # explicitly from the x-ui menu (SSL Certificate Management).
  193. local access_scheme="http" access_host="${server_ip}"
  194. if [[ -n "$existing_cert" ]]; then
  195. access_scheme="https"
  196. access_host=$(basename "$(dirname "$existing_cert")")
  197. fi
  198. echo ""
  199. echo -e "${green}═══════════════════════════════════════════${plain}"
  200. echo -e "${green} Panel Access Information ${plain}"
  201. echo -e "${green}═══════════════════════════════════════════${plain}"
  202. echo -e "${green}Access URL: ${access_scheme}://${access_host}:${existing_port}/${existing_webBasePath}${plain}"
  203. echo -e "${green}═══════════════════════════════════════════${plain}"
  204. if [[ -z "$existing_cert" ]]; then
  205. echo -e "${yellow}No SSL certificate is configured; the panel is served over HTTP.${plain}"
  206. echo -e "${yellow}Run 'x-ui' and choose SSL Certificate Management to set one up.${plain}"
  207. fi
  208. if [[ "$panel_needs_restart" -eq 1 ]]; then
  209. echo -e "${yellow}Restarting panel to apply the new web base path...${plain}"
  210. systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null
  211. fi
  212. }
  213. # setup_fail2ban auto-installs and configures fail2ban for the IP Limit feature
  214. # by invoking the freshly downloaded x-ui CLI. IP Limit is load-bearing on
  215. # fail2ban (without it the panel disables the limitIp field and zeroes existing
  216. # limits), so updating an older install should make it work without a manual
  217. # trip through the IP Limit menu. Non-fatal: a fail2ban failure must never abort
  218. # the update. XUI_ENABLE_FAIL2BAN is honored (load_xui_env exports it from the
  219. # persisted env file, so a deliberate opt-out survives updates).
  220. setup_fail2ban() {
  221. if [[ -n "${XUI_ENABLE_FAIL2BAN+x}" && "${XUI_ENABLE_FAIL2BAN}" != "true" ]]; then
  222. echo -e "${yellow}XUI_ENABLE_FAIL2BAN=${XUI_ENABLE_FAIL2BAN}, skipping Fail2ban auto-setup.${plain}"
  223. return 0
  224. fi
  225. if [[ ! -x /usr/bin/x-ui ]]; then
  226. echo -e "${yellow}x-ui CLI not found; skipping Fail2ban auto-setup.${plain}"
  227. return 0
  228. fi
  229. # Scripts older than v3.4.0 have no setup-fail2ban and exit 0 from the
  230. # usage banner, which would read as success here.
  231. if ! grep -q '"setup-fail2ban")' /usr/bin/x-ui; then
  232. echo -e "${yellow}This x-ui.sh predates 'x-ui setup-fail2ban'; skipping Fail2ban auto-setup.${plain}"
  233. return 0
  234. fi
  235. echo -e "${green}Setting up Fail2ban for the IP Limit feature...${plain}"
  236. if /usr/bin/x-ui setup-fail2ban; then
  237. echo -e "${green}Fail2ban setup complete.${plain}"
  238. else
  239. echo -e "${yellow}Fail2ban setup did not finish; IP Limit stays disabled until you run 'x-ui' and open the IP Limit menu. Continuing.${plain}"
  240. fi
  241. return 0
  242. }
  243. # The hardened unit makes /usr, /boot, /efi and /etc read-only. The panel's own
  244. # updater is expected to escape that sandbox by running this script through a
  245. # transient systemd-run unit; when systemd-run is unavailable it starts this
  246. # script as a plain child instead, and that child inherits the sandbox and then
  247. # cannot write anything this update needs. Say so once, up front, instead of
  248. # dying partway through with "Failed to download x-ui".
  249. require_writable_update_paths() {
  250. local dir probe
  251. for dir in "${xui_folder%/*}" "/usr/bin"; do
  252. [[ -n "$dir" && -d "$dir" ]] || continue
  253. probe="${dir}/.x-ui-write-test.$$"
  254. # A real write test rather than [[ -w ]]: this runs as root, where a
  255. # permission bit means little and the test only reflects the file mode
  256. # and the mount flags, not an immutable attribute or a full filesystem.
  257. if ! : > "$probe" 2> /dev/null; then
  258. _fail "ERROR: ${dir} is not writable for this process (read-only mount, attribute or full filesystem). The panel's fallback updater cannot run inside the hardened systemd sandbox; update from the panel UI (which uses systemd-run) or run 'x-ui update' in a shell."
  259. fi
  260. rm -f "$probe"
  261. done
  262. }
  263. # Major version of the local systemd, 0 when it cannot be determined. The
  264. # SystemCallFilter=@system-service group only exists from systemd 239 on (other
  265. # @-named groups exist since 231); on older versions an unknown group is not
  266. # ignored safely, the filter stays in force and leaves a whitelist the panel
  267. # cannot run under.
  268. _xui_systemd_major_version() {
  269. local version=""
  270. if command -v systemctl > /dev/null 2>&1; then
  271. version="$(systemctl --version 2>/dev/null | awk 'NR == 1 {print $2}')"
  272. fi
  273. if [[ ! "$version" =~ ^[0-9]+$ ]]; then
  274. echo 0
  275. return 0
  276. fi
  277. echo "$version"
  278. }
  279. # The shipped units list hardening that older systemd does not know: the
  280. # directive is logged and ignored at load time rather than rejected, so the
  281. # panel still starts, only without that protection. Each entry is the systemd
  282. # release that introduced the directive (systemd.exec(5)); everything else in
  283. # the unit predates the oldest systemd install.sh supports (CentOS 7 has 219).
  284. # SystemCallFilter= is listed because the drop-in only writes it from 239 on.
  285. _xui_warn_unsupported_hardening() {
  286. local version entry missing=""
  287. version="$(_xui_systemd_major_version)"
  288. [[ "$version" -gt 0 ]] || return 0
  289. for entry in RestrictRealtime:231 ReadWritePaths:231 ProtectKernelTunables:232 \
  290. ProtectKernelModules:232 RestrictNamespaces:233 LockPersonality:235 \
  291. SystemCallFilter:239 ProtectHostname:242 RestrictSUIDSGID:242 \
  292. ProtectKernelLogs:244 ProtectClock:245; do
  293. if [[ "$version" -lt "${entry##*:}" ]]; then
  294. missing="${missing:+$missing, }${entry%%:*} (${entry##*:})"
  295. fi
  296. done
  297. [[ -n "$missing" ]] || return 0
  298. echo -e "${yellow}Note: systemd ${version} ignores part of the hardening in x-ui.service; the panel still starts.${plain}"
  299. echo " Not applied, needs a newer systemd: ${missing}."
  300. if [[ "$version" -lt 231 ]]; then
  301. echo " The panel's folders stay writable through ReadWriteDirectories=, the alias this script installs."
  302. fi
  303. echo " The rest of the hardening is in force. Upgrade systemd to apply the above."
  304. return 0
  305. }
  306. # ProtectSystem=full makes /usr, /boot, /efi and /etc read-only. ProtectSystem=
  307. # strict would make the whole hierarchy read-only (only the kernel API
  308. # filesystems stay as they are), and that would break the panel's own use of
  309. # /tmp. The panel's stores are configurable (XUI_DB_FOLDER, XUI_LOG_FOLDER,
  310. # XUI_BIN_FOLDER), and XUI_MAIN_FOLDER is the folder install.sh/update.sh place
  311. # the files in -- the unit's WorkingDirectory on a stock install, and what a
  312. # relative XUI_BIN_FOLDER is resolved against. So a hard-coded list in the unit
  313. # either misses a relocated store -- the panel then cannot write its own SQLite
  314. # database and sits in a Restart=on-failure loop -- or forces the operator to
  315. # edit a file that every install/update overwrites from the release tarball.
  316. # install.sh and update.sh therefore regenerate the drop-in from the folders
  317. # actually in use, and the unit's own ReadWritePaths only carry the
  318. # plain-install defaults. A relocated store means re-running install or update:
  319. # the drop-in is only written here.
  320. _xui_service_write_paths_dropin() {
  321. # $1 is the env file to resolve the XUI_* folders from; callers pass nothing
  322. # and get the OS-specific path the unit itself uses.
  323. local env_file="${1:-}"
  324. local dropin_dir dropin temp_file
  325. local db_folder log_folder bin_folder main_folder
  326. local path line="" whitespace_paths="" seen_paths="" escaped_path
  327. if [[ -z "$env_file" ]]; then
  328. env_file="$(xui_env_file_path)"
  329. fi
  330. if [[ -r "$env_file" ]]; then
  331. set -a
  332. # shellcheck disable=SC1090
  333. source "$env_file"
  334. set +a
  335. fi
  336. # XUI_* wins over the script's own default: the unit hands that same env
  337. # file to the panel through EnvironmentFile=, so these are the folders it
  338. # will actually use.
  339. main_folder="${XUI_MAIN_FOLDER:-${xui_folder}}"
  340. db_folder="${XUI_DB_FOLDER:-/etc/x-ui}"
  341. log_folder="${XUI_LOG_FOLDER:-/var/log/x-ui}"
  342. # An empty XUI_BIN_FOLDER resolves to "bin" relative to the panel's working
  343. # directory, which the unit sets to the main folder.
  344. bin_folder="${XUI_BIN_FOLDER:-bin}"
  345. if [[ "$bin_folder" != /* ]]; then
  346. bin_folder="${main_folder%/}/${bin_folder#./}"
  347. fi
  348. for path in "$db_folder" "$log_folder" "$bin_folder" "$main_folder"; do
  349. [[ "$path" == /* ]] || continue
  350. # ReadWritePaths= is a whitespace-separated list, and a folder whose
  351. # name contains whitespace cannot be written into it without relying on
  352. # quoting. A wrong entry makes systemd reject the whole drop-in and the
  353. # panel would not start, so leave such a folder out and say so instead.
  354. if [[ "$path" != "${path//[[:space:]]/}" ]]; then
  355. whitespace_paths="${whitespace_paths:+$whitespace_paths }$path"
  356. continue
  357. fi
  358. case " $seen_paths " in
  359. *" $path "*) continue ;;
  360. esac
  361. seen_paths="${seen_paths}${seen_paths:+ }$path"
  362. # systemd expands %-specifiers in unit files, so a folder name carrying
  363. # a literal % has to be written as %%, or the entry stops naming the
  364. # folder systemd is meant to keep writable.
  365. escaped_path="${path//%/%%}"
  366. line="${line} -${escaped_path}"
  367. done
  368. if [[ -n "$whitespace_paths" ]]; then
  369. echo "Warning: these folders contain whitespace and were left out of" >&2
  370. echo " 10-xui-sandbox.conf: $whitespace_paths" >&2
  371. echo " The panel cannot write to them under the unit's sandbox." >&2
  372. fi
  373. line="${line# }"
  374. [[ -n "$line" ]] || return 1
  375. dropin_dir="${xui_service}/x-ui.service.d"
  376. dropin="${dropin_dir}/10-xui-sandbox.conf"
  377. temp_file="${dropin}.tmp.$$"
  378. mkdir -p "$dropin_dir" || return 1
  379. cat > "$temp_file" << EOF
  380. # Regenerated by install.sh/update.sh on every install and update: edits here
  381. # are lost, and the list only reflects the XUI_* variables read from
  382. # ${env_file} at that moment. Re-run install/update after moving a store.
  383. # It lists the folders the panel writes to. Put local additions in their own
  384. # drop-in, for example 20-x-ui-local.conf, which nothing here touches.
  385. [Service]
  386. ReadWritePaths=${line}
  387. ReadWriteDirectories=${line}
  388. EOF
  389. if [[ "$(_xui_systemd_major_version)" -ge 239 ]]; then
  390. cat >> "$temp_file" << 'EOF'
  391. # @system-service needs systemd >= 239; on older versions the unknown group
  392. # would leave the panel with a filter it cannot start under (x-ui.service.*).
  393. SystemCallFilter=@system-service
  394. SystemCallErrorNumber=EPERM
  395. EOF
  396. fi
  397. if [[ ! -s "$temp_file" ]]; then
  398. rm -f "$temp_file"
  399. return 1
  400. fi
  401. chmod 644 "$temp_file"
  402. mv -f "$temp_file" "$dropin" || { rm -f "$temp_file"; return 1; }
  403. if command -v systemctl > /dev/null 2>&1; then
  404. systemctl daemon-reload > /dev/null 2>&1 || true
  405. fi
  406. return 0
  407. }
  408. # Lands a systemd unit file at ${xui_service}/x-ui.service via a temp file +
  409. # atomic mv, so a failed cp/curl or an interrupted mv never leaves a
  410. # truncated unit file at the live path -- systemd would then fail to parse
  411. # it on the next daemon-reload/start. Same pattern already used for
  412. # /usr/bin/x-ui elsewhere in this script. source_is_url picks cp (from a
  413. # file already extracted from the release tarball) vs curl (GitHub fallback).
  414. _install_xui_service_unit() {
  415. local source="$1"
  416. local source_is_url="$2"
  417. local dest="${xui_service}/x-ui.service"
  418. local temp_file="${dest}.tmp.$$"
  419. rm -f "$temp_file"
  420. if [[ "$source_is_url" == "true" ]]; then
  421. ${curl_bin} -fLRo "$temp_file" "$source" > /dev/null 2>&1
  422. else
  423. cp -f "$source" "$temp_file" > /dev/null 2>&1
  424. fi
  425. if [[ $? -ne 0 ]]; then
  426. rm -f "$temp_file"
  427. return 1
  428. fi
  429. if [[ ! -s "$temp_file" ]]; then
  430. rm -f "$temp_file"
  431. return 1
  432. fi
  433. mv -f "$temp_file" "$dest"
  434. if [[ $? -ne 0 ]]; then
  435. rm -f "$temp_file"
  436. return 1
  437. fi
  438. if ! _xui_service_write_paths_dropin; then
  439. echo -e "${yellow}Warning: could not refresh ${xui_service}/x-ui.service.d/10-xui-sandbox.conf.${plain}"
  440. echo -e "${yellow}If XUI_DB_FOLDER or XUI_LOG_FOLDER points outside /etc/x-ui and /var/log/x-ui, the panel may not be able to write to it under ProtectSystem=full.${plain}"
  441. fi
  442. _xui_warn_unsupported_hardening
  443. return 0
  444. }
  445. # Older tags predate some of these files (x-ui.rc arrived in v2.8.4). Serving
  446. # main's copy against an old binary is the mismatch this pinning exists to
  447. # prevent, so probe before the old install is removed and refuse the tag.
  448. require_repo_files() {
  449. local ref="$1" name status
  450. shift
  451. [[ "${ref}" == "main" ]] && return 0
  452. for name in "$@"; do
  453. status=$(${curl_bin} -sIL --retry 3 --connect-timeout 15 -o /dev/null -w '%{http_code}' "https://raw.githubusercontent.com/MHSanaei/3x-ui/${ref}/${name}")
  454. if [[ "${status}" != "200" ]]; then
  455. _fail "ERROR: ${name} is not available for ${ref} (HTTP ${status}). Update to a release that ships it, or to 'dev-latest'. The current installation is untouched."
  456. fi
  457. done
  458. }
  459. update_x-ui() {
  460. cd ${xui_folder%/x-ui}/
  461. load_xui_env
  462. if [ -f "${xui_folder}/x-ui" ]; then
  463. current_xui_version=$(${xui_folder}/x-ui -v)
  464. echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
  465. else
  466. _fail "ERROR: Current x-ui version: unknown"
  467. fi
  468. echo -e "${green}Downloading new x-ui version...${plain}"
  469. # XUI_UPDATE_TAG lets the panel target a specific release tag (e.g. the
  470. # rolling dev-latest pre-release). Empty keeps the default latest-stable flow.
  471. if [[ -n "${XUI_UPDATE_TAG}" ]]; then
  472. tag_version="${XUI_UPDATE_TAG}"
  473. echo -e "${green}Using update tag: ${tag_version}${plain}"
  474. else
  475. tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2> /dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
  476. if [[ ! -n "$tag_version" ]]; then
  477. _fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
  478. fi
  479. fi
  480. echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
  481. # x-ui.sh, x-ui.rc and the unit files must come from the same release as
  482. # the binary; only the rolling dev build tracks main.
  483. script_ref="${tag_version}"
  484. if [[ "${tag_version}" == "dev-latest" ]]; then
  485. script_ref="main"
  486. fi
  487. # The unit files are only fetched when the release tarball lacks them, so
  488. # they are checked at that point instead of here.
  489. local required_files=("x-ui.sh")
  490. [[ $release == "alpine" ]] && required_files+=("x-ui.rc")
  491. require_repo_files "${script_ref}" "${required_files[@]}"
  492. ${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2> /dev/null
  493. if [[ $? -ne 0 ]]; then
  494. _fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
  495. fi
  496. if [[ ! -s ${xui_folder}-linux-$(arch).tar.gz ]]; then
  497. rm ${xui_folder}-linux-$(arch).tar.gz -f > /dev/null 2>&1
  498. _fail "ERROR: Downloaded x-ui release archive is empty, please be sure that your server can access GitHub"
  499. fi
  500. # Releases publish <asset>.sha256 next to each archive. A mismatch or a
  501. # failed sidecar download aborts the update; only a 404 (releases
  502. # predating the sidecar) is tolerated with a warning.
  503. archive="${xui_folder}-linux-$(arch).tar.gz"
  504. rm -f "${archive}.sha256"
  505. sidecar_code=$(${curl_bin} -sL --retry 3 --retry-delay 3 --connect-timeout 15 --max-time 60 -o "${archive}.sha256" -w '%{http_code}' "https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz.sha256" 2> /dev/null)
  506. if [[ "${sidecar_code}" == "200" ]]; then
  507. expected_sha256=$(awk 'NR == 1 {print $1}' "${archive}.sha256")
  508. actual_sha256=$(sha256sum "${archive}" | awk '{print $1}')
  509. rm -f "${archive}.sha256"
  510. if [[ ! "${expected_sha256}" =~ ^[0-9a-f]{64}$ || "${expected_sha256}" != "${actual_sha256}" ]]; then
  511. rm -f "${archive}"
  512. _fail "ERROR: Checksum mismatch for $(basename "${archive}"): expected ${expected_sha256:-<none>}, got ${actual_sha256}"
  513. fi
  514. echo -e "${green}Checksum verified: ${actual_sha256}${plain}"
  515. elif [[ "${sidecar_code}" == "404" ]]; then
  516. rm -f "${archive}.sha256"
  517. echo -e "${yellow}No checksum published for this release, skipping verification${plain}"
  518. else
  519. rm -f "${archive}.sha256" "${archive}"
  520. _fail "ERROR: Failed to download the checksum for x-ui-linux-$(arch).tar.gz (HTTP ${sidecar_code})"
  521. fi
  522. if [[ -e ${xui_folder}/ ]]; then
  523. echo -e "${green}Stopping x-ui...${plain}"
  524. if [[ $release == "alpine" ]]; then
  525. if [ -f "/etc/init.d/x-ui" ]; then
  526. rc-service x-ui stop > /dev/null 2>&1
  527. rc-update del x-ui > /dev/null 2>&1
  528. echo -e "${green}Removing old service unit version...${plain}"
  529. rm -f /etc/init.d/x-ui > /dev/null 2>&1
  530. else
  531. rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
  532. _fail "ERROR: x-ui service unit not installed."
  533. fi
  534. else
  535. if [ -f "${xui_service}/x-ui.service" ]; then
  536. systemctl stop x-ui > /dev/null 2>&1
  537. systemctl disable x-ui > /dev/null 2>&1
  538. echo -e "${green}Removing old systemd unit version...${plain}"
  539. rm ${xui_service}/x-ui.service -f > /dev/null 2>&1
  540. systemctl daemon-reload > /dev/null 2>&1
  541. else
  542. rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
  543. _fail "ERROR: x-ui systemd unit not installed."
  544. fi
  545. fi
  546. # Kill any leftover mtg (MTProto) sidecars. x-ui runs them outside its own
  547. # lifecycle, so on Linux a stale one can survive the stop and keep holding
  548. # an inbound port with an outdated secret, silently breaking new clients.
  549. # The new panel respawns a clean mtg per inbound on next start.
  550. pkill -f 'mtg-linux-[^ ]* run ' > /dev/null 2>&1 || true
  551. pkill -f 'tuic-server.*-c .*bin/tuic/tuic_[0-9]+\.json' > /dev/null 2>&1 || true
  552. echo -e "${green}Removing old x-ui version...${plain}"
  553. rm ${xui_folder} -f > /dev/null 2>&1
  554. rm ${xui_folder}/x-ui.service -f > /dev/null 2>&1
  555. rm ${xui_folder}/x-ui.service.debian -f > /dev/null 2>&1
  556. rm ${xui_folder}/x-ui.service.arch -f > /dev/null 2>&1
  557. rm ${xui_folder}/x-ui.service.rhel -f > /dev/null 2>&1
  558. rm ${xui_folder}/x-ui -f > /dev/null 2>&1
  559. rm ${xui_folder}/x-ui.sh -f > /dev/null 2>&1
  560. echo -e "${green}Removing old mtg version...${plain}"
  561. rm ${xui_folder}/bin/mtg-linux-$(arch) -f > /dev/null 2>&1
  562. echo -e "${green}Removing old xray version...${plain}"
  563. rm ${xui_folder}/bin/xray-linux-$(arch) -f > /dev/null 2>&1
  564. echo -e "${green}Removing old README and LICENSE file...${plain}"
  565. rm ${xui_folder}/bin/README.md -f > /dev/null 2>&1
  566. rm ${xui_folder}/bin/LICENSE -f > /dev/null 2>&1
  567. rm ${xui_folder}/bin/tuic-server -f > /dev/null 2>&1
  568. rm ${xui_folder}/bin/tuic -rf > /dev/null 2>&1
  569. else
  570. rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
  571. _fail "ERROR: x-ui not installed."
  572. fi
  573. echo -e "${green}Installing new x-ui version...${plain}"
  574. tar zxvf x-ui-linux-$(arch).tar.gz > /dev/null 2>&1
  575. if [[ $? -ne 0 ]]; then
  576. rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
  577. _fail "ERROR: 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 update again"
  578. fi
  579. rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
  580. cd x-ui > /dev/null 2>&1
  581. if [[ $? -ne 0 || ! -s x-ui ]]; then
  582. _fail "ERROR: 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 update again"
  583. fi
  584. chmod +x x-ui > /dev/null 2>&1
  585. # Check the system's architecture and rename the file accordingly.
  586. # The panel binary maps GOARCH=arm to "arm32" (internal/xray/process.go),
  587. # so the Xray binary must be named xray-linux-arm32; mtg keeps plain "arm".
  588. if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
  589. mv bin/xray-linux-$(arch) bin/xray-linux-arm32 > /dev/null 2>&1
  590. chmod +x bin/xray-linux-arm32 > /dev/null 2>&1
  591. if [[ -f bin/mtg-linux-$(arch) ]]; then
  592. mv bin/mtg-linux-$(arch) bin/mtg-linux-arm > /dev/null 2>&1
  593. chmod +x bin/mtg-linux-arm > /dev/null 2>&1
  594. fi
  595. fi
  596. chmod +x x-ui bin/xray-linux-$(arch) > /dev/null 2>&1
  597. if [[ -f bin/mtg-linux-arm ]]; then
  598. chmod +x bin/mtg-linux-arm > /dev/null 2>&1
  599. elif [[ -f bin/mtg-linux-$(arch) ]]; then
  600. chmod +x bin/mtg-linux-$(arch) > /dev/null 2>&1
  601. fi
  602. echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
  603. local xui_script_temp="/usr/bin/x-ui-temp.$$"
  604. rm -f "${xui_script_temp}"
  605. ${curl_bin} -fLRo "${xui_script_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.sh" > /dev/null 2>&1
  606. if [[ $? -ne 0 ]]; then
  607. rm -f "${xui_script_temp}"
  608. _fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
  609. fi
  610. if [[ ! -s "${xui_script_temp}" ]]; then
  611. rm -f "${xui_script_temp}"
  612. _fail "ERROR: Downloaded x-ui.sh script is empty, please be sure that your server can access GitHub"
  613. fi
  614. mv -f "${xui_script_temp}" /usr/bin/x-ui
  615. if [[ $? -ne 0 ]]; then
  616. rm -f "${xui_script_temp}"
  617. _fail "ERROR: Failed to install x-ui.sh script"
  618. fi
  619. chmod +x ${xui_folder}/x-ui.sh > /dev/null 2>&1
  620. chmod +x /usr/bin/x-ui > /dev/null 2>&1
  621. mkdir -p /var/log/x-ui > /dev/null 2>&1
  622. echo -e "${green}Changing owner...${plain}"
  623. chown -R root:root ${xui_folder} > /dev/null 2>&1
  624. if [ -f "${xui_folder}/bin/config.json" ]; then
  625. echo -e "${green}Changing on config file permissions...${plain}"
  626. chmod 640 ${xui_folder}/bin/config.json > /dev/null 2>&1
  627. fi
  628. # Finish the schema/data migrations before the service starts, so the service and
  629. # config_after_update's CLI calls never run them on the same database at once (#6728).
  630. echo -e "${green}Migrating database...${plain}"
  631. "${xui_folder}/x-ui" migrate
  632. if [[ $release == "alpine" ]]; then
  633. echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
  634. xui_rc_temp="/etc/init.d/x-ui.tmp.$$"
  635. rm -f "${xui_rc_temp}"
  636. ${curl_bin} -fLRo "${xui_rc_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.rc" > /dev/null 2>&1
  637. if [[ $? -ne 0 ]]; then
  638. rm -f "${xui_rc_temp}"
  639. _fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
  640. fi
  641. if [[ ! -s "${xui_rc_temp}" ]]; then
  642. rm -f "${xui_rc_temp}"
  643. _fail "ERROR: Downloaded startup unit x-ui.rc is empty, please be sure that your server can access GitHub"
  644. fi
  645. mv -f "${xui_rc_temp}" /etc/init.d/x-ui
  646. if [[ $? -ne 0 ]]; then
  647. rm -f "${xui_rc_temp}"
  648. _fail "ERROR: Failed to install startup unit x-ui.rc"
  649. fi
  650. chmod +x /etc/init.d/x-ui > /dev/null 2>&1
  651. chown root:root /etc/init.d/x-ui > /dev/null 2>&1
  652. rc-update add x-ui > /dev/null 2>&1
  653. rc-service x-ui start > /dev/null 2>&1
  654. else
  655. if [ -f "x-ui.service" ]; then
  656. echo -e "${green}Installing systemd unit...${plain}"
  657. if ! _install_xui_service_unit "x-ui.service" "false"; then
  658. echo -e "${red}Failed to copy x-ui.service${plain}"
  659. exit 1
  660. fi
  661. else
  662. service_installed=false
  663. case "${release}" in
  664. ubuntu | debian | armbian)
  665. if [ -f "x-ui.service.debian" ]; then
  666. echo -e "${green}Installing debian-like systemd unit...${plain}"
  667. if _install_xui_service_unit "x-ui.service.debian" "false"; then
  668. service_installed=true
  669. fi
  670. fi
  671. ;;
  672. arch | manjaro | parch)
  673. if [ -f "x-ui.service.arch" ]; then
  674. echo -e "${green}Installing arch-like systemd unit...${plain}"
  675. if _install_xui_service_unit "x-ui.service.arch" "false"; then
  676. service_installed=true
  677. fi
  678. fi
  679. ;;
  680. *)
  681. if [ -f "x-ui.service.rhel" ]; then
  682. echo -e "${green}Installing rhel-like systemd unit...${plain}"
  683. if _install_xui_service_unit "x-ui.service.rhel" "false"; then
  684. service_installed=true
  685. fi
  686. fi
  687. ;;
  688. esac
  689. # If service file not found in tar.gz, download from GitHub
  690. if [ "$service_installed" = false ]; then
  691. echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
  692. case "${release}" in
  693. ubuntu | debian | armbian)
  694. service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.service.debian"
  695. ;;
  696. arch | manjaro | parch)
  697. service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.service.arch"
  698. ;;
  699. *)
  700. service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.service.rhel"
  701. ;;
  702. esac
  703. if ! _install_xui_service_unit "$service_unit_url" "true"; then
  704. echo -e "${red}Failed to install x-ui.service from GitHub (${script_ref}) -- the release tarball did not ship one either${plain}"
  705. exit 1
  706. fi
  707. fi
  708. fi
  709. chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1
  710. chmod 644 ${xui_service}/x-ui.service > /dev/null 2>&1
  711. systemctl daemon-reload > /dev/null 2>&1
  712. systemctl enable x-ui > /dev/null 2>&1
  713. systemctl start x-ui > /dev/null 2>&1
  714. fi
  715. config_after_update
  716. # IP Limit relies on fail2ban; install + configure it now so the feature
  717. # works out of the box on update too (no-op when XUI_ENABLE_FAIL2BAN=false).
  718. # Never fatal.
  719. setup_fail2ban
  720. echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
  721. echo -e ""
  722. echo -e "┌───────────────────────────────────────────────────────┐
  723. │ ${blue}x-ui control menu usages (subcommands):${plain} │
  724. │ │
  725. │ ${blue}x-ui${plain} - Admin Management Script │
  726. │ ${blue}x-ui start${plain} - Start │
  727. │ ${blue}x-ui stop${plain} - Stop │
  728. │ ${blue}x-ui restart${plain} - Restart │
  729. │ ${blue}x-ui status${plain} - Current Status │
  730. │ ${blue}x-ui settings${plain} - Current Settings │
  731. │ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
  732. │ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
  733. │ ${blue}x-ui log${plain} - Check logs │
  734. │ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
  735. │ ${blue}x-ui update${plain} - Update │
  736. │ ${blue}x-ui legacy${plain} - Legacy version │
  737. │ ${blue}x-ui install${plain} - Install │
  738. │ ${blue}x-ui uninstall${plain} - Uninstall │
  739. └───────────────────────────────────────────────────────┘"
  740. }
  741. echo -e "${green}Running...${plain}"
  742. require_writable_update_paths
  743. install_base
  744. update_x-ui $1