Commit graph

1519 commits

Author SHA1 Message Date
biohazardous-man
97f88fb1a9
feat(sub): modern xray JSON format with unified finalmask editor (#4912)
* feat(sub): add finalmask support to JSON subscriptions

* feat(sub): modern xray JSON format with unified finalmask editor

Drop the legacy JSON subscription format entirely and always emit the
modern xray shape:

- Flatten proxy outbounds (no vnext/servers) for vless/vmess/trojan/
  shadowsocks; hysteria was already flat.
- Express fragment/noise via streamSettings.finalmask instead of the
  legacy direct_out freedom dialer + dialerProxy sockopt.

The global finalmask (tcp/udp masks + quicParams) is stored as a single
setting (subJsonFinalMask) and merged into every generated stream,
replacing the separate subJsonFragment/subJsonNoises/subJsonQuicParams
settings.

Reuse the existing FinalMaskForm (used by inbound/outbound) for the
settings UI via a small bridge component; add a showAll prop so all
TCP/UDP/QUIC sections render for the global case. This supersedes the
hand-rolled Fragment/Noises/quicParams tabs with the full mask editor
(all mask types).

Note: this is a breaking change — JSON subscriptions now require a
recent xray client on the consumer side.

* fix

---------

Co-authored-by: biohazardous-man <biohazardous-man@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-06-04 23:51:48 +02:00
Misfit-s
f947fbd6c6
feat(Clash): Add routing rules and enable routing option for Clash subscriptions (#4904)
Some checks are pending
CI / go-test (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
* feat(clash): add routing rules and enable routing option for Clash/Mihomo subscriptions

Allows adding custom YAML blocks and placeholders to Clash exports.

Why: Shifting routing to the client prevents server IP exposure for
DIRECT traffic and reduces unnecessary server bandwidth/CPU usage.

* fix

---------

Co-authored-by: Misfit-s <>
2026-06-04 21:55:51 +02:00
康厚超
73ce11508e
fix(tgbot): ignore commands for other bots (#4894)
Telegram group chats can contain multiple bots. Commands addressed to another bot, such as /status@other_bot, should not be handled by the 3x-ui bot.

Closes #4893
2026-06-04 21:45:44 +02:00
MHSanaei
d3db828b46
perf(clients): scale-audit remaining client/inbound endpoints to 200k
Drive every client/inbound/group endpoint at 100k-200k clients on PostgreSQL and fix the latent issues found in previously-unbenchmarked paths:

- enrichClientStats: chunk the email IN lookup (was an unchunked bind that crashed past 65535 clients without traffic rows, taking down GetInbounds/GetInboundDetail/GetAllInbounds)

- GetOnlineClients: add the missing nil-process guard its siblings already have, so ListPaged no longer panics before xray starts

- GetClientTrafficByEmail: read UUID/subId from the indexed clients table instead of parsing the inbound's full settings JSON (439ms to ~1.5ms, flat in N)

- BulkResetTraffic: replace the per-email serialized loop with one chunked bulk UPDATE in a single transaction

- DelDepleted: delegate to the already-batched BulkDelete instead of deleting each depleted client one by one

Adds a postgres-gated full endpoint sweep plus an A/B benchmark, and SQLite correctness tests for the changed methods.
2026-06-04 21:32:15 +02:00
MHSanaei
d1e733b9e9
perf(clients): chunk IN queries and de-quadratic bulk delete/group/list
Bulk client operations bound their entire working set in a single
WHERE x IN (...) clause, which exceeds PostgreSQL's 65535-parameter limit
(and SQLite's 32766) and gives the planner a pathological query, so they
failed outright on inbounds/selections larger than the limit. Every such
query is now chunked at 400 items:

- BulkDelete / delete-all-clients: six IN queries chunked, and the
  per-row delete tombstone (which swept the whole in-memory map on every
  call, O(N^2)) replaced with a single bulk sweep.
- BulkAdjust: record and inbound-mapping lookups chunked.
- AddToGroup / RemoveFromGroup (bulk add/remove to group): three IN
  queries chunked.
- replaceGroupValue (rename/delete group): inbound-mapping lookup chunked.
- List (all-clients listing): link and traffic lookups chunked.

Measured on PostgreSQL 16: delete-all-clients on a 100k-client inbound
now completes in ~7s (previously crashed at the parameter limit); bulk
add/remove to group ~6s and full client list ~1s at 100k.

sync_scale_postgres_test.go adds skip-gated benchmarks for delete-all,
group add/remove, and list.
2026-06-04 20:35:30 +02:00
MHSanaei
f185d3315c
perf(clients): scale add/delete and bulk client operations
Follow-up to the SyncInbound bulk rewrite, fixing the remaining O(M*N)
and O(M)-round-trip behaviour in the add/delete and bulk paths that made
them time out on large inbounds (worst case minutes), especially on
PostgreSQL.

- compactOrphans: chunk the "email IN (...)" lookup (400/batch) instead
  of binding every email at once. A single huge IN exceeded PostgreSQL's
  65535-parameter limit (and SQLite's) and made the planner pathological,
  so add/delete failed outright past ~100k clients.

- emailsUsedByOtherInbounds: new batched form used by delInboundClients
  (BulkDetach) and bulkDelInboundClients (BulkDelete), replacing a
  per-email global JSON scan (O(M*N)) with one scan, and skipped entirely
  when keepTraffic is set.

- BulkCreate: rewritten to validate/dedup in one pass, then group clients
  by inbound and add them in a single addInboundClient call per inbound
  (one getAllEmailSubIDs, one settings rewrite, one SyncInbound) instead
  of running the full single-create pipeline per client.

- Bulk delete/adjust: batch DelClientStat/DelClientIPs with IN deletes
  and wrap the settings Save + SyncInbound in one transaction, so the
  per-row writes share a single fsync instead of one per row.

Measured on PostgreSQL 16 (one inbound, M=2000 affected clients):
  - create: 8m35s (M=500) -> ~1-5s
  - detach: 52s -> ~4s (flat in N)
  - delete: ~16s -> ~1-4s
  - adjust: ~20s -> ~7-10s
add/delete of a single client on a 200k-client inbound stays in seconds.

sync_scale_postgres_test.go adds skip-gated benchmarks (XUI_DB_TYPE=
postgres) for the single add/delete and the five bulk operations.
2026-06-04 19:41:00 +02:00
MHSanaei
756746dbca
perf(clients): make SyncInbound bulk to fix large-inbound timeouts (#4885)
Every client mutation funnels through SyncInbound, which ran O(n) DB
round-trips per call: one SELECT per client, a Save+UpdateColumn per
client, and a per-row junction INSERT. Toggling a single client on a
large inbound issued thousands of queries and timed out, badly so on
PostgreSQL where each round-trip pays TCP latency.

SyncInbound now:
- loads existing records with a single chunked SELECT ... email IN (...)
  instead of one query per client
- writes only the records that actually changed (skips no-op Saves), so
  toggling/editing one client writes one row, not all of them
- batch-creates new records and batch-inserts the junction rows

Merge and sticky-field semantics are unchanged. Measured on PostgreSQL
16: a single-client toggle on a 50k-client inbound drops from ~8m54s to
~0.9s, and seeding 50k clients from ~2m48s to ~1.6s; 200k clients sync
in seconds.

A skip-gated benchmark (web/service/sync_scale_postgres_test.go, run
with XUI_DB_TYPE=postgres) reproduces and verifies the scaling.
2026-06-04 18:14:25 +02:00
MHSanaei
db86007ab8
fix(multi-node): scope remote client update/delete to one inbound (#4892)
UpdateUser and DeleteUser hit the node's email-based full-client endpoints, which fanned out to every inbound the client had on the node: editing a client wiped flow on the node's other inbounds, and detaching one node inbound deleted the client from all of them.

Make both inbound-scoped, mirroring AddClient. DeleteUser now detaches the resolved remote inbound id; UpdateUser passes an inboundIds scope so the node updates only that inbound.
2026-06-04 16:45:40 +02:00
MHSanaei
a07c7b7f4e
feat(migrate-db): SQLite <-> .dump conversion and Download Migration in Overview
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.
2026-06-04 15:32:22 +02:00
MHSanaei
4813a2fe00
fix(api-token): hash tokens at rest and show plaintext only once
Store API tokens as SHA-256 hashes instead of plaintext and return the token value only in the create response. List no longer exposes the token, and the UI drops the Show/Copy buttons in favor of a one-time reveal modal at creation.

Match hashes the presented bearer token before the constant-time compare, and a migration hashes any pre-existing plaintext rows in place so existing tokens keep authenticating. Docs and translations updated.
2026-06-03 22:57:50 +02:00
MHSanaei
7a72aeda7a
i18n: translate connection-limit strings for all languages
Adds connectionLimits/connIdle/bufferSize/seconds keys to the remaining 11 locales (ar, es, id, ja, pt, ru, tr, uk, vi, zh-CN, zh-TW); en-US and fa-IR shipped with the feature.
2026-06-03 21:59:40 +02:00
MHSanaei
ceef413dc4
feat(xray): add connIdle and bufferSize policy controls
Expose level-0 connection policies in the panel's Basics tab: idle timeout (connIdle) and per-connection buffer size (bufferSize). Empty fields delete the key so Xray falls back to its own defaults. Adds en-US/fa-IR strings and types policy.levels in the Zod schema.
2026-06-03 21:52:37 +02:00
MHSanaei
55d6729955
fix(nodes): Set Cert from Panel uses the node's own web cert for node inbounds
For an inbound deployed to a node, the button read the central panel's webCertFile/webKeyFile and inserted paths that don't exist on the node, crashing the node's Xray on startup.

Add a token-accessible GET /panel/api/server/getWebCertFiles that returns a panel's own web cert/key paths, Remote.GetWebCertFiles to fetch it from a node, and GET /panel/api/nodes/webCert/:id to proxy it. setCertFromPanel now calls the node endpoint for a node-assigned inbound and the local settings otherwise, warning instead of inserting wrong paths on error/empty.

Fixes #4854
2026-06-03 16:41:02 +02:00
MHSanaei
ef8882a5c0
fix(online): scope per-inbound online to inbounds that carried traffic
Multi-inbound clients showed online on every inbound they were attached to. Xray's user-level traffic stat aggregates across all inbounds a client belongs to, so the email signal alone can't say which inbound was used.

Pair it with the inbound-level traffic signal under the same 20s grace and gate the per-inbound rollup on it: a client only shows online on inbounds that actually moved bytes this window. Remote nodes report no per-inbound activity and stay ungated (no regression). Adds GetActiveInboundsByNode, the activeInbounds WS field and POST /panel/api/clients/activeInbounds.

Fixes #4859
2026-06-03 16:19:00 +02:00
MHSanaei
039d05a743
fix(ci): bump Go to 1.26.4 and exempt /panel/groups SPA route from api-docs test
- Bump go directive to 1.26.4 to pick up stdlib security fixes in
  crypto/x509, mime and net/textproto flagged by govulncheck
- Add /panel/groups to the api_docs_test SPA-page allowlist so the
  UI page route is not treated as an undocumented API endpoint
- go.sum carries pgx/v5 v5.10.0 bump
2026-06-03 15:38:44 +02:00
MHSanaei
db5ce06256
fix(panel-proxy): route custom geo and http(s) Telegram through panelProxy
Custom geosite/geoip downloads built their own ssrfSafeTransport and never used the configured Panel Network Proxy, so geo updates failed on servers where GitHub is filtered. Route all custom-geo HTTP (startup probes + downloads) through panelProxy when set, falling back to the direct SSRF-guarded transport otherwise; the target URL stays SSRF-validated.

The Telegram bot only honored a socks5:// panel proxy and silently rejected http(s)://, despite the setting advertising both. Branch the fasthttp dialer (FasthttpHTTPDialer for http(s), FasthttpSocksDialer for socks5) and accept all three schemes in the fallback and NewBot validation.

Add tests proving the panel proxy is used by custom geo and that the bot dialer speaks HTTP CONNECT vs SOCKS5 per scheme.
2026-06-03 14:57:49 +02:00
MHSanaei
e7c11c913a
feat(inbounds): per-proxy Pinned Peer Cert SHA-256 + labeled External Proxy form
Redesign the Add Inbound -> Stream External Proxy section into labeled per-entry cards (Force TLS / Host / Port / Remark and, under TLS, SNI / Fingerprint / ALPN) and add a Pinned Peer Cert SHA-256 field with a generate-random-hash button to each entry.

The pin flows end to end into share links: pcs for vmess/vless/trojan/ss (stripped when a proxy forces security off) and the hex-normalized pinSHA256 for Hysteria. JSON and Clash subscriptions emit the native pinnedPeerCertSha256 / pin-sha256 via the cloned stream. Adds the forceTls label across all 13 locales plus frontend and Go tests.
2026-06-03 13:46:54 +02:00
MHSanaei
df7ccd3a64
fix(clients): use client_inbounds link to resolve inbound, not stale id
client_traffics.inbound_id is a legacy single-inbound pointer that goes stale when an inbound is deleted and recreated: the email-keyed traffic row survives but references a missing inbound. Code that resolved the owning inbound from it broke several client operations.

- adjustTraffics: 'Start After First Use' (negative expiry) never converted to an absolute deadline on first traffic, so the countdown never started. Now resolves inbounds via the client_inbounds link and computes the new expiry once per email so multi-inbound clients stay consistent.

- GetClientInboundByEmail / GetClientInboundByTrafficID: fall back to client_inbounds when the pointer is dead, fixing reset traffic ('record not found'), client info, and Telegram set-tgId.

- autoRenewClients: resolve renew targets via client_inbounds so scheduled renews are not silently skipped.

- clients page: allow resetting a client with no inbound attachment (the backend already zeroes counters by email).

Add regression test for the delayed-start conversion under a stale inbound_id.
2026-06-03 13:42:32 +02:00
MHSanaei
d4c020f365
feat(dashboard): more System History metrics, persistence & localized labels
- Sample swap %, TCP/UDP connection counts and disk-usage % on the host ticker
- System History: Swap overlaid on the RAM tab, plus new Connections and Disk Usage tabs
- Persist the host time-series across restarts: gob snapshot beside the DB, written on a timer and at shutdown, restored on boot
- Live-refresh the open chart (2s for short ranges, 10s for longer)
- Localize CPU/RAM/Swap and the new tab/chart titles across all 13 languages and route legend series names through i18n
2026-06-03 12:16:31 +02:00
MHSanaei
4b11c54206
feat(dashboard): richer System History & Xray Metrics charts
- Collect disk read/write and network packet-rate metrics on the host sampler
- Sparkline: optional 2nd/3rd overlaid series with a colored legend
- System History: merge Bandwidth (up/down), Disk I/O (read/write) and Load (1m/5m/15m) into single multi-line tabs
- Add a descriptive per-chart title and mobile-only tab icons to both modals
- Localize every chart title and tab label across all 13 languages
2026-06-03 11:25:45 +02:00
MHSanaei
e63cde8fcb
feat(settings): move the remark model control to the subscription tab
Some checks are pending
CI / go-test (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
Relocate Remark Model & Separation Character from the General/Panel tab to the Subscription tab's Information section, beside Show Info and Email in Remark, since it only governs how share-link remarks are composed. The sample preview uses concrete example values and renders the separator literally.

Also drop the port from the subscription page link rows so each row shows just the inbound remark; the port still appears in the client QR modal and the client info modal.
2026-06-03 02:45:16 +02:00
MHSanaei
ccfd04219b
fix(panel): register /groups SPA route so hard refresh returns index.html
The frontend has a groups page route and sidebar entry, but the backend
never registered a GET handler for /panel/groups. A hard browser refresh
on that page fell through to the 404 handler. Add the missing panelSPA
registration alongside the other page routes.

Fixes #4837
2026-06-03 02:17:56 +02:00
MHSanaei
b08fc0c963
fix(clients): keep reverse tag clearable and preserve flow on attach
Two multi-inbound client bugs from issue #4834:

- Clearing a client's reverse tag never persisted: SyncInbound keeps a non-empty sticky guard on reverse (shared with node-sync/rename), so the cleared value never reached the canonical clients.reverse column the edit form reads. Update now writes that column authoritatively from the submitted client, matching how it already writes email/updated_at directly.

- Attaching a new inbound reset xtls-rprx-vision: Attach seeded its wire client from the canonical clients.flow column, which a non-flow inbound can zero during the preceding update. It now derives the flow from EffectiveFlow (the per-inbound flow_override), so flow-capable targets keep the flow and others stay empty.

Adds service tests for both paths and a guard test confirming node-snapshot sync still preserves a stored reverse tag.
2026-06-02 23:47:03 +02:00
MHSanaei
6ee462ac8e
fix(links): use configured domain for panel copy/QR links on loopback
The panel's copy/QR share links are built client-side and fell back to window.location.hostname, so reaching the panel over an SSH tunnel (127.0.0.1/localhost) leaked localhost into the links - unlike the backend subscription path, which falls back to the configured Sub/Web Domain (issue #4829).

Expose webDomain/subDomain via /defaultSettings and add preferPublicHost: when the browser host is loopback, prefer the configured Sub Domain (then Web Domain) for share/QR links. An explicit node override or per-inbound listen still wins; a routable browser host is kept as-is.

Closes #4829
2026-06-02 22:52:44 +02:00
MHSanaei
fcc6787a64
fix(settings): fall back to defaults for empty/NULL setting values
A setting row whose value column is empty or NULL (seen on some migrated databases) was parsed directly, so getInt/getBool and the GetAllSetting reflection path crashed with 'strconv.Atoi: parsing "": invalid syntax'. This made the Inbounds page (/defaultSettings -> GetPageSize) and the Settings page fail to load.

Treat an empty stored value the same as a missing row and fall back to the built-in default at the int/bool parse sites. String getters are unchanged, so legitimately-empty string settings stay empty.

Closes #4830
2026-06-02 22:26:22 +02:00
MHSanaei
3af2da0142
fix(online): scope online status per node instead of a global union
The inbounds page and Nodes page checked each client's email against a
single deduped union of every node's online clients, so a client connected
to one node showed as online on every inbound across every node. The local
online set was also derived from the email-keyed client_traffics.last_online
column, which remote-node syncs bump too, leaking remote-only clients onto
local inbounds.

Track online clients per node: the local panel's own xray clients under key
0 (derived from live traffic-poll deltas via RefreshLocalOnline, kept in
memory and independent of the shared last_online column) and each remote
node under its id. Add GetOnlineClientsByNode plus a /clients/onlinesByNode
endpoint and onlineByNode WS field; node.go and the inbounds rollup now scope
online by node. The flat GetOnlineClients union is kept for client-centric and
total-count views (Clients page, dashboard, telegram).

Closes #4809
2026-06-02 18:33:21 +02:00
MHSanaei
8f5a7b9434
fix(xray): default freedom finalRules to allow-all so reverse egress works
xray-core >=26.5 makes the freedom finalRules context-aware: reverse-proxy traffic defaults to "block all targets". The template seeded finalRules with only allow geoip:private, so a bridge could not exit to WAN and reverse proxy silently broke

Switch the default direct freedom to a no-condition allow rule, the documented way to restore pre-policy behavior. Unlike an ip-based rule (0.0.0.0/0 or !geoip:private), it does not force per-connection OS DNS resolution under domainStrategy AsIs, so happyEyeballs/AsIs pass-through stay intact. LAN is still blocked by the geoip:private->blocked routing rule, and removing that rule still regains LAN access
Note: only affects new configs; existing installs keep their stored finalRules until reset or a follow-up migration.
2026-06-02 15:58:48 +02:00
MHSanaei
1e3c186b2c
fix(clients): derive edit-form flow from per-inbound override
SyncInbound runs once per inbound and unconditionally overwrites the canonical clients.Flow column. A non-flow inbound (Hysteria, WS, gRPC) strips flow to "", so when it syncs after a VLESS Reality inbound the column is wiped, and the hydrate endpoint returned that empty value — the edit form loaded a blank flow for multi-inbound clients (#4792).

Derive the hydrate flow from the first flow-capable client_inbounds.flow_override instead, which is always correct and order-independent. A non-empty guard in SyncInbound was rejected because it would make flow impossible to clear.

Closes #4792
2026-06-02 15:32:48 +02:00
MHSanaei
c9abda7ab8
fix(tls): correct pinned cert SHA-256 hint to hex, not base64
xray-core hex-decodes pinnedPeerCertSha256 and the panel forwards the value as-is into share links and the JSON subscription, so clients hex-decode it too. The tooltip/placeholder wrongly said base64 (copied from the retired pinnedPeerCertificateChainSha256 field), and the "generate random hash" button emitted base64 via btoa, producing an unusable pin. Tooltip/placeholder now say hex across all locales and the generator emits hex.

Closes #4793
2026-06-02 15:14:17 +02:00
MHSanaei
2f12b34635
fix(settings): allow pagination size of 0 to disable pagination
The pageSize setting described '(0 = disable)' and the inbounds table already treated 0 as show-all, but every validation layer enforced a minimum of 1. Relax the bound to gte=0 in the AllSetting struct tag (source of truth for the generated frontend schemas), regenerate zod, and lower the min on the hand-written schema and the InputNumber control.
2026-06-02 14:54:11 +02:00
MHSanaei
66d4d04776
fix(iplimit): populate client IP log without an IP limit
The per-client IP log was only filled as a side effect of IP-limit enforcement: Run() scraped the access log only when some client had limitIp>0, so installs without a limit always showed an empty IP log (#4800).

Decouple collection from enforcement: scrape the access log whenever it is available and thread an enforce flag through processLogFile/updateInboundClientIps so banning still only happens for limited clients. The XUI_ENABLE_FAIL2BAN kill-switch is preserved.

Closes #4800
2026-06-02 14:43:11 +02:00
MHSanaei
91f325eca6
feat(clients): show filtered count in clients list
Surface a "Showing X of Y" counter in the clients filter bar that appears whenever a search term or any filter is active, using the server-provided filtered and total counts. Added the showingCount string across all 13 locales.

Closes #4808
2026-06-02 14:23:52 +02:00
MHSanaei
02043a432d
fix(node): fix "invalid input" on save and gate save on connectivity
The pinnedCertSha256 form field unmounts for non-pin TLS modes, so antd dropped it from the onFinish values and Zod rejected the missing string (the user-facing "invalid input"). Make it optional with a default so saving works in every TLS mode.

Saving now runs the connection test first and only persists when the probe is online; the add/update endpoints enforce the same probe so an unreachable node cannot be stored via the API either.

Selecting the http scheme forces TLS verify mode to skip and disables the control, normalized on open for existing http nodes.

http-vs-https probe failures report a clear "set the node scheme to http" message across the test button, save, and the backend gate.

Closes #4794
2026-06-02 13:57:02 +02:00
MHSanaei
4f597a08c4
perf(clients): batch bulk attach/detach to cut per-item DB work
BulkDetach removed one client per (email x inbound) pair, each with its own
settings rewrite, transaction and full SyncInbound. Add delInboundClients to
remove all targeted clients from an inbound in a single pass and group removals
by inbound, turning O(emails x inbounds) write cycles into O(inbounds).

BulkAttach ran the global getAllEmailSubIDs scan once per target inbound via
checkEmailsExistForClients. Compute that snapshot once per call and thread it
through a new internal addInboundClient; the duplicate check is unaffected
because attach reuses each client's existing identity (same subId).

Covered by bulk_clients_test.go: VLESS round-trip (linkage, settings JSON,
idempotency, record survival), skip-unattached, and Trojan key matching.
2026-06-02 03:59:10 +02:00
MHSanaei
d56505004e
style: gofmt -s (doc-comment list separator, struct field alignment) 2026-06-02 03:58:58 +02:00
MHSanaei
f0e459e51e
fix(node): suppress unavoidable InsecureSkipVerify alert for cert pinning
FetchCertFingerprint must accept any certificate by design: it fetches a
not-yet-pinned node's leaf cert (trust-on-first-use) so the admin can pin
it. Disabling verification is inherent to that, so go/disabled-certificate-check
cannot be cleared by code changes. Suppress the finding inline, matching the
existing lgtm convention in custom_geo.go.
2026-06-02 03:58:52 +02:00
MHSanaei
d2dc589f14
fix(node): capture node cert via VerifyConnection for fingerprint fetch
FetchCertFingerprint read the leaf certificate from a bare insecure TLS
handshake, which CodeQL flagged as go/disabled-certificate-check. The
function intentionally accepts any cert (trust-on-first-use, so the admin
can pin a not-yet-trusted node), so verification cannot be enabled.

Capture the leaf cert inside a VerifyConnection callback instead, matching
the existing pattern in nodeHTTPClientFor that already clears the same
query. Behavior is unchanged.
2026-06-02 03:09:33 +02:00
MHSanaei
7bc31dd194
feat(outbounds): pick dialerProxy from other outbound tags for proxy chaining
Turn the outbound sockopt dialerProxy free-text input into a searchable Select populated with the other outbound tags, so users can build a proxy chain (route one outbound through another) without typing tags by hand. The list excludes the current outbound, so self-reference cycles cannot be selected. A tooltip and placeholder explain the chaining concept. Adds dialerProxyPlaceholder and dialerProxyHint to all 13 locales.

Closes #4446
2026-06-02 01:52:38 +02:00
Mayurifag
8fa248c621
fix(job): skip fail2ban IP limit when disabled (#4581)
Honor XUI_ENABLE_FAIL2BAN before running fail2ban-dependent IP-limit work. This avoids spawning fail2ban-client on disabled Docker installs while preserving the default enabled behavior when the env var is unset.

Co-authored-by: Mayurifag <Mayurifag@users.noreply.github.com>
2026-06-02 01:36:24 +02:00
MHSanaei
56ec359041
feat(nodes): add per-node TLS verification mode for self-signed certs (#4757)
Adds a per-node TLS verification mode to the Add/Edit Node dialog so the panel can reach nodes that serve HTTPS with a self-signed certificate:

- verify (default): normal CA validation.
- skip: InsecureSkipVerify, with a clear UI warning that it drops MITM protection.
- pin: validates the leaf certificate's SHA-256 (base64 or hex) via VerifyConnection while bypassing the default chain/name check — keeps MITM protection for self-signed certs, the secure alternative to skip.

New Node model fields tlsVerifyMode + pinnedCertSha256 (gorm auto-migrated). Probe() selects the HTTP client per node via nodeHTTPClientFor, keeping the SSRF-guarded dialer. A new POST /panel/api/nodes/certFingerprint endpoint (FetchCertFingerprint) lets the UI fetch and pin the node's current certificate in one click. Endpoint documented in api-docs/openapi; i18n added across all locales. Verified end-to-end in Docker (verify rejects, skip bypasses, fetch matches, pin accepts correct / rejects wrong).
2026-06-02 01:24:27 +02:00
MHSanaei
b2e2120eb3
feat(inbounds): support Unix domain socket path in Listen field (#4429)
UDS listen already worked for proxying (the listen string is passed to xray verbatim and port 0 is accepted), and the Go sub/link layer already ignores the bind listen. The only gap was the frontend resolveAddr, which would put a socket-path listen into share/sub links (e.g. vless://uuid@/run/xray/x.sock:0). resolveAddr now treats a path-style listen (starting with / or @) as having no client-reachable address and falls back to hostOverride/hostname. Adds a test and a Listen-field help hint across all locales.
2026-06-02 00:37:20 +02:00
MHSanaei
49bec1db0f
fix(fallbacks): allow free-form dest entries for external servers (#4748)
Since v3.1.0 every fallback row had to reference a panel inbound via childId, so rows with only a free-form dest (e.g. 8080 or 127.0.0.1:8080 to an external Nginx) were silently dropped at three layers: the frontend save filter, the backend SetByMaster guard, and BuildFallbacksJSON. A row is now valid when it has a child OR an explicit dest; self-references normalize to childId 0, and BuildFallbacksJSON prefers an explicit dest (also fixing rows whose child was deleted). UI gains allowClear on the child picker; help text updated across all locales. Verified end-to-end in Docker: a free-form dest fallback now persists and is injected into the live xray config. Refs #4554, #4639.
2026-06-02 00:17:21 +02:00
MHSanaei
3657ed55dc
fix(warp): persist client_id so WARP outbound gets reserved bytes (#4781)
RegWarp now stores config.client_id from the Cloudflare registration, and WarpModal sources the reserved bytes from the live config response (falling back to stored creds). Previously reservedFor read an always-missing client_id, producing an empty reserved array.
2026-06-01 23:14:40 +02:00
MHSanaei
5b9ed34009
fix(nodes): sum client traffic across nodes instead of overwriting
A client shared across multiple nodes has a single email-keyed client_traffics row, but each node reports its cumulative up/down. setRemoteTrafficLocked overwrote the row with one node's cumulative, so non-owning nodes hit the create branch and OnConflict-DoNothing, silently dropping their traffic and under-counting the client.

Make the shared row a pure accumulator (like the local path): a new node_client_traffics(node_id, email) baseline table stores each node's last cumulative; the node path converts cumulative to a per-node delta (clamped to the post-reset value on a negative delta) and does up = up + delta. First observation seeds the baseline and adds 0 so upgrades and newly-shared clients are not double-counted. Create-vs-accumulate now keys off global email existence. Baselines are cleaned in DelClientStat, the node sweeps, and NodeService.Delete.
2026-06-01 22:54:56 +02:00
MHSanaei
72121784fe
test(iplimit): align ban-policy tests with last-IP-wins (#4699)
PR #4699 restored the "keep newest live IP, ban the oldest" policy in
check_client_ip_job.go but left the integration test asserting the old
"protect original, ban newcomer" behavior, so it failed. Update the test
to expect the oldest live IP banned and the newest kept, and fix the now
misleading name/comment on the partitionLiveIps concurrency unit test.
2026-06-01 17:17:43 +02:00
ALOKY
16edb037e7
Fix IP limit enforcement and clarify related comments (#4699)
* fix: keep latest IP for limit enforcement

* chore: clarify IP limit comment

* chore: clarify timestamp sorting comment

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 16:34:08 +02:00
MHSanaei
a3dca4b82d
fix(inbounds): drop listen address from auto-generated inbound tag
A non-empty, non-any Address (listen) leaked into the tag as
in-<listen>:<port>-<transport> (e.g. in-127.0.0.1:443-tcp). The tag is
now always in-<port>-<transport>, with the node prefix and numeric dedup
suffix still handling uniqueness across nodes and same-port/different-listen
inbounds. Mirrored in the Go authority and the TS form preview, kept in
parity by tests.

Existing colon-form tags are now treated as custom, so editing such an
inbound preserves its tag rather than rewriting it; new inbounds (or a
cleared tag field) get the clean form.
2026-06-01 09:33:49 +02:00
MHSanaei
88a3677318
feat(clients): enforce unique subId per client like email
Reject creating or editing a client with a subId already owned by a different client, mirroring the email-uniqueness checks against client_records in Create and Update (BulkCreate inherits via Create). The old multi-inbound model duplicated a client across inbounds sharing one subId, so this check was dropped; the first-class multi-client model makes per-client subId uniqueness correct again. Existing duplicates are left untouched; only new/edited duplicates are blocked.
2026-06-01 08:34:48 +02:00
MHSanaei
dd14e9b3b0
feat(inbounds): attach existing clients to an inbound in one click
Adds an 'Attach Existing Clients' row action on multi-user inbounds (shown even when the inbound is empty). It opens a modal listing the whole client pool with search and group filter, all attachable clients pre-selected, and attaches the selection to that inbound via the existing bulkAttach endpoint. Clients already on the inbound are shown disabled and skipped. Translations added for all 13 locales.
2026-06-01 07:26:30 +02:00
MHSanaei
971843f669
feat(nodes): bulk panel self-update with live online indicator
Adds the ability to update node panels to the latest release from the Nodes
page: select online, enabled nodes (checkboxes) and trigger their official
self-updater, or use the per-row Update action. A node whose reported panel
version trails the latest GitHub release is flagged with an 'update available'
tag (compared via lib/panel-version, mirroring the Go isNewerVersion).

Backend: Remote.UpdatePanel calls the node's existing
POST /panel/api/server/updatePanel; NodeService.UpdatePanels fans out over the
selected ids, skipping disabled/offline nodes with a per-node reason; exposed
as POST /panel/api/nodes/updatePanel (documented in endpoints.ts + openapi.json).

The bulk request sends a JSON body, so it sets Content-Type: application/json
explicitly — axios defaults POST to form-urlencoded, which made ShouldBindJSON
fail with 'invalid character i'.

Also reuses the clients-page online cue on the Nodes page: a pulsing green dot
plus green label for an online node. The .online-dot style moved to the shared
styles/utils.css so both pages load it.

Translations for all new node keys added across every language file.
2026-06-01 07:03:06 +02:00