* feat(utils): add speedFormat utility and tests
* feat(inbounds): add InboundSpeedEntry type
* feat(inbounds): add speed column to inbound list
* feat(inbounds): show speed in inbound stats modal
* feat(inbounds): compute inbound speed from traffic deltas
* feat(inbounds): wire inbound speed through page
* feat(i18n): add speed translation for all locales
* refactor(inbounds): dedupe live-speed UI and harden formatting
Extract a shared InboundSpeedTag component and isActiveSpeed guard used by the speed column and stats modal, unify InboundSpeedEntry into a single type, and route speedFormat through sizeFormat.
Also guard sizeFormat against non-finite input (no more "NaN PB/s") and clear stale per-inbound speeds when a traffic poll returns no deltas.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
The first-boot smoke test and install.sh fetched the released binary with
a single curl attempt, so a transient GitHub/CDN 504 failed the whole job.
- smoke-firstboot.sh: add --retry/--retry-all-errors with connect/max
timeouts to the version API and tarball downloads, split the download
into a guarded step, and assert the tarball is non-empty.
- install.sh: add --retry plus connect/max timeouts to the release-binary
downloads and version lookups. Omit --retry-all-errors here for curl
< 7.71 (Ubuntu 20.04 / Debian 10 / CentOS 7) compatibility; plain
--retry already covers 504 and other transient errors.
* feat(docker): support XUI_PORT runtime override
Allow deployments to select the panel listener port without mutating the persisted webPort setting. Invalid values fall back to the database-backed port and are covered by parser boundary tests.
* docs: describe XUI_PORT deployment usage
Add commented local and Compose examples, explain runtime precedence, and call out matching Docker bridge port mappings.
Add a top-level `permissions: contents: read` block so the smoke-test
workflow no longer inherits the repository default token permissions.
Resolves CodeQL actions/missing-workflow-permissions.
Add a Links tab to the client form for attaching third-party share
links and remote subscription URLs per client. They are merged into
the client's raw/JSON/Clash subscription output: links are emitted
verbatim and parsed for JSON/Clash; subscription URLs are fetched
(cached, with a short timeout) and their configs merged in.
i18n keys added across all 13 locales.
* feat(install): add non-interactive install path for cloud/golden-image use
Trigger non-interactive mode when XUI_NONINTERACTIVE=1 or stdin is not a
TTY (curl | bash, cloud-init). Every prompt is then replaced by an env var
or a sane default; interactive prompts stay byte-for-byte identical.
Honored env vars: XUI_USERNAME, XUI_PASSWORD, XUI_PANEL_PORT,
XUI_WEB_BASE_PATH (unset => random, as before), XUI_SSL_MODE=none|ip|domain
(default none), XUI_DOMAIN, XUI_ACME_EMAIL, XUI_DB_TYPE/XUI_DB_DSN, plus
additive XUI_ACME_HTTP_PORT, XUI_SSL_IPV6, XUI_SERVER_IP.
On success, write /etc/x-ui/install-result.env (mode 600) with the panel
creds + access URL + api token, in both interactive and non-interactive
modes, so cloud-init/MOTD can surface them. Postgres in non-interactive
mode requires XUI_DB_DSN or installs locally; never silently downgrades.
* feat(deploy): add first-boot per-instance credential generation
Golden images ship with no x-ui.db. x-ui-firstboot.sh runs once (guarded by
/etc/x-ui/.firstboot-done), before x-ui.service, and replaces the seeded
admin/admin with fresh random username/password on a random high port,
regenerates the session secret/panel GUID via 'x-ui setting -reset', mints an
API token, and writes the creds to /etc/x-ui/credentials.txt (600) + /etc/motd.
Idempotent: skips regeneration if a non-default admin already exists. The
oneshot unit is ordered After=network-online/cloud-init and Before=x-ui.service
so the panel never serves default credentials.
* chore(deploy): force LF for cloud-image deploy assets (.service/.hcl/.yaml)
* feat(deploy): add Packer config + provisioning scripts for golden image
One build, two sources: amazon-ebs (AWS AMI, Canonical Ubuntu 24.04 base via
source_ami_filter) and qemu (qcow2 + raw, NoCloud-seeded for build-time SSH).
Provisioner order is fixed: provision.sh -> harden.sh -> cleanup.sh.
- provision.sh: downloads the released x-ui tarball (no Go build), installs the
panel + firstboot unit, enables but does NOT start services, creates NO DB.
- harden.sh: key-only SSH, no root password login, locks default account
passwords, enables unattended-upgrades (scanner-compliant).
- cleanup.sh: wipes any DB/creds, SSH host keys, authorized_keys, machine-id,
cloud-init state, logs and history; fails the build if any secret survives.
packer fmt -check clean; packer validate passes for both sources.
* feat(deploy): add generic cloud-init user-data for unattended install
cloud-init.yaml installs the latest 3x-ui non-interactively (XUI_NONINTERACTIVE=1)
on any cloud-init platform, generating unique per-instance credentials and
surfacing them via /etc/x-ui/install-result.env, serial console and MOTD.
README documents per-provider usage (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
and all XUI_* knobs.
* ci: add image.yml to build cloud images on release
On release: published (or workflow_dispatch with a tag), waits for the
x-ui-linux-amd64.tar.gz asset (handles the release-matrix upload race), then:
- qemu-image (always): builds the qcow2 with Packer and attaches a compressed
.qcow2.xz + sha256 to the GitHub release. Uses KVM when /dev/kvm exists,
else TCG.
- ami-image (gated): builds the AWS AMI only when AWS creds exist (OIDC role
preferred, else access keys), so forks skip cleanly. Prints the AMI ID to the
job summary. No secrets or AMI IDs are committed.
* test(deploy): add container smoke tests for install + firstboot
smoke-noninteractive.sh: runs install.sh piped (no TTY) with
XUI_NONINTERACTIVE=1 in an Ubuntu container; asserts install-result.env (600)
holds random non-default creds, hasDefaultCredential is false, and the panel
serves HTTP.
smoke-firstboot.sh: installs the released binary with no DB, runs
x-ui-firstboot.sh; asserts per-instance creds + credentials.txt (600) + MOTD,
no admin/admin, and that a second run is a no-op (sentinel honored).
smoke.yml runs both as gated jobs on PRs/pushes touching install.sh or deploy/**.
Both pass locally against the v3.3.1 release binary.
* docs(deploy): add Packer/marketplace docs and link from README
- deploy/README.md: index of the cloud-deploy tooling and the two models
- deploy/packer/README.md: how to build locally, variables, first-boot behavior
- deploy/marketplace/aws/README.md: seller registration -> AMI scan ->
limited-visibility preview -> go-public checklist
- deploy/marketplace/hetzner/README.md: cloud-init-first guidance + snapshot
caveat (delete x-ui.db first) + hetznercloud/apps reference
- README.md: link the unattended-install / cloud-image docs from Quick Start
* feat(deploy): build golden images for arm64 as well as amd64
The install path was already multi-arch (install.sh auto-detects arch); this
extends the golden image + CI to arm64:
- packer: xui_arch (amd64|arm64, validated) now derives the base AMI filter and
the Ubuntu cloud image; the qemu source switches to qemu-system-aarch64 + virt
machine + AAVMF UEFI firmware for arm64. amd64 path unchanged.
- image.yml: arch matrix. AMIs for amd64 (t3.small) + arm64 (t4g.small/Graviton)
from one runner; qcow2 for amd64 on a standard runner and arm64 on a native
ubuntu-24.04-arm runner. Waits for both release tarballs.
- smoke.yml: run install + firstboot smoke tests on amd64 and arm64 runners;
smoke-firstboot.sh now resolves the arch tarball via dpkg.
- docs updated for both arches.
packer fmt/validate pass for amd64 and arm64; actionlint + shellcheck clean.
Verified locally: non-interactive install AND firstboot run on the real arm64
release binary under emulation (ELF aarch64, no admin/admin).
* chore(deploy): default AWS region to eu-central-1 (Frankfurt)
Replace the us-east-1 fallback in image.yml (4 sites) and the Packer 'region'
default + doc examples. Still overridable via the AWS_REGION repo variable / the
-var 'region=...' flag.
* feat(deploy): add Amazon Lightsail support (launch script + snapshot builder)
Lightsail can't launch from an EC2 AMI and its blueprint list isn't
self-publishable, so add the two self-service paths instead:
- launch-script.sh: paste into Lightsail 'Add launch script' (or --user-data) to
install 3x-ui non-interactively with unique per-instance credentials.
- snapshot-userdata.sh + build-snapshot.sh: AWS CLI pipeline that provisions a
build instance (panel installed, NO DB, firstboot enabled), runs the shared
cleanup.sh, then snapshots it. Instances launched from the snapshot mint their
own credentials on first boot. Optional --panel-port pins a known port for the
Lightsail firewall.
- README documents both paths, the firewall caveat, and the blueprint reality.
EC2 AMI / Marketplace path kept untouched alongside. All scripts shellcheck-clean.
* fix(deploy): address Copilot PR review findings
- install.sh + firstboot: write install-result.env / credentials.txt values with
printf %q so the files stay safe to source even if creds are pinned with shell
metacharacters (no-op for the alphanumeric random defaults).
- firstboot: fail closed if 'x-ui setting -show' can't be parsed to true/false —
exit without writing the sentinel so the next boot retries, instead of silently
skipping regeneration and risking admin/admin.
- firstboot + cloud-init + lightsail launch-script: keep secrets out of the
world-readable /etc/motd (show URL + username only; full creds via the mode-600
file / serial console).
- lightsail build-snapshot: handle download-default-key-pair returning either a
PEM or base64, and assert a valid PEM before using it for SSH.
- image.yml: pin hashicorp/setup-packer@v3 (was @main).
- deploy/README: document XUI_ACME_HTTP_PORT / XUI_SSL_IPV6 / XUI_SERVER_IP.
Both container smoke tests still pass; shellcheck + actionlint clean.
Test All only iterated the editable template outbounds, so subscription
outbounds (the read-only "from subscriptions" table) were never probed in
bulk. They are now queued too, keyed by tag in subscriptionTestStates so
their rows light up live; the template and subscription HTTP lanes run
serially to respect the backend's single-batch lock (TCP runs alongside).
Also stop testing freedom ("direct") and dns outbounds: they aren't
proxies, so an HTTP probe through them only measures the host's own
reachability, not a tunnel. They are now untestable in every mode -- the
per-row button is disabled and Test All skips them -- with a matching
backend guard so a direct API caller can't HTTP-test them either.
The 'o' remark block is sourced from an external proxy's remark, but the
label 'Other' gave no hint where to set it. Rename the display label to
'External Proxy' to match the inbound form section; the stored 'o' key is
unchanged so existing remarkModel values stay compatible.
The node probe honored the per-node TlsVerifyMode (skip/pin) but
runtime.Remote used a shared client with no TLSClientConfig, so traffic
sync and every other remote op fell back to system-CA verification and
failed against self-signed nodes even after the operator set skip/pin.
Move the TLS client builder into the runtime layer (HTTPClientForNode /
DecodeCertPin) as the single source of truth, have Remote build and cache
its per-node client through it, and delegate the service probe to the same
builder so the two paths can no longer diverge.
v3.3.1 removed the Panel Proxy URL field from the UI but left the stored
panelProxy/tgBotProxy values in the DB. The Telegram bot still reads
tgBotProxy directly, so a stale value masked the panelOutbound egress
fallback. Add a one-off seeder to drop both rows.
Closes#5266
The paged client list is sorted/paginated server-side but fetched with staleTime: Infinity, so the WS client_stats patch only refreshed traffic on already-visible rows — newly connected clients never appeared and the sort order went stale until a manual refresh.
Add a 5s refetchInterval so the current page tracks reality, and drive the table overlay off isPlaceholderData so the background poll does not flash it.
The .online-dot uses vertical-align: middle, which in inline layout
aligns to baseline + half x-height — visibly off-centre inside the Ant
Tag's line box. Add a .dot-tag utility (inline-flex, align-items:
center) and apply it to the Online tag so the dot and label share one
centred axis. Other dot usages (Nodes page Space, card heads, stat
rows) already sit in flex containers and are unaffected.
The #5035 change tagged node-hosted entries with the node name to
disambiguate multi-node subscriptions, but the node name is
panel-internal and leaked into the profile names end users see in
their client apps. Drop the suffix entirely — remarks are the
admin-set inbound remark again.
The vlessenc fix (#5185) enabled flow on XHTTP only in the security=none
branch of genVlessLink, and the Clash builder still gated flow on
network==tcp. With XHTTP+REALITY+vlessenc the panel accepts and stores
the flow (inboundCanEnableTlsFlow passes), but subscriptions dropped it,
so clients received configs without xtls-rprx-vision.
Add vlessFlowAllowed mirroring inboundCanEnableTlsFlow — tcp with
tls/reality, or xhttp with vlessenc regardless of security layer — and
use it in both the vless:// link generator and the Clash proxy builder.
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.
Revert formatInboundLabel to the pre-#5151 behavior: display the inbound
remark when set, otherwise the inbound tag, instead of "tag (remark)".
Affects the Attach clients / Attached inbounds views and client lists.
Routing keeps its own tag (remark) formatting.
Bump several Go indirect dependencies (golang.org/x/exp, golang.org/x/tools, google.golang.org/genproto) and update go.sum accordingly. Regenerate frontend/package-lock.json to record updated npm package versions (including Ant Design and related rc-component packages and other transitive updates).
- Split the Happ and Clash/Mihomo routing sections out of Information into
their own dedicated tabs.
- Extract the profile/branding fields (title, support URL, profile page,
announcement, theme dir) out of the mislabeled "Subscription Title"
divider into a new Profile tab.
- Move the Update Interval setting into Information and drop the
single-field Intervals tab.
- Add the "profile" tab label across all locales.
Client add/update/remove also rewrite settings.clients on each attached
inbound, so the Xray config query could go stale. Invalidate it alongside
the clients and inbounds buckets.
- Replace the Telegram "Notification Time" free-text field with a guided
cron builder: @every + number + unit (s/m/h), the @hourly/@daily/@weekly/
@monthly macros, and a Custom option that seeds a valid 6-field crontab
(cron runs with seconds enabled) as an escape hatch.
- Move "Restart Xray After Auto Disable" from the External Traffic tab to
Panel Settings, where it belongs.
- Add a "Template guide" link to the Sub Theme Directory setting pointing at
docs/custom-subscription-templates.md.
- Localize all new strings across every locale.
Bump row action icons to 18px across the clients, inbounds, groups and
nodes tables for better visibility.
In the clients table, cap the Client column at 220px and give Duration a
fixed width so the Traffic column becomes the flexible one that absorbs
the remaining horizontal space instead of Client growing oversized.
Add a per-inbound "Route through Xray" toggle (off by default) plus an
optional outbound picker on MTProto inbounds. mtg only supports a SOCKS5
upstream, so when enabled the panel injects a loopback SOCKS bridge into
the generated Xray config — tagged with the inbound's own tag — and mtg
dials Telegram through it via a [network] proxies upstream. The router
then governs Telegram egress: matchable in the Routing tab, or forced to a
chosen outbound/balancer via the picker.
- mtproto: Instance carries RouteThroughXray + XrayRoutePort (in the
fingerprint); InstanceFromInbound parses them; renderConfig emits the
socks5 [network] upstream; freeLocalPort exported as FreeLocalPort.
- xray.go: injectMtprotoEgress appends the loopback SOCKS bridge and
prepends an optional inboundTag->outbound/balancer rule, hot-appliable
like injectPanelEgress.
- inbound.go: backend-owned egress port persisted in settings, allocated
once and carried across edits (stored value wins); stripped with the
inert outboundTag when routing is off; allocation failure fails the save;
routed add/update/del force a config regen.
- mtproto_job: skip folding mtg metrics for routed inbounds (the bridge,
carrying the inbound tag, is metered by xray_traffic_job) to avoid
double-counting.
- frontend: toggle + outbound/balancer Select (useOutboundTags) on the
MTProto form; i18n keys for all locales.
Replace the per-outbound burstObservatory polling (one temp xray spawn +
up to 15s of /debug/vars polling per outbound, serialised) with one
shared temp xray instance per batch: every tested outbound gets its own
loopback SOCKS inbound plus an inboundTag->outboundTag routing rule, and
the panel times a real HTTP request through each one in parallel. The
probe returns as soon as the response lands and records the HTTP status
plus an httptrace breakdown (proxy connect / TLS via outbound / first
byte) shown in the result popover.
New POST /panel/api/xray/testOutbounds endpoint (array in, results in
input order, max 50); the legacy /testOutbound endpoint now delegates to
the same engine. Test All chunks HTTP probes 16 per request, and a batch
whose shared process never comes up (one structurally-broken outbound
poisons the config) retries each item in an isolated instance so the
broken outbound reports xray's real error while the rest still test.
Split the group traffic summary into two inbound-style cards: a "Total
upload / download" card with up/down arrow icons and a "Total Usage" card
with the pie icon. Add the totalUpDown label across all locales.
Switching the transport to mKCP auto-seeds a mkcp-legacy entry into
finalmask.udp, but switching back to another transport only dropped the
kcpSettings blob and left the mask behind. It survived downstream pruning
(finalmask.udp was non-empty) and bled into every client share link.
Strip auto-seeded mkcp-legacy entries from finalmask.udp whenever the
network changes away from kcp, leaving user-authored masks intact.
Fixes#5221
Add per-group up/down to GroupSummary (backend + schema), surface them
as Upload/Download columns in the groups table, and fold upload/download
into the Total traffic summary card. Rename the group "Clients in group"
column to just "Clients" across all locales.
Accept the @-prefixed abstract socket form (e.g. @xray/in.sock) for an
inbound listen address, not just path-based sockets. The form now allows
Port 0 for both, and the Address help text documents the @ form across
all locales. The backend already treated both prefixes as unix sockets.
@
An authenticated admin could set xrayTemplateConfig.log.access/error to an
arbitrary path (via the raw Xray editor or a wholesale DB import), making the
supervised Xray process write its log there — an arbitrary file write as the
Xray user (root in many deployments). resolveXrayLogPaths now reduces any log
path to its base filename under config.GetLogFolder(), so absolute paths and
".." traversal can no longer escape the log folder; "" and "none" still
disable logging.
Importing a second inbound whose clients overlap an already-imported inbound
failed with "UNIQUE constraint failed: client_traffics.email". The import path
carries exported ClientStats, and tx.Save(inbound) cascaded that has-many
association as INSERTs whose ON CONFLICT targets only the primary key, so a
shared email (already owning a row from the first import) tripped the global
unique constraint.
Omit the ClientStats association on save and insert the carried stats ourselves
with the same OnConflict{email, DoNothing} guard AddClientStat already uses:
new clients keep their imported counters, shared emails reuse the existing row.
Then run an idempotent AddClientStat pass over all clients so any client present
in settings but missing from the stats payload still gets a traffic row (else it
would escape quota/expiry accounting), and propagate insert errors so the tx
rolls back instead of committing a partial state.
* feat: implement inbound XMUX form fields
* fix: replace any cast to satisfy eslint
* test: update xhttp form snapshot for XMUX
* fix(inbound): persist xmux on save so the XMUX form actually round-trips
The inbound wire normalizer unconditionally deleted xhttpSettings.xmux,
so the new inbound XMUX form was stripped on save and never reached the
stored config — the subscription extra blob (buildXhttpExtra) could
never see it. Gate the deletion on the enableXmux toggle, mirroring the
outbound adapter, and add regression tests for both on/off cases.
* fix(xmux): enforce xray-core's maxConnections/maxConcurrency exclusivity
xray-core's XmuxConfig rejects a config that sets both maxConnections
and maxConcurrency. The panel pre-fills maxConcurrency ('16-32') whenever
XMUX is enabled, so an explicit maxConnections would always collide and
make xray refuse the config. Mirror core's semantics in the wire
normalizer: when maxConnections is set (>0, an explicit opt-in since it
defaults to 0), drop the leftover default maxConcurrency. Applies to both
inbound and outbound xhttp.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* Update ExecReload command in x-ui.service.debian
* fix(systemd): use absolute path in ExecReload for arch and rhel units
systemd requires absolute executable paths; kill -USR1 $MAINPID fails
with 'Executable path is not absolute'. Matches the debian unit fix.
---------
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
Add a subSortIndex field to inbounds that controls the order of links
in subscription output only: the raw sub body, the HTML sub page, and
the JSON/Clash formats (all served from the same query). Lower values
come first; ties keep id order. The panel inbound list is unaffected.
The value is editable in the inbound form next to the share-address
fields, propagates to nodes via wireInbound, and follows the usual
node-sync rules (copied on import, mirrored while not dirty, never a
structural change).
Rescoped from #5214 by @Ponywka.
- Rename tabs: "Basic" → "Basics", "Config" → "Credentials"
- Move reverseTag field from Credentials tab to Basics tab
- Move IP log button inline with limitIp field (tooltip button, edit mode only)
- Hide random email button when editing an existing client
- Add tooltips to totalGB and limitIp fields with descriptive hints
- Rename labels: "Total Sent/Received (GB)" → "Traffic Limit (GB)", "Duration" → "Duration (days)"
- Add renewDays translation key for auto-renew label with unit hint
- Remove redundant filterOption and width style from AutoComplete group selectors
- Update all 15 locale files with new and renamed translation keys
Multi-node panels had no way to narrow the inbounds or clients lists to
a single node. Add a node filter to both pages:
- Inbounds: a toolbar select (All / Local / each node) that filters the
list client-side; shown only when the panel has nodes or node-attached
inbounds.
- Clients: a Nodes multi-select in the filter drawer. Node selections
are mapped onto inbound IDs client-side and fed through the existing
inbound CSV paging parameter, so the paging backend is untouched; an
impossible id (-1) is sent when no inbound matches so the filter
yields an honest empty result. InboundOption now carries nodeId to
make the mapping possible.
The local panel is selectable via a 0 sentinel (inbounds without a
nodeId). New i18n keys in all 13 locales.
WG peers were only identifiable by their keys. Add an optional panel-side
comment per peer: editable in the inbound form (echoed next to "Peer N"
in the section header), stored in the settings JSON alongside the
panel-only privateKey (xray-core ignores unknown peer fields), and
appended to the share link / .conf remark so the device is identifiable
in client apps too.
GET /panel/api/clients/get/:email returned the quota (totalGB) but not
the bytes the client has actually used, forcing API consumers to scrape
it elsewhere. Add a sibling "usedTraffic" field (up+down, including the
cross-node global overlay) next to "client" and "inboundIds".
The fragment "packets" field was a locked dropdown (tlshello / 1-3 / 1-5)
in both the finalmask TCP-mask form and the Freedom outbound form, while
xray-core accepts any "n-m" packet range. Replace both with an
AutoComplete that keeps the presets as suggestions and validates free
input as "tlshello" or a numeric range.
The settings page validates the whole AllSetting object before saving, so a
tgCpu value that isn't an integer in 0-100 (left over from an older or corrupt
setting) failed validation with "tgCpu: Invalid input" and blocked saving every
other setting too. Clamp/round tgCpu to a valid integer in the model
constructor, defaulting to 80 when it isn't a finite number.
NodeFormSchema required inboundTags, but the inboundTags Form.Item is only
mounted when inboundSyncMode is "selected" - antd onFinish omits unmounted
fields, so saving with the default "all" mode failed schema validation with
Zod generic "Invalid input" (regression from #5178; same class as the
earlier pinnedCertSha256 fix).
Also tolerate null inboundTags (Go nil slice) for nodes saved before #5178,
both in the form schema and NodeRecordSchema, and normalize edit-mode values.
The panel seeded xhttp configs with scMaxEachPostBytes=1000000 and
scMinPostsIntervalMs=30 — xray-core''s own defaults — and emitted them
into every generated config and share link. The literal
scMinPostsIntervalMs=30 is a stable DPI fingerprint that Russia''s TSPU
keys on to block connections on mobile networks.
New configs no longer seed these values (empty schema/template defaults,
so xray-core applies its internal defaults). For configs already stored
with the old defaults, the link/subscription builders now drop values
equal to xray-core''s defaults instead of advertising them — covering
panel share links, the raw subscription, and the JSON subscription
without requiring every inbound to be re-saved. Non-default values the
user set deliberately are still emitted.
- 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.
applyClientFieldByEmail patched only the first inbound that the
client_traffics row pointed at. For a multi-inbound client the sibling
inbounds kept the old expiryTime/totalGB/limitIp in their settings JSON,
and the next SyncInbound over a stale sibling reverted the edit in the
normalized records — the Telegram bot's expiry change appeared to apply
and then sprang back. Patch the field on every inbound linked to the
email, falling back to the legacy single-inbound lookup for clients that
were never normalized.
An inbound pushed to nodes keeps the same remark on every copy, so a
multi-node subscription (and the panel's per-client link view) listed
several identically-named entries differing only by address. Append the
node name to the remark of node-hosted inbounds unless the admin already
included it.