txlyre

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

4 hours ago

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

4 hours ago

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

  • 74c3cc3882 fix(database): blank boolean-era verifyPeerCertByName values on hosts A host's verify_peer_cert_by_name must only hold a server name an operator typed. The column started life as a boolean, and the migration that was meant to discard those values runs after AutoMigrate, which has already cast the column to text: SQLite's table rebuild stores 0/1 as "0"/"1", and Postgres' USING col::text stores "false"/"true". The migration then sees a text column and leaves the values alone. Since 3.10.0 the JSON subscription lifts host TLS verification fields to the level xray reads, so that leftover "0" is now sent as the cert name to verify and clients such as Happ refuse the server certificate. A one-shot HostVerifyPeerCertByNameBoolFix seeder blanks exactly the four cast values on existing databases; real names are untouched. The Postgres variant joins the CI schema job, since that cast only happens there.
  • 6843583706 fix(xray): fall back to the userspace TUN where kernel TUN cannot be set up The core puts a WireGuard outbound on a kernel TUN whenever it holds CAP_NET_ADMIN and noKernelTun is unset, the default for a hand-added outbound. That needs a writable /proc/sys/net and /dev/net/tun, and where either is missing the core does not fall back: it aborts the whole start. The shipped docker-compose grants NET_ADMIN for fail2ban while Docker keeps /proc/sys read-only and exposes no /dev/net/tun, so one such outbound took every inbound down with the #6796 error; any unit or drop-in that keeps /proc/sys/net read-only did the same. xray.KernelTunBlocked probes once, from the panel process whose capabilities and mounts the core inherits, whether the core would pick a kernel TUN it cannot create. GetXrayConfig then sets noKernelTun on every WireGuard outbound, so cold starts and hot reloads both get it. Hosts that can create the device keep the kernel TUN. Verified with the pinned core's "run -test" on the generated config, in a NET_ADMIN container as docker-compose runs it and under systemd 257: the container and the v3.10.0 unit report blocked and pass on gVisor, the container fails with the #6796 error when the GetXrayConfig hook is bypassed, and the new unit keeps the kernel TUN. Refs #6796
  • 1f2738eda5 fix(systemd): let the WireGuard kernel TUN write rp_filter under the sandbox ProtectKernelTunables=yes (#6718) mounts all of /proc/sys read-only, and a WireGuard outbound in kernel-TUN mode zeroes net.ipv4.conf.all.rp_filter (and its ipv6 and per-device siblings) while Xray creates its handlers, so the core aborted with "failed to disable ipv4 rp_filter for all: ... read-only file system". The unit is reinstalled on every update, so a hand edit did not stick. Keep ProtectKernelTunables and reopen only /proc/sys/net. Turning the protection off (#6797) also lets the core start, but leaves kernel.core_pattern writable to the root panel, which turns any panel-level code execution into a way out of the whole sandbox. net.* is per network namespace and grants nothing beyond the CAP_NET_ADMIN the panel already holds. Verified on systemd 257 (Debian trixie) with the shipped unit and the pinned core: the v3.10.0 unit reproduces the error, the new one starts the kernel TUN, and kernel.core_pattern stays read-only for the service. Closes #6796
  • 097bb7646a fix(ci): move the issue analyst's static instructions out of the workflow GitHub evaluates a `with:` value holding ${{ }} as one expression and rejects the whole workflow when it passes 21,000 characters. The analyst prompt was 19,883; 6eeaaa91 took it to 21,389 and 99d971ff to 23,257, so since that push the file is invalid: no issue gets an analysis, and every push records a failed, job-less run named after the file. actionlint does not check this limit, which is how it shipped. The static parts - repository context pointer, issue forms, how to investigate, resuming and maintainer-request modes, evidence discipline, the severity/confidence/verdict scales, the security exception, labels and closing, and the comment format - move verbatim to .github/claude/issue-analyst-instructions.md, which the prompt tells the analyst to read in full first, as it already does for issue-analyst-context.md. The prompt keeps everything that needs an expression or guards the run: the trigger mode, CURRENT ISSUE, the fenced untrusted data, RULES and the marker. It is now 5,597 characters. The stale "code changes happen when the maintainer mentions @claude" rule is replaced: no workflow here changes code, and @claude analyze now exists. TestBotPromptsFitGitHubExpressionLimit in bot_context_test.go reads every claude-*.yml and fails on a ${{ }} value over the limit; it is red on 99d971ff (23,257). The instructions file joins the analyst paths pinned by TestAnalystContextNamesRealPaths, and the file's header now names everything it pins. actionlint 1.7.7 passes.
  • 544907f4e2 v3.10.0
  • View comparison for these 10 commits »

4 hours ago

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

  • c321933a54 perf: reduce traffic formatter allocations (#6790) Co-authored-by: Javier Guerrero <[email protected]> Co-authored-by: Copilot App <223556219+[email protected]>
  • d43e0c7ead fix(sub): default VLESS client encryption to none when the inbound has none stored (#6781) VLESS inbounds whose settings carry no "encryption" key (rows saved by panels before the field existed, or inbounds added over the API with only "decryption":"none", as the API docs example does) produced a JSON subscription outbound with "encryption": "". Xray refuses to load that config ("VLESS users: please add/set \"encryption\":\"none\" for every user"), so every VLESS entry of the JSON subscription was unusable. The share link left the parameter out instead. Treat a missing or empty value as "none" for both outputs, matching the frontend inbound schema default and the docs link generator.
  • View comparison for these 2 commits »

12 hours ago

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

  • f55938ba0a fix(amneziawg): respect tunnel MTU when writing UDP replies (#6780) * fix(amneziawg): respect tunnel MTU when writing UDP replies Send replies through gVisor FindRoute/Route.WritePacket instead of WriteRawPacket so the network endpoints apply the tunnel MTU and fragment oversized IPv4/IPv6 datagrams. Preserve UDP checksums including the zero-complement IPv6 case and validate lengths before narrowing to uint16. Regression tests inspect and reassemble actual channel-backed TUN output at and above the MTU, verify checksums and exercise concurrent sources. * docs(review): put the tally before Design and name who can store a trigger
  • a4699696ef docs(review): put the tally before Design and name who can store a trigger
  • 5939fda397 fix(ci): let the PR review save its body, and scope the write-check bypass Run 38187893539 reviewed PR #6784 in full but posted nothing: the model's Write to /tmp/review.md was refused. Claude Code checks file permissions against Edit(path) rules only and never consults a Write(path) rule, so the allow rule was dead - and so were the Write(//home/**) and Write(//opt/**) deny rules meant to keep $GITHUB_ENV out of reach. All three are Edit rules now; a local headless run confirms Write(path) is refused, Edit(path) lets the file through, and an Edit deny still blocks a Write under it. allowed_non_write_users="*" skipped the action's write-permission check for every actor and warned each run. Only a pull_request_target from a fork needs it; an `@claude review` requester is already OWNER/MEMBER/COLLABORATOR and passes the action's own check.
  • 9130f6dd75 feat(ci): make the PR review judge the claim and never post code The review only hunted defects: a correct fix got no word on whether the reported problem was real or actually solved, and a feature got no design feedback. The comment now opens with a verdict for every kind of change - for a fix, whether the failure exists on base, whether the head removes its root cause (checked against the issue-analyst's analysis) and whether the test proves it; for a feature, whether it overlaps what the panel already has, which deployment shapes it misses, and a prose-only Design section; for refactor, perf, dependency bumps, docs and CI, a question of their own. The trace scales to the change, so a typo is not traced like a runtime change. It also searches for duplicate merged or in-flight work, and runs at high effort since settling whether a bug is real takes tracing base and head. No code may appear anywhere in the comment, and the model no longer holds a write token. The workflow is three jobs: `prepare` (ack, pin the head, skip a reviewed PR), `review` (read-only token; the model writes /tmp/review.md) and `post` (no model, no checkout), which strips any fenced block, keeps the Reviewed head marker the skip relies on, and only then posts one plain comment. Before, `gh api` with a pull-requests: write token could touch any PR in the repository, `sed -i` could write $GITHUB_ENV, and the inline-comment tool told the model to use suggestion blocks. Concurrency stays on the review job: at workflow level any unrelated comment on the PR would replace a pending review in the queue. Restores three guards the earlier claude-bot review had: all author-controlled input, including text inside the checked-out code, is data and a diff adding text aimed at a reviewer or bot is a HIGH finding; a blocking finding tags the repository owner; and a diff too large to read whole has a reading order and must name what went unread.
  • dbfa369632 fix(frontend): close panel update confirmation before progress (#6766) * fix(frontend): close panel update confirmation promptly * chore(ci): rerun flaky checks
  • View comparison for these 21 commits »

21 hours ago

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

  • c7d89ceac2 feat(outbounds): TLS for XDRIVE outbounds xray-core 26.10.10 dials an XDRIVE outbound's storage API with the stream's tlsSettings: their SNI, uTLS fingerprint and ALPN are what the API sees, which is the point of fronting it. canEnableTls kept XDRIVE out of the TLS-capable networks, so the security picker never offered TLS and switching a TLS outbound to XDRIVE silently reset it to none. canEnableOutboundTls admits a TLS-eligible protocol on xdrive and backs both the picker and the network-change cascade. Inbounds keep the plain predicate: their tlsSettings carry server certificates, not a client hello. Also restores the plain oxfmt / oxlint --fix lint-staged commands, reverting da6872a3: that hook change was not wanted.
  • ef4bda4e06 feat(inbounds): TUN adapter type and auto-system options xray-core 26.9 added three TUN settings the inbound form could not set: - desc, the tunnel type a new Wintun adapter is created with on Windows. The settings schema did not list it, so the parse stripped it and a value entered as JSON vanished on the next save; - autoSystemDnsToGateway (Linux), which points systemd-resolved at the TUN gateway; - autoSystemWfpBlockLeak (Windows), whose dns / misconfigtun filters keep traffic from leaking past the TUN. The tooltips name the platform each one acts on and what it needs, since the core refuses to build or start the TUN otherwise.
  • b6c859f5b9 feat(xray): edit Lua DNS and routing scripts xray-core 26.10.10 can hand DNS and routing to Lua: dns.script and routing.script name a .lua file (absolute, or found next to the Xray binary) that defines HandleDNSQuery / HandleRoute, and a DNS server's id is how a DNS script picks it. The keys round-tripped through the JSON template but had no field. - DNS: a Lua Script field, and an ID field in the server modal; - Basics: a Routing Script field beside the routing strategy; - Routing: a warning above the rules while a script is set, since the core then routes only through the script and skips every rule, the panel's api rule included. The descriptions say the script must still send the api inbound to the api outbound, and that Xray will not start if the file is missing. An empty field drops the key, because the core rejects an empty path.
  • 1c050f872a feat(finalmask): header-custom variables, sudoku UDP mask and noise reset ranges xray-core 26.6 grew the finalmask surface the editor could not express, and the finalmask watch drops any key without a registered field, so a config written by hand lost these keys on the next save: - header-custom items can capture their bytes into a variable, reuse one or emit a transform expression; a Kind select keeps each item to exactly one kind, as the core requires, and only parsed JSON reaches transform; - TCP header-custom gains the errors group (the reply sent when a client group does not match), UDP header-custom its prefix/standalone mode; - sudoku is offered as a UDP mask, and share links keep it in fm=; - the noise reset is an Int32Range, so it now takes "60-120" as well. The new golden fixture is saved unchanged by the outbound editor and built by xray-core in TestGoldenStreamFixturesBuildInXray.
  • View comparison for these 4 commits »

1 day ago

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

  • ec0a57fc4f fix(finalmask): hop client UDP through the udphop mask xray-core 26.9.9 dropped finalmask.quicParams.udpHop for the client-only "udphop" UDP mask {mode, interval, remoteIPs, remotePorts}. The panel kept writing the old key, so hysteria/xhttp outbounds and JSON-subscription clients silently stopped hopping ports. An inbound keeps quicParams.udpHop: it is panel metadata advertised as the link's mport, and a udphop mask on a listener fails with "udphop: client only". The conversion (maskcompat.UpgradeLegacyUDPHop, intervalRemote mode) runs only where a client config is produced: - UDPHopClientMaskFix seeder over hosts, sub-JSON finalmask, cached outbound-subscription outbounds and template outbounds (never inbounds); - GetXrayConfig heals template outbounds at build time; - the JSON subscription upgrades each generated client stream; - the Go and frontend link importers upgrade a legacy fm= param. The finalmask editor gains a side prop: the client side lifts the dead key on mount and offers a udphop mask editor (also for xhttp's HTTP/3 dial); the inbound side keeps the UDP Hop switch, labelled as advertised.
  • 00aaef386f fix(inbounds): accept masque in the inbound protocol validator The inbound add and update handlers bind the body into model.Inbound and validate Protocol against a oneof list that predates MASQUE, so every save of a MASQUE inbound from the panel or the API failed validation even though the rest of the stack (b04039c2) handles the protocol. Add masque to the list and regenerate the schemas and OpenAPI document that mirror it.
  • da6872a3b8 chore(frontend): let lint-staged pass commits that stage only generated files lint-staged hands every staged src/**/*.ts file to oxfmt and oxlint, but both ignore src/generated, so a commit whose only TypeScript change is a regenerated schema (a validator tag edit plus make gen) aborts with "Expected at least one target file". Tell both tools an empty selection is not an error.
  • 04cccef311 feat(warp): register WARP over MASQUE from the WARP modal xray-core's masque transport reaches Cloudflare WARP with an enrolled ECDSA P-256 key, the endpoint's public key and the assigned tunnel addresses, which only a WARP enrollment hands out. A new regMasque action on /panel/api/xray/warp/:action registers a separate WARP device and enrolls a secp256r1 key for the masque tunnel (the API revision and client version the WARP apps enroll MASQUE keys with). Enrolling replaces a device's WireGuard key, so it never touches the stored WireGuard registration. It returns the PEM key pair, the addresses and the endpoint, and the WARP modal turns them into a warp-masque outbound (HTTP/3, Cloudflare's MASQUE SNI), or refreshes the one already there.
  • b04039c2bd feat(xray): add the MASQUE protocol and transport xray-core v26.10.10 serves MASQUE: CONNECT-IP over HTTP/3 or HTTP/2, always behind TLS, as an inbound and an outbound protocol riding a dedicated "masque" transport. The panel could not create either. Inbound: MASQUE is a multi-user protocol whose users log in with their email and password over Basic auth. The panel keeps the client's "password" like trojan and GenXrayInboundConfig hands it to the core as "pass", so the full-config path, the live AddInbound path and node pushes all emit what the core reads; one user with an empty pass makes the core refuse the whole inbound. Live user adds build a masque.Account from either key, client-only changes diff per user so a new client does not drop every connected tunnel, and client creation, copy, validation and edits key on the password. The address pool and MTU get form fields, TLS is mandatory, and the port check follows the core's listener choice: HTTP/2 on TCP when the ALPN offers h2, HTTP/3 on UDP when it offers h3 or not h2. Nodes may host MASQUE from v3.9.1, the first panel release that knows to rename the password. MASQUE has no share-link format, so links and Clash skip it; the JSON subscription builds the client's masque outbound with its email and password, and the subscription allowlist now lets a subId see MASQUE inbounds at all. The IP-limit job, client pages and traffic tracking list the protocol. Outbound: the form edits the server, remote DNS, and the transport's host, path, Basic credentials, headers and WARP block (enrolled key, endpoint public key, tunnel addresses), which xray-core refuses next to user/pass. The outbound test probes it through the core, since its default HTTP/3 dial is UDP.
  • View comparison for these 9 commits »

1 day ago

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

  • 442b7fb163 chore(deps): bump github.com/valyala/fasthttp from 1.74.0 to 1.75.0 (#6767) Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.74.0 to 1.75.0. - [Release notes](https://github.com/valyala/fasthttp/releases) - [Commits](https://github.com/valyala/fasthttp/compare/v1.74.0...v1.75.0) --- updated-dependencies: - dependency-name: github.com/valyala/fasthttp dependency-version: 1.75.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • f0016f6cf9 test: stop swapping http.DefaultTransport in the Happ network test (#6742) * test: stop swapping http.DefaultTransport in the Happ network test The Happ local-generation test replaced the http.DefaultTransport global to refuse sockets. resolvePublicIPsInBackground fires a fire-and-forget resolver (ServerService.GetStatus -> resolvePublicIPs -> getPublicIP), and that goroutine can outlive the test that scheduled it, so a later test writes the global while the resolver is reading it: a data race that `make race` (-shuffle=on) only reports when the status test happens to run first. Route the lookup transport through an atomic swap point and count in-flight lookups, so the test stubs its own path and waits for earlier resolvers to settle before installing it. That also stops another test's dials from landing in this test's zero-network tally. Reproduced on the parent commit with -shuffle=6 (read in getPublicIP, previous write in TestHappGenerateLocallyWithoutNetwork); the same seed is clean with this change, across repeats and for the whole package under -race. * test: prove the Happ network guard intercepts panel egress The PR review pointed out that HappService.Generate never reaches a stub installed only on the public-IP lookup, so the "zero network attempts" assertion could not fail. Move the override onto the panel's shared egress seam — getPublicIP and SettingService.NewProxiedHTTPClient — and gate the test on a canary request that must be refused and must move the tally, so a future outbound call from local generation is reported instead of passing unnoticed. * test(service): stop the cold-status test leaking a public-IP resolver The race CI hit (run 37345781756) came from TestCurrentStatusSamplesBeforeFirstTick: its CurrentStatus call starts resolvePublicIPsInBackground, a goroutine that makes real internet requests and outlives the test. On a runner without IPv6 every lookup waits out its 3s timeout, so the goroutine is still building clients from http.DefaultTransport when the Happ test swaps it. Pre-settling the IP cache keeps that test from starting the resolver at all, so no unit test reaches the internet or leaks the goroutine. This replaces the earlier panelEgressTransport / panelEgressLookups seam, which added test-only hooks to production code without removing the leak. Reproduced in golang:1.27-bookworm with egress routed to a blackhole (HTTPS_PROXY=http://10.255.255.1:9, -race -count=3 on the two tests): the same server.go:406 race as CI before, clean over -count=5 after. --------- Co-authored-by: MHSanaei <[email protected]>
  • 881f646d2c fix(clients): balance renewal form layout and mode guidance (#6672) * fix(clients): balance renewal form layout and mode guidance Move renewal out of the traffic-reset half-column so its help and preview no longer leave a tall empty area beside the reset selector. Keep the mode, active schedule field and renewal limit in equal desktop columns, stack them on small screens, and preserve the bulk form's stacked layout. Keep monthly traffic-reset controls together in their own row and allow the first-cutoff button to wrap without shrinking its click target. Show fixed-duration guidance only for interval renewal and the month-end boundary note only for monthly renewal. Synchronize all thirteen locales and the client docs, remove obsolete zero-value mode-switching tooltips, and cover the help transitions through the real client form. Keep preview markup and existing monthly copy intact so the independent date-display work in #6662 can merge without overlapping these edits. This is presentation-only: saved values, expiry selection, renewal and traffic-reset calculations, APIs and database formats are unchanged. * refactor(clients): drop the renewal CSS that #6662 made redundant #6662 landed the full-width preview Space and the wrapping first-cutoff Button as inline styles, so ClientRenewalFields.css and its wrapper div styled the same two elements a second time; both are removed. * test(tuic): wait for downstream association activity refresh The UDP lifecycle regression can receive its QUIC response before the server's sending goroutine updates the association timestamp. Reading the protected field once then mistakes a valid scheduling order for a missing refresh, intermittently failing the race-enabled CI job. Wait for lastActive to advance under the registry mutex, bounded by a one-second deadline, before checking idle reaping. Retain the payload, association ID, idle-survival and shutdown assertions so a missing activity update still fails the test. This only changes test synchronization. Production relay behavior, renewal logic, network timeouts and CI configuration remain unchanged. --------- Co-authored-by: JacktheRanger <219502738+[email protected]> Co-authored-by: MHSanaei <[email protected]>
  • View comparison for these 3 commits »

3 days ago

txlyre synced and deleted reference dependabot/go_modules/github.com/valyala/fasthttp-1.75.0 at txlyre/3x-ui from mirror

3 days ago

txlyre synced commits to dependabot/go_modules/github.com/valyala/fasthttp-1.75.0 at txlyre/3x-ui from mirror

  • 359baaaca4 chore(deps): bump github.com/valyala/fasthttp from 1.74.0 to 1.75.0 Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.74.0 to 1.75.0. - [Release notes](https://github.com/valyala/fasthttp/releases) - [Commits](https://github.com/valyala/fasthttp/compare/v1.74.0...v1.75.0) --- updated-dependencies: - dependency-name: github.com/valyala/fasthttp dependency-version: 1.75.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]>
  • 257559c389 fix(update): leave TLS alone during panel updates The panel's web Update runs update.sh with stdin on /dev/null. On a panel with no certificate configured, config_after_update opened the SSL menu: read got EOF, the empty answer selected "Let's Encrypt for IP", x-ui was stopped and IP-certificate issuance ran, and its failure path rm -rf'd /root/cert/ip and the acme.sh state, deleting a pair other services may share. The URL printed afterwards was always https:// even when nothing was configured. When the public-IP lookup failed, the same run instead looped forever on an "enter your server IP" prompt it could never answer. An update now only updates the panel: the SSL setup path and the helpers only it used are removed from update.sh, the IP lookup falls back without prompting (it is only used for the printed URL), and the URL reports the scheme the panel actually serves. TLS stays configurable from the x-ui menu and install.sh. Supersedes #6667.
  • f217212247 web: overlay WireGuard peer public key from the inbound (#6751) The WireGuard branch now copies PublicKey from the matching inbound settings client. When that entry's KeepAlive is nil the peer keepalive is cleared, and when it is set the integer is copied into a new pointer. The dual-tunnel test seeds different keys and keepalives, checks that the shared row still has the AmneziaWG identity, and expects the emitted WireGuard peer to use the WireGuard public key and keepalive, including a case where the WireGuard settings entry has no keepalive and the peer omits it. Fixes #6731
  • 54d1712544 fix(clients): clarify renewal preview times and estimates (#6662) * fix(clients): clarify renewal preview times and estimates The preview rendered RFC3339 strings directly and labeled the server's Local zone without explaining which clock it represented. Its renewal count also looked like a record of renewals already performed. Display readable dates with an explicit UTC offset while preserving the server's wall time, per-date DST offset and fractional cutoff precision. Explain the panel/server timezone and read-only preview, and label the renewal count as estimated allowance consumption in all 13 locales. Let the first-cutoff button wrap on narrow screens and update the English and Chinese client documentation plus display/form regression coverage. This changes presentation only: renewal calculations, stored expiry values, API payloads and first-use behavior remain unchanged. * test(clients): remove redundant renewal preview copy checks The locale-only preview case repeated the Local timezone branch without covering a distinct behavior. Exact textContent comparisons after matching the same text with getByText also only pinned constant wording, contrary to the repository's testing guidance. Remove that duplicate case, its locale setup and the redundant copy assertions. Keep the UTC, per-boundary DST offset, fractional precision and original datetime checks, together with the existing real form-save tests. Production rendering, translations and renewal behavior are unchanged. --------- Co-authored-by: JacktheRanger <219502738+[email protected]>
  • b3f1cced70 fix(tuic): preserve IP destinations during sniffing (#6753) Keep sniffed TLS domains available for routing without replacing the destination address requested by the TUIC client. Add a regression assertion for the generated relay sniffing settings.
  • View comparison for these 10 commits »

4 days ago

txlyre synced new reference dependabot/go_modules/github.com/valyala/fasthttp-1.75.0 to txlyre/3x-ui from mirror

4 days ago

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

  • 257559c389 fix(update): leave TLS alone during panel updates The panel's web Update runs update.sh with stdin on /dev/null. On a panel with no certificate configured, config_after_update opened the SSL menu: read got EOF, the empty answer selected "Let's Encrypt for IP", x-ui was stopped and IP-certificate issuance ran, and its failure path rm -rf'd /root/cert/ip and the acme.sh state, deleting a pair other services may share. The URL printed afterwards was always https:// even when nothing was configured. When the public-IP lookup failed, the same run instead looped forever on an "enter your server IP" prompt it could never answer. An update now only updates the panel: the SSL setup path and the helpers only it used are removed from update.sh, the IP lookup falls back without prompting (it is only used for the printed URL), and the URL reports the scheme the panel actually serves. TLS stays configurable from the x-ui menu and install.sh. Supersedes #6667.

4 days ago

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

  • f217212247 web: overlay WireGuard peer public key from the inbound (#6751) The WireGuard branch now copies PublicKey from the matching inbound settings client. When that entry's KeepAlive is nil the peer keepalive is cleared, and when it is set the integer is copied into a new pointer. The dual-tunnel test seeds different keys and keepalives, checks that the shared row still has the AmneziaWG identity, and expects the emitted WireGuard peer to use the WireGuard public key and keepalive, including a case where the WireGuard settings entry has no keepalive and the peer omits it. Fixes #6731
  • 54d1712544 fix(clients): clarify renewal preview times and estimates (#6662) * fix(clients): clarify renewal preview times and estimates The preview rendered RFC3339 strings directly and labeled the server's Local zone without explaining which clock it represented. Its renewal count also looked like a record of renewals already performed. Display readable dates with an explicit UTC offset while preserving the server's wall time, per-date DST offset and fractional cutoff precision. Explain the panel/server timezone and read-only preview, and label the renewal count as estimated allowance consumption in all 13 locales. Let the first-cutoff button wrap on narrow screens and update the English and Chinese client documentation plus display/form regression coverage. This changes presentation only: renewal calculations, stored expiry values, API payloads and first-use behavior remain unchanged. * test(clients): remove redundant renewal preview copy checks The locale-only preview case repeated the Local timezone branch without covering a distinct behavior. Exact textContent comparisons after matching the same text with getByText also only pinned constant wording, contrary to the repository's testing guidance. Remove that duplicate case, its locale setup and the redundant copy assertions. Keep the UTC, per-boundary DST offset, fractional precision and original datetime checks, together with the existing real form-save tests. Production rendering, translations and renewal behavior are unchanged. --------- Co-authored-by: JacktheRanger <219502738+[email protected]>
  • b3f1cced70 fix(tuic): preserve IP destinations during sniffing (#6753) Keep sniffed TLS domains available for routing without replacing the destination address requested by the TUIC client. Add a regression assertion for the generated relay sniffing settings.
  • View comparison for these 3 commits »

5 days ago

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 »

5 days 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 »

6 days 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 week 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 week 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.

1 week ago

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

1 week ago

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

1 week ago