|
|
@@ -940,6 +940,180 @@ setup_fail2ban() {
|
|
|
return 0
|
|
|
}
|
|
|
|
|
|
+# The hardened unit makes /usr, /boot, /efi and /etc read-only. The panel's own
|
|
|
+# updater is expected to escape that sandbox by running this script through a
|
|
|
+# transient systemd-run unit; when systemd-run is unavailable it starts this
|
|
|
+# script as a plain child instead, and that child inherits the sandbox and then
|
|
|
+# cannot write anything this update needs. Say so once, up front, instead of
|
|
|
+# dying partway through with "Failed to download x-ui".
|
|
|
+require_writable_update_paths() {
|
|
|
+ local dir probe
|
|
|
+ for dir in "${xui_folder%/*}" "/usr/bin"; do
|
|
|
+ [[ -n "$dir" && -d "$dir" ]] || continue
|
|
|
+ probe="${dir}/.x-ui-write-test.$$"
|
|
|
+ # A real write test rather than [[ -w ]]: this runs as root, where a
|
|
|
+ # permission bit means little and the test only reflects the file mode
|
|
|
+ # and the mount flags, not an immutable attribute or a full filesystem.
|
|
|
+ if ! : > "$probe" 2> /dev/null; then
|
|
|
+ _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."
|
|
|
+ fi
|
|
|
+ rm -f "$probe"
|
|
|
+ done
|
|
|
+}
|
|
|
+
|
|
|
+# Major version of the local systemd, 0 when it cannot be determined. The
|
|
|
+# SystemCallFilter=@system-service group only exists from systemd 239 on (other
|
|
|
+# @-named groups exist since 231); on older versions an unknown group is not
|
|
|
+# ignored safely, the filter stays in force and leaves a whitelist the panel
|
|
|
+# cannot run under.
|
|
|
+_xui_systemd_major_version() {
|
|
|
+ local version=""
|
|
|
+ if command -v systemctl > /dev/null 2>&1; then
|
|
|
+ version="$(systemctl --version 2>/dev/null | awk 'NR == 1 {print $2}')"
|
|
|
+ fi
|
|
|
+ if [[ ! "$version" =~ ^[0-9]+$ ]]; then
|
|
|
+ echo 0
|
|
|
+ return 0
|
|
|
+ fi
|
|
|
+ echo "$version"
|
|
|
+}
|
|
|
+
|
|
|
+# The shipped units list hardening that older systemd does not know: the
|
|
|
+# directive is logged and ignored at load time rather than rejected, so the
|
|
|
+# panel still starts, only without that protection. Each entry is the systemd
|
|
|
+# release that introduced the directive (systemd.exec(5)); everything else in
|
|
|
+# the unit predates the oldest systemd install.sh supports (CentOS 7 has 219).
|
|
|
+# SystemCallFilter= is listed because the drop-in only writes it from 239 on.
|
|
|
+_xui_warn_unsupported_hardening() {
|
|
|
+ local version entry missing=""
|
|
|
+ version="$(_xui_systemd_major_version)"
|
|
|
+ [[ "$version" -gt 0 ]] || return 0
|
|
|
+ for entry in RestrictRealtime:231 ReadWritePaths:231 ProtectKernelTunables:232 \
|
|
|
+ ProtectKernelModules:232 RestrictNamespaces:233 LockPersonality:235 \
|
|
|
+ SystemCallFilter:239 ProtectHostname:242 RestrictSUIDSGID:242 \
|
|
|
+ ProtectKernelLogs:244 ProtectClock:245; do
|
|
|
+ if [[ "$version" -lt "${entry##*:}" ]]; then
|
|
|
+ missing="${missing:+$missing, }${entry%%:*} (${entry##*:})"
|
|
|
+ fi
|
|
|
+ done
|
|
|
+ [[ -n "$missing" ]] || return 0
|
|
|
+ echo -e "${yellow}Note: systemd ${version} ignores part of the hardening in x-ui.service; the panel still starts.${plain}"
|
|
|
+ echo " Not applied, needs a newer systemd: ${missing}."
|
|
|
+ if [[ "$version" -lt 231 ]]; then
|
|
|
+ echo " The panel's folders stay writable through ReadWriteDirectories=, the alias this script installs."
|
|
|
+ fi
|
|
|
+ echo " The rest of the hardening is in force. Upgrade systemd to apply the above."
|
|
|
+ return 0
|
|
|
+}
|
|
|
+
|
|
|
+# ProtectSystem=full makes /usr, /boot, /efi and /etc read-only. ProtectSystem=
|
|
|
+# strict would make the whole hierarchy read-only (only the kernel API
|
|
|
+# filesystems stay as they are), and that would break the panel's own use of
|
|
|
+# /tmp. The panel's stores are configurable (XUI_DB_FOLDER, XUI_LOG_FOLDER,
|
|
|
+# XUI_BIN_FOLDER), and XUI_MAIN_FOLDER is the folder install.sh/update.sh place
|
|
|
+# the files in -- the unit's WorkingDirectory on a stock install, and what a
|
|
|
+# relative XUI_BIN_FOLDER is resolved against. So a hard-coded list in the unit
|
|
|
+# either misses a relocated store -- the panel then cannot write its own SQLite
|
|
|
+# database and sits in a Restart=on-failure loop -- or forces the operator to
|
|
|
+# edit a file that every install/update overwrites from the release tarball.
|
|
|
+# install.sh and update.sh therefore regenerate the drop-in from the folders
|
|
|
+# actually in use, and the unit's own ReadWritePaths only carry the
|
|
|
+# plain-install defaults. A relocated store means re-running install or update:
|
|
|
+# the drop-in is only written here.
|
|
|
+_xui_service_write_paths_dropin() {
|
|
|
+ # $1 is the env file to resolve the XUI_* folders from; callers pass nothing
|
|
|
+ # and get the OS-specific path the unit itself uses.
|
|
|
+ local env_file="${1:-}"
|
|
|
+ local dropin_dir dropin temp_file
|
|
|
+ local db_folder log_folder bin_folder main_folder
|
|
|
+ local path line="" whitespace_paths="" seen_paths="" escaped_path
|
|
|
+
|
|
|
+ if [[ -z "$env_file" ]]; then
|
|
|
+ env_file="$(xui_env_file_path)"
|
|
|
+ fi
|
|
|
+ if [[ -r "$env_file" ]]; then
|
|
|
+ set -a
|
|
|
+ # shellcheck disable=SC1090
|
|
|
+ source "$env_file"
|
|
|
+ set +a
|
|
|
+ fi
|
|
|
+
|
|
|
+ # XUI_* wins over the script's own default: the unit hands that same env
|
|
|
+ # file to the panel through EnvironmentFile=, so these are the folders it
|
|
|
+ # will actually use.
|
|
|
+ main_folder="${XUI_MAIN_FOLDER:-${xui_folder}}"
|
|
|
+ db_folder="${XUI_DB_FOLDER:-/etc/x-ui}"
|
|
|
+ log_folder="${XUI_LOG_FOLDER:-/var/log/x-ui}"
|
|
|
+ # An empty XUI_BIN_FOLDER resolves to "bin" relative to the panel's working
|
|
|
+ # directory, which the unit sets to the main folder.
|
|
|
+ bin_folder="${XUI_BIN_FOLDER:-bin}"
|
|
|
+ if [[ "$bin_folder" != /* ]]; then
|
|
|
+ bin_folder="${main_folder%/}/${bin_folder#./}"
|
|
|
+ fi
|
|
|
+
|
|
|
+ for path in "$db_folder" "$log_folder" "$bin_folder" "$main_folder"; do
|
|
|
+ [[ "$path" == /* ]] || continue
|
|
|
+ # ReadWritePaths= is a whitespace-separated list, and a folder whose
|
|
|
+ # name contains whitespace cannot be written into it without relying on
|
|
|
+ # quoting. A wrong entry makes systemd reject the whole drop-in and the
|
|
|
+ # panel would not start, so leave such a folder out and say so instead.
|
|
|
+ if [[ "$path" != "${path//[[:space:]]/}" ]]; then
|
|
|
+ whitespace_paths="${whitespace_paths:+$whitespace_paths }$path"
|
|
|
+ continue
|
|
|
+ fi
|
|
|
+ case " $seen_paths " in
|
|
|
+ *" $path "*) continue ;;
|
|
|
+ esac
|
|
|
+ seen_paths="${seen_paths}${seen_paths:+ }$path"
|
|
|
+ # systemd expands %-specifiers in unit files, so a folder name carrying
|
|
|
+ # a literal % has to be written as %%, or the entry stops naming the
|
|
|
+ # folder systemd is meant to keep writable.
|
|
|
+ escaped_path="${path//%/%%}"
|
|
|
+ line="${line} -${escaped_path}"
|
|
|
+ done
|
|
|
+ if [[ -n "$whitespace_paths" ]]; then
|
|
|
+ echo "Warning: these folders contain whitespace and were left out of" >&2
|
|
|
+ echo " 10-xui-sandbox.conf: $whitespace_paths" >&2
|
|
|
+ echo " The panel cannot write to them under the unit's sandbox." >&2
|
|
|
+ fi
|
|
|
+ line="${line# }"
|
|
|
+ [[ -n "$line" ]] || return 1
|
|
|
+
|
|
|
+ dropin_dir="${xui_service}/x-ui.service.d"
|
|
|
+ dropin="${dropin_dir}/10-xui-sandbox.conf"
|
|
|
+ temp_file="${dropin}.tmp.$$"
|
|
|
+
|
|
|
+ mkdir -p "$dropin_dir" || return 1
|
|
|
+ cat > "$temp_file" << EOF
|
|
|
+# Regenerated by install.sh/update.sh on every install and update: edits here
|
|
|
+# are lost, and the list only reflects the XUI_* variables read from
|
|
|
+# ${env_file} at that moment. Re-run install/update after moving a store.
|
|
|
+# It lists the folders the panel writes to. Put local additions in their own
|
|
|
+# drop-in, for example 20-x-ui-local.conf, which nothing here touches.
|
|
|
+[Service]
|
|
|
+ReadWritePaths=${line}
|
|
|
+ReadWriteDirectories=${line}
|
|
|
+EOF
|
|
|
+ if [[ "$(_xui_systemd_major_version)" -ge 239 ]]; then
|
|
|
+ cat >> "$temp_file" << 'EOF'
|
|
|
+# @system-service needs systemd >= 239; on older versions the unknown group
|
|
|
+# would leave the panel with a filter it cannot start under (x-ui.service.*).
|
|
|
+SystemCallFilter=@system-service
|
|
|
+SystemCallErrorNumber=EPERM
|
|
|
+EOF
|
|
|
+ fi
|
|
|
+ if [[ ! -s "$temp_file" ]]; then
|
|
|
+ rm -f "$temp_file"
|
|
|
+ return 1
|
|
|
+ fi
|
|
|
+ chmod 644 "$temp_file"
|
|
|
+ mv -f "$temp_file" "$dropin" || { rm -f "$temp_file"; return 1; }
|
|
|
+ if command -v systemctl > /dev/null 2>&1; then
|
|
|
+ systemctl daemon-reload > /dev/null 2>&1 || true
|
|
|
+ fi
|
|
|
+ return 0
|
|
|
+}
|
|
|
+
|
|
|
# Lands a systemd unit file at ${xui_service}/x-ui.service via a temp file +
|
|
|
# atomic mv, so a failed cp/curl or an interrupted mv never leaves a
|
|
|
# truncated unit file at the live path -- systemd would then fail to parse
|
|
|
@@ -971,6 +1145,11 @@ _install_xui_service_unit() {
|
|
|
rm -f "$temp_file"
|
|
|
return 1
|
|
|
fi
|
|
|
+ if ! _xui_service_write_paths_dropin; then
|
|
|
+ echo -e "${yellow}Warning: could not refresh ${xui_service}/x-ui.service.d/10-xui-sandbox.conf.${plain}"
|
|
|
+ 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}"
|
|
|
+ fi
|
|
|
+ _xui_warn_unsupported_hardening
|
|
|
return 0
|
|
|
}
|
|
|
|
|
|
@@ -1292,5 +1471,6 @@ update_x-ui() {
|
|
|
}
|
|
|
|
|
|
echo -e "${green}Running...${plain}"
|
|
|
+require_writable_update_paths
|
|
|
install_base
|
|
|
update_x-ui $1
|