Files
3x-ui/x-ui.service.arch
T
MHSanaei 1f2738eda5 fix(systemd): let the WireGuard kernel TUN write rp_filter under the sandbox
ProtectKernelTunables=yes (#6718) mounts all of /proc/sys read-only, and a
WireGuard outbound in kernel-TUN mode zeroes net.ipv4.conf.all.rp_filter (and
its ipv6 and per-device siblings) while Xray creates its handlers, so the core
aborted with "failed to disable ipv4 rp_filter for all: ... read-only file
system". The unit is reinstalled on every update, so a hand edit did not stick.

Keep ProtectKernelTunables and reopen only /proc/sys/net. Turning the
protection off (#6797) also lets the core start, but leaves kernel.core_pattern
writable to the root panel, which turns any panel-level code execution into a
way out of the whole sandbox. net.* is per network namespace and grants nothing
beyond the CAP_NET_ADMIN the panel already holds.

Verified on systemd 257 (Debian trixie) with the shipped unit and the pinned
core: the v3.10.0 unit reproduces the error, the new one starts the kernel TUN,
and kernel.core_pattern stays read-only for the service.

Closes #6796
2026-10-12 17:52:21 +02:00

100 lines
5.0 KiB
Plaintext

[Unit]
Description=x-ui Service
After=network.target
Wants=network.target
StartLimitIntervalSec=180
StartLimitBurst=10
[Service]
EnvironmentFile=-/etc/conf.d/x-ui
Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple
WorkingDirectory=/usr/local/x-ui/
ExecStart=/usr/local/x-ui/x-ui
ExecReload=/bin/kill -USR1 $MAINPID
Restart=on-failure
RestartSec=5s
# The panel intentionally stays root: it supervises the Xray child processes,
# edits netfilter state and reads TLS private keys. These settings only bound
# what a panel-level flaw can reach.
#
# PrivateTmp=yes is deliberately absent: the web updater writes its script into
# /tmp and hands the absolute path to a "systemd-run" transient unit, which
# does not share this service's private /tmp (the download would vanish).
NoNewPrivileges=yes
ProtectSystem=full
# Default stores: DB/update status/node-token in XUI_DB_FOLDER (/etc/x-ui),
# xray binary/config/geo in XUI_BIN_FOLDER under /usr/local/x-ui, logs in
# XUI_LOG_FOLDER (/var/log/x-ui). These are the plain-install defaults and the
# floor, not the whole list: install.sh and update.sh regenerate a drop-in
# (x-ui.service.d/10-xui-sandbox.conf) with the folders resolved from the XUI_*
# variables, so a relocated XUI_DB_FOLDER or XUI_LOG_FOLDER stays writable and
# the list survives an update instead of being reset to these defaults. Changing
# one of those variables in the env file is not enough by itself: the drop-in has
# to be refreshed as well, i.e. install or update the panel again.
# Add local extras in your own drop-in (e.g. 20-local.conf).
# The leading '-' keeps the unit startable if a path does not exist yet.
# What ProtectSystem=full covers, precisely: /usr, /boot, /efi and /etc become
# read-only, everything else stays writable. So this list matters for stores
# under those trees -- the default main folder under /usr/local is one.
#
# The in-panel updater is expected to leave this sandbox: it runs update.sh
# through a transient systemd-run unit, which does not inherit these settings.
# Its plain-child fallback (taken when systemd-run is unavailable) cannot work
# here -- update.sh stages the release archive beside the main folder, replaces
# /usr/bin/x-ui and calls the package manager -- so it stops with one clear
# message instead of failing halfway, and the sandbox deliberately does not
# grant /usr or /etc to accommodate it.
# ReadWritePaths= requires systemd >= 231; ReadWriteDirectories= is the deprecated
# alias for older systemd, needed so ProtectSystem=full keeps /etc/x-ui writable.
ReadWritePaths=-/etc/x-ui -/usr/local/x-ui -/var/log/x-ui
ReadWriteDirectories=-/etc/x-ui -/usr/local/x-ui -/var/log/x-ui
# A kernel-TUN WireGuard outbound zeroes net.ipv{4,6}.conf.*.rp_filter as Xray starts
# (#6796). Reopen only /proc/sys/net: kernel.* (core_pattern among them) stays read-only.
# No ReadWriteDirectories alias needed: ProtectKernelTunables itself requires systemd 232.
ReadWritePaths=-/proc/sys/net
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectClock=yes
ProtectHostname=yes
# read-only rather than yes: installs keep TLS certs under /root/cert, and the
# panel must still be able to read them.
ProtectHome=read-only
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RestrictNamespaces=yes
UMask=0077
# AF_NETLINK for interface/route lookups and the ip(8) child used by the
# AmneziaWG IPv6-alias feature.
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
# NET_ADMIN for ip(8)/tproxy, NET_BIND_SERVICE for ports below 1024, NET_RAW
# for raw sockets and SO_BINDTODEVICE.
#
# DAC_OVERRIDE has to stay. The panel runs as root, but a bounding set is
# subtracted from root's own privileges too: without it root can only read a
# file when the owner/group/other bits let uid 0 through, and any TLS private
# key belonging to another account becomes unreadable -- a certificate issued to
# Caddy, an acme.sh home, any 0600 file that root does not own. TLS then fails
# quietly: the panel listener catches the tls.LoadX509KeyPair error, logs it and
# keeps serving plain HTTP, and every Xray inbound using that key stops. Those
# reads worked before the sandbox because the panel is root.
# DAC_READ_SEARCH is deliberately absent: directory search is already covered by
# DAC_OVERRIDE, so it would only widen the set without adding anything.
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
SystemCallArchitectures=native
# No seccomp whitelist here on purpose. @system-service needs systemd >= 239
# (other @-named groups exist since 231), and older systemd does not ignore an
# unknown group name gracefully: on
# <231 the name fails to resolve and the filter stays the built-in whitelist of
# execve/exit/exit_group/rt_sigreturn/sigreturn, on 231..238 it degrades to
# @default -- either way the panel then gets EPERM on read/openat/mmap/clone and
# cannot start. install.sh and update.sh add SystemCallFilter=@system-service and
# SystemCallErrorNumber=EPERM to the generated drop-in, but only when
# "systemctl --version" reports 239 or newer.
[Install]
WantedBy=multi-user.target