The generated 3x-ipl fail2ban action only matched -p tcp, so UDP-based
inbounds (Hysteria2, TUIC, WireGuard) from a banned IP kept working,
bypassing IP-limit enforcement. Drop the protocol qualifier from the
chain jump and ban both tcp and udp, keeping the SSH/panel port exemption.
Updating geo files printed raw curl progress meters showing 0 bytes when
files were already current (curl -z conditional download), claimed success
unconditionally, and restarted xray even when nothing was downloaded —
confusing enough to be reported as a bug (#5230).
Now each file reports updated / already up to date / download failed,
failures no longer print a success message, and the restart (which drops
live connections) only happens when a file actually changed. Same for the
non-interactive 'x-ui update-all-geofiles' command.
- Issue acme.sh HTTP-01 over IPv4 unless the host has no global IPv4
address: the hardcoded --listen-v6 started a v6-only standalone
listener, so validation of a domain whose A record points at this
host always failed (#4994).
- Add a custom cert/key path option to the "Set Cert paths" menu so
certificates living outside /root/cert (e.g. certbot under
/etc/letsencrypt) can be wired to the panel from the CLI (#5010).
- Derive the displayed Access URL from the certificate's actual SAN
list instead of the cert folder name, list the other covered names,
and show the panel's custom-path certificate in "Show Existing
Domains" (#5070). Also silence find when /root/cert doesn't exist.
sysctl --system re-applies every sysctl file on the host, surfacing
unrelated "Invalid argument" errors from the distro's own defaults
(e.g. Ubuntu 22.04's 50-default.conf on kernels 5.14+). Apply only
/etc/sysctl.d/99-bbr-x-ui.conf on enable, and drop the redundant
re-apply on disable since sysctl -w already restores the live values.
Expose mtg's [domain-fronting] section (ip/port/proxy-protocol) plus
proxy-protocol-listener, prefer-ip, and debug on MTProto inbounds. Each
key is written to the generated mtg-<id>.toml only when set, so mtg's own
defaults apply otherwise. The instance fingerprint now covers these
fields, so editing an option restarts the sidecar.
Since MTProto is mtg-served (not Xray), sniffing does not apply: hide the
Sniffing tab and the Advanced sniffing sub-editor, drop it from the
Advanced "All" JSON view, and emit empty sniffing in the wire payload,
all gated by a new canEnableSniffing predicate.
* fix(logs): render journalctl output in the SysLog viewer
The log viewer's parseLogLine only understood the app-log format
(2006/01/02 15:04:05 LEVEL - body). With SysLog ticked the backend
returns journalctl lines (Mon DD HH:MM:SS host ident[pid]: LEVEL - body),
so the parser mistook the journal time for the level and dropped the
body, leaving only timestamps. Detect and strip the journald prefix,
keep the journal timestamp as the stamp, then parse the real level and
body from the remainder.
* feat(mtproto): surface mtg output and add status reporting
mtg's stdout/stderr was captured by a writer that kept only the last
line and showed it nowhere, so the reason a proxy could not reach
Telegram was invisible. Stream mtg output line-by-line into the x-ui
log, tagged per inbound, so it appears in the panel log viewer and
journald.
Also fix mangled log lines: logger.Info uses fmt.Sprint, which drops
the space between adjacent string operands, producing output like
'inbound3on0.0.0.0:8443'. Switch the affected mtproto calls to the
formatted (*f) variants.
Add show_mtproto_status to x-ui.sh so 'x-ui status' reports each
mtproto inbound's mtg process state and bind address.
* fix(logs): parse all journalctl message shapes in SysLog viewer
Real journalctl output mixes four message shapes after the
'Mon DD HH:MM:SS host ident[pid]:' prefix: go-logging 'LEVEL - msg'
(x-ui/xray), Go std-log with an embedded date (net/http, runtime),
telego's '[timestamp] LEVEL msg', and systemd lines. The viewer only
understood the first, so std-log and telego lines — which never contain
' - ' — collapsed to a bare timestamp (e.g. the 8s telego 409 spam).
Extract the parser into a pure, testable module and teach it the other
shapes: strip the redundant Go std-log date, lift the level out of
telego brackets, and always keep the message body. Add a unit test
covering each shape with real captured lines.
* fix(mtproto): reap orphaned mtg sidecars so a stale one can't break new clients
On Linux x-ui does not kill its mtg children when it dies (no kill-on-exit,
unlike the Windows job object). After a crash, OOM, kill -9, or update, a
stale mtg keeps holding the inbound port with an OLD secret, so new clients
fail the FakeTLS handshake and get silently domain-fronted to the fakeTLS
domain instead of proxied to Telegram (a few MB of traffic, never connects).
Sweep orphans at startup: on the first reconcile, before x-ui starts any of
its own mtg, scan /proc and SIGKILL any process whose executable is our
mtg-<goos>-<goarch> binary. x-ui is the sole owner of mtg, so anything alive
then is an orphan. Runs once per process (swept guard), survives the
binary-deleted-during-update case via /proc/<pid>/cmdline, and is a no-op on
Windows (job object) and other platforms.
Also clear stray mtg in update.sh/install.sh after stopping x-ui, anchored to
the 'mtg-linux-<arch> run ' invocation so the pattern can't match unrelated
command lines (e.g. x-ui.sh's own 'grep mtg-linux').
* fix(logs): drop dead body initializer flagged by eslint no-useless-assignment
* fix(mtproto): drop remark fragment from tg://proxy export link
The mtproto export link appended the inbound remark as a URL fragment
(tg://proxy?server=...&port=...&secret=...#remark). Telegram Desktop
rejects a proxy deep link with a trailing fragment as 'This proxy link
is invalid', breaking one-click import, and a remark is meaningless for
proxy links across clients. Stop adding it in both the panel link
(genMtprotoLink) and the subscription service. Fixes#5105.
* fix(x-ui.sh): remove unused check_mtproto_status helper
show_mtproto_status does its own process check, so check_mtproto_status
was dead code. Drop it (per Copilot review on #5107).
The SSL Certificate Management "Revoke" option only called acme.sh --revoke,
leaving local files under /root/cert/<domain>, acme.sh renewal state under
~/.acme.sh/<domain>, and the panel cert paths in the DB. Renamed to
"Revoke & Remove": it now also drops acme.sh tracking, deletes the local cert
directory and acme.sh state dirs (RSA + ECC), resolves the real IP address(es)
for IP certs so their renewal state is torn down too, and clears the panel cert
paths (x-ui cert -reset) + restarts when the deleted domain was in use.
* feat(x-ui.sh): add migrateDB command and menu for SQLite .db <-> .dump
Adds an "x-ui migrateDB <file>" subcommand and a PostgreSQL-menu option (9)
that convert between a SQLite .db and a portable .dump file. Direction is
auto-detected from the extension and delegated to the bundled binary
(x-ui migrate-db --dump/--restore), so no external sqlite3 client is needed.
Depends on the matching binary support, so it is only usable from the next
panel release.
* fix(x-ui.sh): address review feedback on migrateDB
Per Copilot review on PR #4910:
- Probe the bundled binary for migrate-db --dump support and fail with a clear
upgrade message instead of a raw "flag not defined" error on old builds.
- Prompt before overwriting an existing .dump in dump mode (parity with restore).
- Refuse to restore into the live database path while x-ui is running, to avoid
corrupting the running panel.
- Fix the usage/synopsis strings to show input is optional ([file] not <file>).
- Cleanup on issuance/install failure now also removes the acme.sh
${domain}_ecc (and ${ip}_ecc) directory, not just ${domain}, so a
failed run no longer leaves partial state behind.
- The 'existing certificate' check only reuses a cert when its
fullchain.cer and key files are actually present and non-empty;
otherwise the broken state is removed and issuance proceeds. This
fixes the 0-byte fullchain.pem produced by reusing failed state.
- Menu option 5 (set cert paths) now registers the acme.sh --installcert
hook with --reloadcmd 'x-ui restart' when acme.sh knows the domain, so
auto-renewal copies the renewed cert and reloads the panel.
The 3x-ipl action used iptables-allports, so a banned IP lost all TCP
access including SSH and the panel, locking admins out (especially with
dynamic-IP clients). The ban now blocks every TCP port except the SSH
and panel ports via a multiport negation, derived at jail-creation time
in both x-ui.sh and DockerEntrypoint.sh. This keeps IP-limit working for
all current and future inbounds without per-port config.
Binary: extend the migrate-db subcommand with --dump and --restore so a
SQLite database can be exported to a portable SQL text dump and rebuilt from
one, alongside the existing --dsn PostgreSQL copy. Implemented in Go via the
bundled sqlite driver (new database/dump_sqlite.go); no external sqlite3 client
is required. Add ExportPostgresToSQLite (reverse of MigrateData) to build a
SQLite .db from live PostgreSQL data, reusing the shared copyAllModels helper.
Overview: add a "Download Migration" item to Backup & Restore plus a
getMigration endpoint/service that returns a .dump on SQLite or a .db on
PostgreSQL, so the data can seed a panel on the other backend. Document the
endpoint in api-docs and translate the three new strings across all locales.
Tests: cover the destination-side copy (AutoMigrate + copyTable into SQLite)
and the dump/restore round-trip including quoted values. Ignore *.dump.
The x-ui.sh helper that drives this from the CLI is in PR #4910.
check_status() only recognized a systemd service or Alpine's
/etc/init.d/x-ui, neither of which exists in a container where the panel
runs as the foreground main process (PID 1 via "exec /app/x-ui"). Every
CLI command therefore failed with "Please install the panel first", and
restart/restart-xray relied on rc-service/systemctl that aren't present.
Detect the container (/.dockerenv or XUI_IN_DOCKER) and, when inside one:
- resolve the panel binary under /app instead of /usr/local/x-ui
- derive status from the running process instead of a service file
- restart via SIGHUP and restart-xray via SIGUSR1 to the panel process
- show Docker-appropriate guidance for start/stop/enable/disable
The Dockerfile sets XUI_IN_DOCKER/XUI_MAIN_FOLDER so detection is
explicit even though /.dockerenv alone suffices.
Closes#4817
Menu 20 only exported CF_Key/CF_Email, so a restricted Cloudflare API Token was misread as a Global Key and acme.sh failed with 'invalid domain'. Add a token-or-global-key prompt (default token): an API Token exports CF_Token, the Global Key keeps the previous CF_Key + CF_Email behavior. Also stop echoing the key/token value to the debug log.
Go's flag package parses '-resetTwoFactor false' as '-resetTwoFactor=true' with a dangling positional 'false', so two-factor auth was always wiped on username/password reset regardless of the prompt answer. Omit the flag in the preserve branch (default is false) and use '-resetTwoFactor=true' in the disable branch.
Add a self-contained 'PostgreSQL Management' submenu (main-menu option 27) so the panel can be set up and migrated without re-running the remote install script:
- Install PostgreSQL locally (server + client tools + dedicated xui user/db), ported from install.sh so x-ui.sh stays standalone
- Migrate SQLite to PostgreSQL via 'x-ui migrate-db', then write XUI_DB_TYPE/XUI_DB_DSN to the service env file and restart the panel; client tools are ensured first so in-panel backup/restore works for local and external databases
- Service control: status (clusters + port 5432), start, stop, restart, enable autostart, view log, with auto-detected cluster version
The IP certificate flow auto-set the panel cert path silently, unlike the
domain and Cloudflare flows which ask first. Add the same
"Would you like to set this certificate for the panel? (y/n)" prompt so
the IP flow is consistent and only configures the panel on confirmation.
* feat(install): prompt for SQLite vs PostgreSQL during install
* fix(install): write env file to per-distro path and handle pg-install failure
The env file was hardcoded to /etc/default/x-ui, but RHEL/Fedora units read
/etc/sysconfig/x-ui, Arch reads /etc/conf.d/x-ui, and Alpine OpenRC auto-
sources /etc/conf.d/x-ui. PostgreSQL selection was silently dropped on every
distro except Debian. Also initdb on openSUSE (service wouldn't start) and
prompt the operator on local-install failure instead of silently demoting
to SQLite.
* fix(scripts): make x-ui.sh and update.sh PostgreSQL-aware
update.sh ran setting -show and migrate without sourcing the env file, so
PostgreSQL users had migrations applied to the SQLite default and settings
introspection read the wrong DB. Sourcing the per-distro env file at the
start of update_x-ui exports XUI_DB_TYPE/XUI_DB_DSN to all binary calls.
x-ui.sh now shows the active backend in View Current Settings (password
masked) and removes the env file on uninstall so a later reinstall doesn't
inherit a stale DSN.
`14. Restart Xray` failed on Alpine with `systemctl: command not found` —
restart_xray was the only service action missing an Alpine branch. While
fixing it, the OpenRC reload action was passing the pidfile path to `kill`
instead of the PID inside it, so `rc-service x-ui reload` would have
failed too.
Fail2ban parses % as variable interpolation in action.d configs, so the
unescaped %Y/%m/%d %H:%M:%S in the date command crashed fail2ban on
startup. Double the %s in the heredoc so the rendered action file
contains %% and fail2ban collapses it back to a literal % when invoking
the shell command.
Try six IPv4 providers in turn, accept only HTTP 200 + IPv4-shaped body,
and prompt the user to enter their IP if every provider fails.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- DockerEntrypoint.sh: create jail.d/filter.d/action.d config files
before starting fail2ban so Docker containers no longer start with
0 active jails (fixes#4134)
- x-ui.sh create_iplimit_jails: lower maxretry from 2 to 1 so
fail2ban bans on the first log entry; with maxretry=2 and the
partitionLiveIps logic the second occurrence could arrive after the
32 s findtime window, silently preventing any ban (fixes#4163)
- x-ui.sh: fix datepattern (%%Y -> %Y) so fail2ban parses the Go
log timestamp correctly instead of looking for a literal %%Y string
- x-ui.sh / DockerEntrypoint.sh: fix date command in actionban /
actionunban echo (%%Y -> %Y) so the ban log records actual dates
- check_client_ip_job.go: replace log.SetOutput / log.SetFlags on
the global standard-library logger with a local log.New instance,
eliminating the dangling closed-file-handle between calls and
stopping unrelated stdlib log output from polluting 3xipl.log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On fresh Debian 12+, Ubuntu 24+ and recent RHEL-family minimal images
the fail2ban package ships with `banaction = nftables-multiport` as
the default in /etc/fail2ban/jail.conf but does not pull in the
`nftables` package as a dependency. The first SSH brute-force attempt
hits the default sshd jail and fail2ban logs
stderr: /bin/sh: 1: nft: not found
returned 127 -- HINT on 127: "Command not found"
repeatedly, which users mistake for a 3x-ui regression (see the
discussion on #4083). The 3x-ipl jail itself is unaffected — it uses
an iptables-based action configured in create_iplimit_jails — so this
is only stray noise, but noisy enough to look like a real failure on
first install.
Add `nftables` to the package list in every branch of install_iplimit
so new installs end up with a working default sshd jail out of the
box. Existing installs where `nftables` is already present are a
no-op.
it should be 'default' runlevel when add x-ui service to openrc, default is 'sysinit' runlevel. 'sysinit' runlevel is unnecessary,maybe.
if not, there is an error when call to function 'check_enabled()' as command 'grep default -c' can`t print 'default' runlevel.
check_enabled() {
if [[ $release == "alpine" ]]; then
if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then
return 0
else
return 1
fi
Replace simple curl+trim checks with a response+http_code parse to ensure the remote URL returns HTTP 200 and a non-empty body before assigning server_ip. Changes applied to install.sh, update.sh and x-ui.sh: use curl -w to append the status code, extract http_code and ip_result, and only set server_ip when http_code == 200 and ip_result is non-empty. This makes the IP discovery more robust against error pages or partial responses while keeping the existing timeout behavior.
Introduces user prompts to select the port for ACME HTTP-01 certificate validation (default 80), checks if the chosen port is available, and provides guidance for port forwarding. Adds is_port_in_use helper to all scripts and improves messaging for certificate issuance and error handling.
The is_domain function was being called in ssl_cert_issue() but was never
defined in x-ui.sh, causing 'Invalid domain format' errors for valid domains.
Added is_ipv4, is_ipv6, is_ip, and is_domain helper functions to match
the definitions in install.sh and update.sh.
Co-authored-by: wyatt <wyatt@Wyatts-MacBook-Air.local>