txlyre

txlyre synced commits to main at txlyre/3x-ui from mirror

  • c9207b6f3e feat(install): install PostgreSQL 18 from PGDG for new local databases The local PostgreSQL setup installed whatever the distro metapackage pointed at, so Ubuntu 22.04 got 14 (EOL Nov 2026), Debian 12 got 15 and EL9 got 13. Servers ended up on different majors, and a pg_dump backup taken on a newer server does not restore onto an older one. New installs now add the official PGDG repository and install PostgreSQL 18 (apt on Debian/Ubuntu, dnf on EL 8+ with the AppStream module disabled, postgresql-18 service and /var/lib/pgsql/18/data). Where PGDG has no build (Ubuntu 20.04, Debian 11, armhf/i386/s390x, EL7, Fedora, Amazon Linux, older EL point releases) the repository change is rolled back and the distro package is installed as before. A host that already has a cluster keeps it, so existing installs are never upgraded. x-ui.sh resolves the postgresql-18 unit on EL for start/stop/log and also removes the PGDG packages on purge. Verified in containers: Ubuntu 22.04/24.04, Debian 12, Rocky 8/9 and AlmaLinux 10 get 18.6; Ubuntu 20.04 falls back to 12 and Debian 12 armv7 to 15 with apt-get update still clean; Ubuntu 22.04 with an existing 14 cluster stays on 14. The CI PostgreSQL test set passes against postgres:18.
  • 6be3c438e1 fix(node): keep client traffic history when a node inbound is replaced When a node snapshot reports an inbound under a tag that matches neither the central tag nor an origin/port alias, SetRemoteTraffic adopts it as a new central inbound and, in the same tick, sweeps the old one. The sweep deleted the old inbound's client_traffics rows even though the snapshot still reported those emails under the new inbound. existingEmails was computed before the sweep, so the rows were not recreated that tick; on the next tick the inbound was no longer new, the rows came back seeded at 0/0 and the node baseline jumped to the node's totals. Quota accounting lost every byte the clients had used. The sweep now moves rows for emails the snapshot still reports to the inbound now carrying them, instead of deleting them. Only emails the node no longer reports at all lose their row, mirroring the existing baseline rule. The alias check also treated a master-created node inbound that had not been tag-matched yet (empty origin_node_guid) as foreign, so a renamed tag on its first sync replaced it instead of aliasing it. An empty origin now counts as this node. PostgreSQL lane not run locally; the change is a plain UPDATE through GORM. Closes #6749
  • d57dcf824b chore(deps): bump frontend and docs deps, clear npm audit findings Bump vite, oxlint/oxfmt, msw, jsdom and plugin-react in the frontend, and fumadocs, lucide-react, oxlint/oxfmt, postcss and pnpm in docs. npm audit reported seroval <=1.6.2 (critical) via solid-js, which the react-query devtools pull in pinned to ~1.5.4, and sprintf-js (no patched release) via remarkable's argparse@1. `npm audit fix --force` "fixed" both by downgrading swagger-ui-react to 3.23.3, which brings in far worse advisories. Instead override seroval/seroval-plugins to ^1.6.8 and give remarkable argparse@2; remarkable only requires argparse from its unused CLI entry, so the bundled library is unaffected. pnpm appends an exact-version minimumReleaseAgeExclude entry on every update of a too-fresh release and never prunes them. Drop the stale ones; keep only fumadocs 16.16.2 and oxlint/oxfmt, still inside the 1-day cutoff.
  • View comparison for these 3 commits »

3 hours ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 66ef5bbc05 fix(sub): emit host TLS verification at xray level in JSON subscription The JSON subscription flattens tlsSettings into xray's client shape before the per-host loop, then applyExternalProxyTLSToStream writes the host's echConfigList, verifyPeerCertByName, pinnedPeerCertSha256 and allowInsecure into the panel-only tlsSettings.settings map. Xray ignores that map, so a host's ECH (and its pins / verify name) never reached the client and connections through the host failed. After the host overrides are applied, lift that map through the same writer tlsData uses for the inbound's own TLS: fields land at the top of tlsSettings, pins are joined into the comma string xray parses, and allowInsecure is dropped exactly as it is for the inbound (removed from xray). The Clash renderer still reads the nested map and is unchanged. The helper-level subtest that pinned the nested location as the "json" shape is replaced by an end-to-end GetJson test on a host-backed inbound. Closes #6743
  • dd9569478e fix(node): ignore a node's stats for clients detached from the inbound Invariant: a node snapshot may move a master client's traffic state only for emails the reporting inbound's settings still list. A master detach calls the node's /detach endpoint, which keeps the stat row (keepTraffic), and the node's GetInbounds preloads ClientStats by inbound_id alone, so the node goes on reporting the detached email under that inbound. setRemoteTrafficLocked merged every reported entry: it copied the node's stale total onto the master's per-email row, after which the matching limits made the node's enable=false look genuine and latched the client disabled on the master. A master quota edit only reaches inbounds the client is attached to, so the next sync (every 5s) wrote the stale quota back. The same unchecked loop also created a fresh client_traffics row for a reported email the master no longer had once its tombstone expired. Snapshot entries are now gated on the inbound's settings.clients. When the settings cannot be parsed, membership is unknown and every entry is merged as before. Closes #6724
  • View comparison for these 2 commits »

1 day ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 49fbcdc09c fix(frontend): wrap overview modal action buttons on long labels The Geodata Auto-Update actions row is a non-wrapping flex row with no gap. Its three buttons fit in English, but the longer Russian, Ukrainian and Turkish labels make the row wider than the default-width modal, so the buttons spill out of it. The row now wraps with an 8px gap. .actions-row is a global class defined identically in VersionModal.css and PanelUpdateModal.css, so both copies change to keep the cascade order irrelevant. Verified in Chromium with a temporary Storybook story rendering VersionModal in ru-RU: the first button sat 88px outside the modal before the change and inside it after. Closes #6737
  • aacfaebab8 fix(tuic): client speed display and certificate button layout (#6723) * fix(tuic): restore client speed and certificate layout * docs(tuic): clarify native runtime and protocol behavior * fix(websocket): preserve traffic updates from independent sources * fix(docs): sync websocket traffic schema and drop restating TUIC tests docs/public/openapi.json still described the old traffic event, without clientTrafficSource/clientTrafficIntervalMs or the TUIC oneOf branch, so the docs site showed a payload the panel no longer sends. No check covers that copy. The TUIC certificate layout test and the TUIC speed payload test only read back the literals the code writes, so neither could fail on a real regression. Both are removed, along with the className that existed only for the layout test. --------- Co-authored-by: MHSanaei <[email protected]>
  • e897b0957a fix(sub): append host serverDescription to hysteria links (#6740) * fix(sub): append host serverDescription to hysteria links A host's Description reached vless/trojan/ss through buildEndpointLinks, but genHysteriaLink renders the fragment in its own externalProxy loop and never added the suffix, so Happ fell back to its "Hysteria | hysteria | TLS" caption for every Hysteria server on a host that also serves VLESS (#6738). Reuse appendHappServerDescription with the description the endpoint map already carries, so no key lookup is duplicated and a host with no description emits the same bytes as before. genTuicLink (service.go:912) has the same gap; left alone to keep this diff to the reported protocol. Regression test is red without the fix for both hysteria:// and hysteria2://. * chore(sub): trim the hysteria serverDescription regression test The no-description test passed with and without the #6738 fix: the empty description branch is already pinned by TestAppendHappServerDescription, so it certified nothing about this change. Also cut the remaining test's comment block to the two-line limit CLAUDE.md sets. --------- Co-authored-by: MHSanaei <[email protected]>
  • b42a1c0ba1 fix(systemd): harden shipped x-ui unit files (#6718) * fix(systemd): harden shipped x-ui unit files The units ran the panel as root with no sandboxing: systemd-analyze security rates them 9.6 UNSAFE. Add NoNewPrivileges, ProtectSystem=full with ReadWritePaths for the default XUI_DB_FOLDER/XUI_BIN_FOLDER/XUI_LOG_FOLDER stores, kernel and clock protections, UMask=0077, RestrictAddressFamilies, a CapabilityBoundingSet with NET_ADMIN/NET_BIND_SERVICE/NET_RAW and SystemCallFilter=@system-service. PrivateTmp is deliberately omitted: the web updater hands a path inside the system temp directory to a systemd-run transient unit, which does not share the service's private tmpfs. ProtectHome stays read-only because installs keep TLS certificates under the root home directory. Fixes #6605 * fix(systemd): ship ReadWriteDirectories= alias for systemd < 231 ReadWritePaths= only exists since systemd 231; install.sh still supports CentOS 7 (systemd 219), where the directive is ignored and ProtectSystem=full would leave the panel state directory read-only, breaking its database. * fix(systemd): keep root's DAC bits and regenerate the write paths Two follow-ups to the hardening, both reported by review on #6718. CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH were dropped from the bounding set. Root holds them normally, and a bounding set is subtracted from root too: the panel could no longer read a private key it does not own (a Caddy-issued certificate under its own state dir, an acme.sh home, any 0600 file owned by another account). That fails quietly for TLS -- the panel listener logs the tls.LoadX509KeyPair error and keeps serving plain HTTP, and Xray inbounds using that key stop -- so both bits stay. ProtectSystem=full plus a hard-coded ReadWritePaths list broke installs whose XUI_DB_FOLDER/XUI_LOG_FOLDER/XUI_BIN_FOLDER live outside the defaults, and the workaround of editing the unit did not survive an update, because install.sh and update.sh reinstall the unit from the release tarball. The folders actually in use are now resolved from the same env file the unit passes to the panel and regenerated into x-ui.service.d/10-xui-write-paths.conf on every install and update, so a relocated store stays writable and the list is not reset. The unit keeps the plain-install defaults plus XUI_SERVICE, which the in-panel updater needs when systemd-run is unavailable and it falls back to a child process that inherits this sandbox while update.sh lands the unit again. Uninstall removes the drop-in with the unit. * fix(systemd): keep the seccomp whitelist off old systemd, tighten the rest Review of the previous head found that SystemCallFilter=@system-service plus SystemCallErrorNumber=EPERM is a hard regression on the platforms this PR means to keep working. @-named filter groups exist from systemd 239 on, and older systemd does not ignore an unknown group name: 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 -- a CentOS 7 or Ubuntu 18.04 install would come up dead after this update. The two directives now live in the generated drop-in and are written only when "systemctl --version" reports 239 or newer, so old hosts keep the rest of the hardening and simply go without seccomp. The same review listed three more items, all addressed here: - a comment claiming ProtectSystem=full "keeps everything outside /var, /run and the listed ReadWritePaths read-only" -- that is `strict`; `full` locks down /usr, /boot, /efi and /etc; - /etc/systemd/system was granted writable for the in-panel updater's fallback, but that fallback cannot work under this sandbox at all: update.sh also stages the release archive beside the main folder, replaces /usr/bin/x-ui and calls the package manager. The entry is gone and update.sh now stops up front with one clear message when the directories it needs are read-only, instead of failing halfway with "Failed to download x-ui"; - CAP_DAC_READ_SEARCH is redundant next to CAP_DAC_OVERRIDE, so the bounding set keeps just the latter. Relocating a store by editing the env file alone is documented in the unit and in the generated drop-in: the drop-in is only written by install/update, so one of those has to be re-run afterwards. Verified with a local harness (9 checks: plain defaults, relocated store read from the env file, the same list produced by update.sh, duplicate collapse, seccomp present at systemd 249 and absent at 238, read-only guard) and bash -n on install.sh, update.sh, x-ui.sh. systemd-analyze is not available here, so the unit files themselves are unverified by a parser. * fix(systemd): actually wire the read-only guard, drop the superseded drop-in Re-review of the previous head caught two leftovers from that commit: - require_writable_update_paths was defined but never called, so the guard the unit comments, the commit message and the PR comment promise did not exist at all. It is now called at the top level, before install_base, i.e. before anything with a side effect: a sandboxed fallback run stops with one clear message instead of failing halfway, which on a relocated main folder meant the old install removed and the service folder rewritten before dying on /usr/bin. - the drop-in this branch replaced (10-xui-write-paths.conf) is no longer written or referenced, but nothing removed it either. Whoever installed the build that wrote it keeps its wider list, including the writable service folder, until it is deleted by hand. Both generators now remove it. Harness extended to 11 checks: the superseded file is gone after a run, the guard is actually called, plus the previous nine (defaults, env-file relocation, same list from update.sh, dedupe, seccomp at 249 / absent at 238, read-only guard) and bash -n on the three scripts. * fix(systemd): correct two comments and keep spaces out of the path list Second-opinion review of the previous head (two models, both asked to state platforms and versions) produced three actionable items: a wrong comment kept from the earlier commits, a wrong generalisation about the filter groups, and a path-list case that would leave the panel unable to start. - the ProtectSystem= comment claimed strict leaves /var and /run writable. It does not: strict mounts the whole hierarchy read-only and only the kernel API filesystems stay as they are. The sentence was already wrong before this branch and moving it to ProtectSystem=full did not fix it. - "the @-named filter groups need systemd >= 239" is the wrong generalisation: named groups exist since 231, it is @system-service that arrived in 239. The unit files, both script comments and the drop-in body now name the group. - a folder containing whitespace (XUI_DB_FOLDER="/srv/panel data") was written into ReadWritePaths= verbatim. That directive is a whitespace-separated list, so the entry splits into "-/srv/panel" and "data", and systemd rejects the whole drop-in: the panel then does not start at all. Such folders are left out and reported to the operator instead; the other paths are still written. Harness extended with three checks for the whitespace case (folder left out, remaining paths intact, warning emitted) and the duplicate-store case now reads its own env file instead of the previous one, so it tests what it claims. 14 checks plus bash -n on the three scripts, all passing. * fix(systemd): act on the independent review of the drop-in generator A read-only review of the branch head (another model, given the diff and the sources, asked to cite only verified lines) confirmed the earlier work and turned up four items that are fixed here: - a folder name carrying a literal % went into ReadWritePaths= as it was, and systemd expands %-specifiers in unit files: with XUI_DB_FOLDER=/srv/x%-ui the entry no longer named the directory the panel writes to and the panel could not write its database. The path is now emitted as %%; the duplicate check keeps comparing the unescaped value. - systemd older than 229/242/244 does not know NoNewPrivileges, ProtectClock, ProtectHostname and ProtectKernelLogs. It logs them and carries on, so CentOS 7 (systemd 219, which install.sh explicitly supports) runs with less hardening than the unit lists. install.sh and update.sh now print which protections need a newer systemd, which ones still apply, and that upgrading systemd is what changes it. - the updater's writability guard asked [[ -w ]] about the parent directories. It creates and removes a probe file instead, so an immutable attribute or a full filesystem is caught as well (a read-only mount was already caught). - the generator's comment claimed to resolve the folders the service actually uses, while the shipped unit hard-codes WorkingDirectory= and ExecStart= under /usr/local/x-ui. The comment now states what XUI_MAIN_FOLDER really feeds -- the location install.sh/update.sh install into and the base for a relative XUI_BIN_FOLDER -- and that a relocated main folder needs the unit edited too. Rejected from the same review, with the evidence: that [[ -w ]] cannot see a read-only mount (access(W_OK)/faccessat consults __mnt_is_readonly before the mode bits), and that the /etc ReadWritePaths entry is an exception granted for /etc rather than a default store already in the list. Harness extended: 19 checks (escaped %, the old-systemd note, whitespace and duplicate folders, seccomp gating, the read-only guard) plus bash -n on the three scripts, all passing. * fix(systemd): name the hardening old systemd really ignores The old-systemd note fired only below 239 and listed wrong versions: RHEL 8 (239) and Debian 10 (241) silently lose ProtectHostname and RestrictSUIDSGID (242), ProtectKernelLogs (244) and ProtectClock (245) with no note, while CentOS 7 was told NoNewPrivileges (187) and ProtectHome=read-only (214) were not applied although both are. The note is now built from a directive/version table taken from systemd.exec(5) and lists only what the running systemd lacks. Also drop the removal of 10-xui-write-paths.conf: only an intermediate commit of this branch wrote that file, no release ever shipped it. --------- Co-authored-by: Кот <[email protected]> Co-authored-by: Sanaei <[email protected]>
  • a8d65a55b0 fix(update): run the database migration before starting the service (#6729) update_x-ui() started x-ui.service and then called config_after_update right away, which runs `x-ui setting -show true` and `x-ui migrate`. The service and the CLI each run InitDB(), and with it every schema migration, on the same database at the same time. On an upgrade that adds schema, the loser exits with an error. Upgrading 3.8.5 to 3.9.0 stopped the service with "duplicate column name: exclude_from_sub", and only Restart=on-failure brought it back 5 s later. Tolerating the duplicate-column error in the column helpers is not enough. The same race also hits the tables new in 3.9.0: concurrent InitDB fails with "table `node_pending_resets` already exists" and "table `tuic_traffic_receipts` already exists". The cause is two processes migrating at once, so the fix is to stop that from happening during update. Run `x-ui migrate` to completion before the service is started, on both the systemd and the OpenRC path. This mirrors install.sh, whose config_after_install already migrates before the first start. The service and the follow-up CLI calls then find the schema current, and their InitDB has nothing to change. Refs #6728
  • View comparison for these 9 commits »

1 day ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 5819a01cdc fix(api-token): let a node-sync master reach every node endpoint it calls Invariant: every request runtime.Remote sends to a node is accepted under the node-sync scope, except /server/updatePanel, which #6201 withholds on purpose. The allowlist was written on 2026-08-15 and three Remote calls arrived after it without being added: /clients/activeInbounds (#6164, same day), /inbounds/:id/subSortIndex (#6179) and /clients/bulkResetTraffic (4210a50c). A master enrolled with a node-sync token or mTLS got 403 on each: the subscription sort order and multi-client traffic resets never reached the node, and online attribution silently fell back to email-only because FetchTrafficSnapshot logs that failure at debug level. TestMasterNodeContract drives every exported Remote method through the production router, once per enrollment scope, fails on any 401/403 the node returns even when Remote swallows it, and fails when a Remote method has no cell. It replaces TestNodeSyncScopeAllowlistMatchesRemoteInventory, a hand-copied duplicate of the allowlist that never read Remote and so missed all three; its updatePanel rule stays in TestNodeSyncScopeUsesFullPathPatterns. It also supersedes TestMasterPushWithAdminTokenAppliesClients (its UpdateInbound cell); that file's removal landed in dae91276 by a staging slip.
  • dae91276aa chore(nodes): run the master+node scopes in parallel and document both layers Each enrollment scope owns its panels, ports and temp dirs, so the two run side by side; the only shared state, the process-global database handle the harness borrows for setup and for simulating a lost inbound, is now behind a mutex. On Linux the suite drops from 188s to 95s. CLAUDE.md now names the fast contract layer (node_contract_test.go, in make test-go) next to the multi-tick nodee2e layer.
  • f843fe5570 chore(nodes): add a master+node end-to-end harness and CI job Node sync had no test with two real panels: the single-panel tests either call the node's controller in-process or hand-set the node-sync scope, so 823db059 shipped a regression no test could see (an admin-token node dropped every client and enable flag its master pushed). internal/nodee2e starts a master and a node as separate panel processes (the DB is a process-wide global, so one process cannot be both) and walks 17 operations per enrollment scope (admin token, node-sync token): adopt, create/edit on the master, small and bulk attach, client disable, detach, client delete, inbound disable, traffic pull and reset, node-side delete mirrored centrally (#6219), master-side delete, node down, node-lost inbound re-created, create/edit while down, node disabled on the master, and selected sync mode leaving unselected inbounds alone. Against the build before 2ffc694a it fails 6 cells for an admin-token node and 1 for a node-sync one; on main all 34 pass, on Windows and on Linux. `make node-e2e` builds a stub-dist binary and runs it; ci.yml runs it as its own job where a SKIP fails the build. Xray is not started, so cells assert the node's stored state, not traffic through the core.
  • 2ffc694a6d fix(nodes): let a master's push and reconcile converge node inbounds Invariant: every inbound the master holds for a node, with its clients and enable flag, converges onto that node at the next push or reconcile. 823db059 made an inbound save keep the stored clients and enable unless the request is a master push, recognised only by a node-sync token scope. Nodes are usually enrolled with an admin token (the -getApiToken and install default), so their nodes treated every master push as a stale form save: clients attached on the master (Attach existing clients, bulk attach above the per-client threshold, any dirty-node reconcile) never reached the node, yet the push was recorded as successful so reconcile stopped retrying. Remote now marks every request with X-3x-Master-Push, and the node honours it like the node-sync scope. The browser form never sends it, so the stale-form protection for panel saves and plain API scripts is unchanged. Separately, an inbound deleted on the node kept its tag->id in the master's cache, so reconcile sent update/<deleted id> forever instead of re-creating it. When the node reports no form of the tag, ReconcileInbound now drops the cached id so the resolve re-reads the node and falls back to add. Two runtime tests pinned the old update-to-cached-id path for an absent inbound; they now count the add, or report the inbound present.
  • View comparison for these 4 commits »

1 day ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 815c9c5772 fix(tuic): accept the server's STOP_SENDING when tests close uni streams The race job failed in TestAudit3ManagerEnsureActualSendersWithPersistentTraffic with "close called for canceled stream 14". The server parses one command per uni stream and then calls CancelRead, as the quinn reference server does on drop, so its STOP_SENDING can reach the client before the client's own Close and quic-go reports that Close as an error. The data was already read. Every test that wrote a command on a uni stream and required Close to succeed shared this race. closeUniStream accepts only a remote StreamError on the stream's context, so any other Close failure still fails the test.

3 days ago

txlyre synced commits to v3.9.0 at txlyre/3x-ui from mirror

3 days ago

txlyre synced new reference v3.9.0 to txlyre/3x-ui from mirror

3 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 05eb06f333 fix(tuic): wait for both traffic counters in the relay E2E tests The race job failed on TestServerUDPDatagramE2E with Up:0 Down:1300. BytesUp is added on the sending goroutine after the relay Send returns, while BytesDown is added on the response goroutine, so the mock echo can be counted and delivered before the upload is. The test drained the counters once right after the reply and assumed both were present. Production is unaffected: deltas left for the next collection window are still summed. The TCP E2E test made the same assumption, so both now accumulate drained deltas until up and down reach the payload size.
  • 3cd4bf504c v3.9.0
  • ede275e4dc fix(server): apply the outbound address policy to remote cert pinning The remote certificate fetch now dials through the same netsafe guard as the REALITY target scan. A private or loopback endpoint is refused unless the request carries allowPrivate; the inbound form asks the operator to confirm and retries with the opt-in.
  • d31465e37b fix(database): keep the dump restore inside its own database file A SQL dump replay only has to rebuild the tables of the database it restores into. Run it on a single connection whose attached-database limit is zero, so the script cannot open or create any other file.
  • 7d232a76c9 style(sponsor): stack the banner's sponsor tag above Visit
  • View comparison for these 5 commits »

3 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 0054e671f8 feat(tuic): implement native in-process Go TUIC v5 server (#6577) * feat(tuic): implement native in-process Go TUIC v5 server - Implement native TUIC v5 protocol server on pure Go using quic-go - Bridge decrypted TCP/UDP traffic into Xray-core via loopback SOCKS5 inbound - Support full Xray routing rules (geosite/geoip) and cascading outbounds - Implement atomic per-client traffic accounting with TotalGB and ExpiryTime - Add automatic legacy cleanup for older Rust tuic-server binaries, configs, and orphaned processes - Eliminate external Rust tuic-server downloads from install/CI scripts * fix(tuic): address traffic accounting, client reload, and socket lifecycle issues * fix(service): update checkTuicSocksReverseConflict to use bindAddr for listenOverlaps * fix(tuic): resolve traffic double-accounting, UDP fragmentation, and socket lifecycle issues * feat(tuic): complete native Go integration and address audit findings - Integrate an isolated QUIC fork pinned to a specific commit - Preserve original QUIC dependencies for Xray, Hysteria and Gin - Apply BBR, CUBIC and Reno to server connections and exported client profiles - Bridge Xray BBR with correct monotonic time and congestion type conversions - Handle congestion sender recreation after PMTU changes - Update congestion control for new connections without restarting the listener - Preserve existing connections and their selected congestion controller - Apply per-inbound log levels through the shared panel logger - Add lifecycle, authentication and TCP/UDP relay events without exposing secrets - Rate-limit repeated authentication and relay warnings - Support native and QUIC UDP relay modes on the same listener - Recover UDP associations after relay worker failures - Fix TCP relay cancellation, idle shutdown and half-close handling - Close active sessions when client credentials are revoked or disabled - Track traffic by immutable client statistics IDs across email and UUID changes - Prevent ambiguous accounting and duplicate UUIDs within TUIC inbounds - Persist pending traffic in a durable shutdown journal - Replay journal batches transactionally without duplicate accounting - Report server shutdown failures through the shared logger - Preserve legacy flat and nested TUIC settings compatibility - Normalize congestion controller values consistently across backend and frontend - Preserve controller, UDP mode and SNI in client links and subscriptions - Separate client profile options from server settings in the TUIC form - Keep certificate path autofill explicit when changing client SNI - Align UDP packet size validation with protocol limits - Simplify and localize TUIC field hints and certificate autofill messages - Add controller, TCP/UDP, logging and live settings update tests - Add accounting identity, journal replay and shutdown regression tests - Add relay recovery, session revocation and legacy frontend form tests * fix(service): alias the TUIC duplicate-UUID subquery for PostgreSQL < 16 syncInboundClients runs a COUNT(*) FROM (subquery) for every client sync, whatever the protocol. PostgreSQL before 16 rejects a FROM subquery with no alias, so on the distro PostgreSQL install.sh provisions (14 on Ubuntu 22.04, 15 on Debian 12) every client add or edit failed with SQLSTATE 42601. Reproduced against postgres:15 with the new env-gated test. * fix(database): create tuic_traffic_receipts through the model migration AddTuicTrafficBatch issued CREATE TABLE IF NOT EXISTS at runtime, a schema change outside db.go. The table was invisible to allModels and migrationModels, so x-ui migrate-db dropped the receipts and a retained journal could be counted twice after a SQLite to PostgreSQL move. It is now a GORM model in both lists, and the insert uses OnConflict DoNothing. * chore(tuic): skip the ICMP-dependent relay test on Windows, drop dead collectors Go disables SIO_UDP_CONNRESET on Windows, so a dead UDP bridge never fails a read there and TestAudit3UDPAssociationMustRecoverAfterBridgeReadFailure was red on every Windows run. Server.CollectTotalTraffic and Manager.CollectTraffic had no caller. * refactor(tuic): serve TUIC on apernet/quic-go instead of a personal fork The native server depended on github.com/poise52/quic-go, a personal fork of apernet/quic-go patched only to pick the congestion controller before the handshake. That put a second QUIC/TLS stack in the binary that no upstream security fix reaches. apernet/quic-go is already in the graph through xray-core and exposes SetCongestionControl, so BBR is now installed on each accepted connection with Xray's own congestion.UseBBR; the cross-module BBR adapter is gone. apernet ships New Reno as its only built-in sender, so a cubic setting is served as new_reno server-side (clients still get cubic in their profile). The test inspectors now read the sender under congestionMutex, which the post-handshake install writes under. Linux loopback, single stream through Xray: 2428 -> 3383 Mbit/s (bbr). --------- Co-authored-by: Sanaei <[email protected]>
  • 40ca2cd72f Reformat clientSearchCols slice literal
  • bb18734c77 fix(tgbot): resolve the panel egress bridge per connection The bot read the panel-egress bridge once at start, so when Xray came up after the bot (or Panel Outbound was set later) it kept dialing Telegram directly until restarted - on a filtered host it never connected. With no dedicated bot proxy, the fasthttp client now resolves the bridge on every new connection and falls back to a direct dial when it is absent. Raised in #6682.
  • 4aa9a382b8 Keep a firewalld pulled in by fail2ban from blocking ports on EL7 (#6688)
  • 3948b83405 Write config.json after a hot apply (#6686) * Write config.json after a hot apply tryHotApply only updated the in-memory snapshot, so bin/config.json stayed stale until the next cold start and the Telegram/Discord config backups uploaded old rules. Persist the new config once the API calls succeed; a write failure is logged and does not restart the running core. * Test that a hot apply refreshes config.json
  • View comparison for these 14 commits »

4 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 05a083eaef fix(api-token): keep a token's scope when -getApiToken regenerates it, add -tokenScope (#6700) * fix(api-token): keep a token's scope when the CLI regenerates it RecreateByName deleted the named row and created a new one without a Scope, so the insert took the column default of admin. Since -tokenName lets the CLI regenerate any token, rotating a monitor or node-sync token silently turned it into a full-access one. The replacement now takes the scope of the row it replaces, and a new name still gets admin as before. A stored scope this build does not know, as after a downgrade, fails the rotation and leaves the row alone instead of guessing. Assisted-by: Claude Code:claude-opus-5-5 (mostly) * feat(cli): let -getApiToken choose the scope of the token it issues -tokenScope sets the scope on both branches of -getApiToken: the token minted on a fresh panel and the one regenerated on a populated panel. Without the flag a regenerated token keeps its scope and a new one gets admin, so every existing invocation, install.sh included, behaves as before. An unknown scope is refused before anything is deleted, so a typo cannot revoke the token it meant to rotate. Assisted-by: Claude Code:claude-opus-5-5 (mostly) * fix(api-token): keep a token's expiry when the CLI regenerates it RecreateByName built the replacement row with ExpiresAt 0, so running `x-ui setting -getApiToken -tokenName <name>` on a token issued through the API with a deadline handed back one that never expires, and said nothing about it - the same silent widening this branch fixed for scope. The replacement now carries the replaced row's ExpiresAt. A token whose deadline has already passed is refused instead of rotated, since keeping the deadline would mint a dead token and dropping it would revive an expired credential without limit; the expired row is left untouched. --------- Co-authored-by: Sanaei <[email protected]>
  • 721de5adde Fix fragment exports for older Xray clients (#6702) * Fix fragment exports for older Xray clients * fix(link): tolerate a finalmask without tcp in panel share links withLegacyFragmentRanges called finalmask.tcp.map unguarded, but stored rows reach the link generator unparsed and dropEmptyFinalMask deletes an empty tcp list on save. Any VMess/VLESS/Trojan/SS inbound with only UDP masks or quicParams threw a TypeError in the QR, info and export-links views. The Go counterpart already skipped a missing tcp. Also trims the Go helper's comment to the two-line cap and drops a []string branch no JSON-decoded finalmask can reach. --------- Co-authored-by: Artem K <[email protected]> Co-authored-by: Sanaei <[email protected]>
  • a716122ef2 feat(ci): let the review bot read the discussion, the issue and xray-core The bot never read the replies under its own findings, so a finding a maintainer had already declined came back on the next `@claude review`. It now reads every comment and inline thread first: a maintainer's answer settles a finding for good, anyone else's is a claim checked against the code, and the summary gives each earlier finding a disposition. It also reads the issue the PR claims to fix and reports a partial fix. REVIEW.md asks for an upstream symbol behind every wire-format claim, but the job had no xray-core source (#6718's review said so). The module the base go.mod pins is now unpacked into a hidden dir in the base workspace; nothing from pr-head runs. REVIEW.md gains the rules only /senior-review carried: keep read, reproduced and inferred claims apart, evidence for performance findings, a traced trust boundary for security ones, and duplicated logic as a finding. The summary now says each inline finding in one line.
  • 8ea8f4bb61 fix(ci): stop the review bot naming where the fix belongs 65b9bfed narrowed the fix carve-out to "one clause naming WHERE the fix belongs", but the bot still closes every finding with that clause, and set beside the defect it already named, the location is the fix. On #6718 it listed the two capabilities the bounding set lacks, then wrote "The fix belongs in the capability bounding set". Drop the carve-out from REVIEW.md and the workflow prompt: the finding's file:line already says where.
  • ce221c33d0 fix(sub): carry REALITY ML-KEM hint in VLESS links (#6712) * fix(sub): carry REALITY ML-KEM hint in VLESS links Keep raw share links in parity with Clash subscriptions for Xray 26.9.8+. Preserve the URI hint through Go and frontend imports, expose it in the outbound editor, and update the documentation tooling. * fix(link): accept REALITY ML-KEM boolean aliases * test(frontend): isolate Happ preset notifications * fix(link): keep the ML-KEM hint out of Xray REALITY settings support-x25519mlkem768 is a Mihomo reality-opts option; xray-core's REALITYConfig (infra/conf/transport_security.go) has no such field and its JSON loader drops unknown keys silently. The PR also stored it as realitySettings.supportX25519Mlkem768 in Xray outbounds (form switch, Go and TS link import, docs outbound builders) and as an inbound settings default that is stripped before Xray and read by no link generator. The outbound switch therefore did nothing, and imported links carried a dead key into the JSON subscription. The share-link hint itself stays: Go, frontend and docs still emit support-x25519mlkem768=true on VLESS REALITY links and drop it on a TLS host override. --------- Co-authored-by: libmur-dev <333915961+[email protected]> Co-authored-by: MHSanaei <[email protected]>
  • View comparison for these 7 commits »

4 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 99bc68fa14 chore(deps): update project dependencies Refresh Go, frontend, and documentation dependencies, including MSW 3 and pnpm 12.8.1. Update the MSW test setup to use the renamed `onUnhandledFrame` option.
  • 62423cacd1 feat(xray): update xray-core to v26.9.30 and adapt panel Bump xtls/xray-core to b26a91de4f (v26.9.30) and the three binary pins (DockerInit.sh, release.yml Linux + Windows) in lockstep. No deleted symbols; the sing and sing-shadowsocks indirect deps drop out with the SS2022 rewrite. XDNS finalmask (#6718) replaced its string lists with objects: domains are {name, types, edns0, lenLimit, labelLimit} and client resolvers {type, settings.addr}. The loader no longer parses the old lists, so a single stored xdns mask keeps the whole core from starting. The new leaf package internal/util/maskcompat converts them: "name[:type]" becomes a domain and "name[:type]+udp://addr" a domain plus a udp resolver. A bare name maps to TXT, the type legacy clients queried by default, and each converted domain keeps the 1232-byte EDNS0 the old code always used (without it the server caps answers at 512). It runs from: - the XdnsFinalmaskObjectsFix seeder, over inbound streams, hosts, the xray template, the global sub-JSON mask and cached subscription outbounds; - inbound save (normalizeStreamSettings) and GetXrayConfig, for rows that never went through the seeder; - both link importers, since fm= from an older panel carries the lists. The finalmask form edits the object shape (every key needs a registered field, or the finalmask watch drops it on save) and lifts legacy masks on open. The udp-mask golden fixture moves to the object shape, which TestGoldenStreamFixturesBuildInXray now builds through the core. The wire format changed as well, so pre-upgrade clients need the new core. WireGuard outbound (#6771) dropped settings.domainStrategy and the remoteDNS "local" mode. The endpoint lookup now follows sockopt.domainStrategy and in-tunnel targets the outbound's targetStrategy. The old key is silently ignored, which undid the IPv4-first endpoint lookup the WARP outbound depends on (#5205), and "local" now panics the core at startup because remoteDNS goes through netip.MustParseAddr. The WireguardDomainStrategyFix seeder moves a stored family preference to both keys (a value already set wins) and turns "local" into targetStrategy; the outbound form lifts legacy rows the same way and drops its select, the WARP modal writes the new placement, and the inbound form loses a field the server never read. ValidateOutboundConfig now refuses a non-IP remoteDNS entry, which conf.Build() lets through, on template save and for outbound subscriptions. Noise finalmask items accept type "exp" (#6862), a tag expression. The form offers it for noise items only: header-custom items go through the core's PraseByteSlice, which refuses it. TUN gained autoSystemDnsToGateway (Linux) and autoSystemWfpBlockLeak (Windows). Both pass through the settings schema so a value set in JSON survives the next form save. MASQUE (inbound, outbound, transport) and the XDRIVE transport are new protocols the panel does not offer yet; their new loader refusals only cover configs the panel never generates. The FakeDNS IPv6 pool default, the SS2022 rewrite (same gRPC account; emails are now deduped case-insensitively, as the panel already does), the restored udphop interval default and the rest change no panel-facing config.
  • View comparison for these 2 commits »

4 days ago

txlyre synced and deleted reference dependabot/npm_and_yarn/frontend/tanstack/react-query-devtools-5.104.0 at txlyre/3x-ui from mirror

4 days ago

txlyre synced and deleted reference dependabot/npm_and_yarn/frontend/tanstack/react-query-5.104.0 at txlyre/3x-ui from mirror

4 days ago

txlyre synced and deleted reference dependabot/npm_and_yarn/frontend/react-hook-form-7.89.0 at txlyre/3x-ui from mirror

4 days ago

txlyre synced and deleted reference dependabot/npm_and_yarn/frontend/lint-staged-17.6.0 at txlyre/3x-ui from mirror

4 days ago

txlyre synced and deleted reference dependabot/go_modules/google.golang.org/grpc-1.86.0-dev at txlyre/3x-ui from mirror

4 days ago

txlyre synced and deleted reference dependabot/go_modules/github.com/shirou/gopsutil/v4-4.26.9 at txlyre/3x-ui from mirror

4 days ago

txlyre synced and deleted reference dependabot/go_modules/github.com/goccy/go-json-0.11.1 at txlyre/3x-ui from mirror

4 days ago

txlyre synced commits to dependabot/npm_and_yarn/frontend/tanstack/react-query-devtools-5.104.0 at txlyre/3x-ui from mirror

  • c696a7dbc1 chore(deps): bump @tanstack/react-query-devtools in /frontend Bumps [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) from 5.103.2 to 5.104.0. - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/[email protected]/packages/react-query-devtools) --- updated-dependencies: - dependency-name: "@tanstack/react-query-devtools" dependency-version: 5.104.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]>
  • 99047c0a63 fix(frontend): keep the given file name on mobile downloads FileManager typed every download text/plain. Android's MediaStore appends the MIME type's extension whenever the name's own extension maps elsewhere, so a subscriber saving a WireGuard config got peer.conf.txt, which the WireGuard app refuses; .json, .yaml and .log downloads were renamed the same way. Desktop browsers honour the download name, which is why only phones saw it. application/octet-stream carries no extension of its own, so the name the panel chose is kept.
  • aee45ca3fe fix(hosts): advertise Hosts in every WireGuard, AmneziaWG and TUIC config Invariant: an inbound's enabled Hosts are the endpoints every client config for it advertises, whichever surface renders that config. WireGuard and AmneziaWG broke it. Their raw generators ignored the externalProxy entries Hosts are injected as and always emitted resolveInboundAddress, so the raw subscription, the sub page .conf, the clients links API and "export all links" gave out the panel address while the JSON and Clash formats of the same inbound used the Host. advertisedEndpoints now states the fan-out once for mtproto, wireguard and amneziawg. The browser-built configs had the same gap. The Clients page WireGuard and AmneziaWG config blocks and QR panels, and its TUIC Clash config, used the panel hostname next to server links that already used Hosts; the Inbounds page peer configs, QR and export ignored them too. withMtprotoHostEndpoints becomes withHostEndpoints over a shared hostEndpointsFor mirror of the backend, the tunnel fan-outs render one config per Host, and the clients page waits for the hosts list the way the inbounds page does, so an empty list means "no hosts" rather than "not loaded yet".
  • 8c023d13dc docs(architecture): fix table padding flagged by oxfmt The NodePendingReset row added in 4210a50c had one extra space of padding, failing the Docs CI format check.
  • 823db05966 fix(inbounds): keep the stored client list and enable on inbound save Invariant: saving an inbound's configuration never changes which clients it holds nor whether it is enabled; both have their own endpoints. The edit modal posts back the clients and the enable flag it loaded when it opened. A client added meanwhile (another admin, the bot, the API, LDAP) was detached and its stats deleted; a client deleted meanwhile came back with its credentials, restoring access that had been revoked; an inbound switched off meanwhile was switched back on. For every save but a master's node-sync push, UpdateInbound now takes the client list and enable from the row it re-reads inside the writer; this replaces the lifecycle-only carry from the previous commit. Client validation (renewal schedule, Hysteria auth, TUIC credentials) moves after that swap so it judges the clients actually saved: a protocol switch keeps the stored clients, and #6268's refusal must apply to them. The edit form no longer loads or sends clients, so neither the JSON editor nor validation sees a copy the server ignores, and the enable switch shows only when adding; the list toggle (/setEnable) covers existing inbounds. Tests that added or re-keyed clients through a panel inbound save pinned the old rule; they now drive the master-push path, where payload clients still apply.
  • View comparison for these 10 commits »

5 days ago