mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-06 21:25:04 +03:00
e535df03745c5ad003ae48e812024eb5fdc2124d
583 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
73a028f52d |
feat(xray): update xray-core to v26.10.10 and adapt panel
Move the three Xray binary pins (DockerInit.sh, both release.yml
fetches) to v26.10.10, in lockstep with the xtls/xray-core module that
|
||
|
|
66234315e4 |
fix(panel): default TLS ALPN to http/1.1 for WebSocket transport (#6787)
* fix(panel): default TLS ALPN to http/1.1 for WebSocket transport WebSocket bootstraps over HTTP/1.1: leaving ALPN at the schema default ['h2','http/1.1'] makes the server negotiate h2 and the WS handshake fails. See issue #6782. * style(frontend): keep comments within the two-line limit * style(frontend): keep comments within the two-line limit |
||
|
|
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+JacktheRanger@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
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+JacktheRanger@users.noreply.github.com> |
||
|
|
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 <ho3ein.sanaei@gmail.com> |
||
|
|
d4a7086c4e |
fix(inbounds): list clients in the detach/attach modals when they mount open (#6736)
* fix(inbounds): list clients in the detach modal when it mounts open DetachClientsModal seeded its synced-source state with the open source, so a modal first mounted with open and a source saw no change on its first render and never read the client rows. The table stayed empty until the modal was closed and reopened. Seed the state with null so the first open render loads the rows, as the reset-during-render check already expects. The test mounts the modal already open and fails without this change. Fixes #6733 * fix(inbounds): list clients in the attach modal when it mounts open AttachClientsModal seeds its synced-source state the same way DetachClientsModal did, so a modal first mounted with open and a source saw no change on its first render and never read the client rows. The table stayed empty, and nothing was pre-selected, until the modal was closed and reopened. Seed the state with null here too. The test mounts the attach modal already open and fails without this change. Refs #6733 --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
d1b60799ec |
fix(frontend): show toasts through message.useMessage, never the static API
Invariant: an antd toast lives inside the React tree of the component that raised it, so it takes the theme from ConfigProvider and unmounts with it. Four components called antd's static message.* (Happ settings, TUIC form, routing tab, command palette); the other 40 use message.useMessage. A static toast renders in a detached root that RTL's cleanup never unmounts, so its rAF-driven close timer kept updating state for ~3s after a test file ended. happ-routing-editor.test.tsx logged 419 act() warnings, 226 of them after its last test; on a slow runner the worker closed first and vitest failed the job with "Closing rpc while onUserConsoleLog was pending" (CI runs 37075337071, 37303631843). In the product the same toasts ignored the dark/ultra theme. The command palette keeps its holder mounted while closed: restartXray closes the palette before its success toast. The Happ editor test now asserts its toast unmounts with the component (red on the static API), and no-static-message.test.ts fails on any static call in src. The spy that silenced message.success in happ-settings-presets.test.tsx intercepted nothing after the change and is gone. |
||
|
|
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. |
||
|
|
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 <ho3ein.sanaei@gmail.com> |
||
|
|
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. |
||
|
|
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". |
||
|
|
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. |
||
|
|
1110caaa65 |
fix(inbounds): keep stored client lifecycle and counters on inbound save
Invariant: saving an inbound's configuration never changes a client's enable, expiry, quota or renewal state, nor the inbound's own traffic counters. The inbound modal posts back the whole settings.clients list it loaded when it opened, and UpdateInbound stored it, synced it into the client records and wrote it into client_traffics. Any client renewed, depleted or reset while the modal was open was reverted - a renewed client came back disabled with its old expiry. The row itself was read before the serialized tx and saved whole, so traffic the poll added in between was rolled back and a depleted inbound could be re-enabled. UpdateInbound now re-reads the row inside the writer and, for clients the inbound already holds, keeps enable, expiryTime, totalGB, reset, resetDay, resetWeekday and resetMax from it; those change through the client endpoints. A master's node-sync push stays authoritative. Existing clients' lifecycle fields are no longer editable through the inbound JSON editor or /inbounds/update, which the API docs now state. |
||
|
|
c8a182b6fb |
fix(wireguard): reject allowedIPs that overlap another client's range
xray's WireGuard inbound credits a packet to the first peer whose allowedIPs
contain its source address, and routes replies by the same table. The panel
only rejected an allowedIPs entry that was string-equal to another client's,
so a pre-assigned address typed in interface notation (10.10.2.9/24, as other
WireGuard tools export it) was accepted and claimed the whole /24: other
clients' traffic and online IPs were credited to that one email, and replies
went to a peer with no endpoint ("no known endpoint for peer").
The collision check now compares masked ranges on every path that uses it:
add, edit, the cross-inbound recheck inside the write transaction, and
AmneziaWG. Auto-allocation skips any address inside a prefix another client
holds. A /0 default route still claims nothing, as before, because legacy
migrated peers carry one. Clients already saved with overlapping ranges keep
working as they do today until edited. The API docs describing the error are
updated, including the stale claim that cross-inbound duplicates are accepted.
Closes #6623
|
||
|
|
09617f04f5 |
feat(panel): let a sponsor slot start at a scheduled time
sponsors.json only had an end date, so a booked placement had to be added to the file on the day it started. An optional `from` now hides a sponsor (and its logo) until that instant; entries without it show immediately as before. |
||
|
|
8f47b53879 |
style(settings): fold the Happ settings into four tabs
Seven tabs, several holding one or two settings, made the page hard to scan. The encrypted-links switch moves above the tabs under auto-detection, the colour profile joins the banners under Appearance & Theme, and Android per-app proxy joins Network & TUN Engine. The QR modal's settings link no longer needs a happTab selector, and the three orphaned tab-label keys are dropped from every locale. |
||
|
|
a33b2341e9 |
style(clients): put Traffic Reset and Auto renewal on one row
The renewal block took a full-width column, pushing Traffic Reset onto its own line. Each now gets a half-width column like the other fields, with its follow-up inputs stacked under it. |
||
|
|
7aa5fc085f |
feat(tgbot): access levels and /start account binding (#6518)
* feat(tgbot): gate the bot behind three user levels Every Telegram account that found the bot could run /help, /status and /usage, and tap any client button it could forge: nothing separated an account no admin had bound from a customer. Each update now resolves to stranger, client or admin, and commands are allowlisted per level so a command added later stays admin-only until it is listed. A stranger may run /start and /id only, and /start answers with the ChatID an admin needs to bind it; a stranger's callbacks are answered and dropped. Client detection reads the same tgId lookup as clientOwnedByTgUser, so the level gate and the ownership check agree. The bot also ignores everything outside private chats: authorization keys on the sender while wizard state keys on the chat, and the two are the same identity only in a private chat. * feat(tgbot): bind Telegram accounts through /start deep links Linking a customer meant the customer sending /id and an admin copying the ChatID into the client by hand, which does not scale past a few customers and is easy to get wrong. The admin client card now offers an invite link, t.me/<bot>?start=<subId>, and the first account to open it is bound through the existing SetClientTelegramUserID. A subId already grants the subscription, so binding gives the holder nothing the token did not. A subscription that spans several clients binds all of them, and is refused if any part belongs to another account; re-opening your own link is idempotent. Unknown and already-claimed tokens share one reply, so the link cannot be used to probe for valid subIds. * fix(tgbot): harden invite claims after review Review of the access-level and binding change found five problems: - Concurrent claims of one link all read the client as unbound, all bound and all were told so, while only the last write held. Resolving and binding now share one lock, and a bind that fails part-way through a multi-client subscription undoes the bindings it already made. - A subId has no minimum strength and the bot needs only its public username, so /start was an unthrottled guessing oracle. Non-admin claim attempts are capped at five per account per hour, the first refused one notifies the admins, and the Subscription ID field now says it doubles as the bot invite code. - levelOf expanded every inbound's client JSON on every non-admin update. It now reads the indexed tg_id column of the clients table. - A button tapped in a group chat was dropped unanswered and kept spinning, with nothing logged. It is answered now, and each ignored chat is logged once. - The subId was pasted raw into the t.me link, so '#' or '&' truncated it and Telegram rejects anything outside A-Za-z0-9_-. The payload is now base64url, and a subId too long for the 64-character limit is refused. * fix(tgbot): answer group chats again and make the claim race test bite ignoredChat dropped every non-private chat because wizard state was keyed by chat while authorization keyed on the sender. #6604 on main re-keyed that state by (chat, user) so admins can drive the bot from a group, so after the merge the drop only took the whole bot away from those admins, report keyboards sent to a group included. The level gate already keys on the sender, so group chats need no special case. TestConcurrentClaimsBindOnlyOneAccount passed with inviteClaimMu removed: the first claimant took the pool's idle connection and bound before the rest had opened theirs, so no two ever raced. It now holds the inbound write the binds need until every claimant has resolved, and fails without the lock ("6 accounts told they bound"). TestCommandAllowed restated the commandsByLevel map; TestGateCommand drives the same allowlist through gateCommand. TestIgnoredChat goes with the code it pinned. * docs(tgbot): document access levels and invite links The command table still said /help and /status answer anyone. An account no admin has linked now reaches only /start and /id, and a customer is linked through the client card's Invite Link, whose token is the Subscription ID. Updated in en, fa, ru and zh. --------- Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
6f40a75909 |
feat(inbound): excludeFromSub hides links without disabling (#6463)
* feat(inbound): excludeFromSub hides links without disabling Add a per-inbound flag that omits subscription output while keeping the inbound enabled for Xray, auth, and traffic accounting. Fixes #6435. * fix(inbound): excludeFromSub review follow-ups gofumpt model.go, sync docs OpenAPI, keep excludeFromSub master-authored on node mirror, and exercise the legacy add-column migration path in tests. * fix(sub): keep excluded inbounds' clients in the usage header The excludeFromSub filter sat in getInboundsBySubId's SQL, so an excluded inbound's clients never reached seenEmails in the raw, Clash or JSON renderer. A client that lives only on a hidden inbound (one client per inbound sharing a subId) dropped out of the Subscription-Userinfo usage, quota and expiry and out of the info-node state, while the inbound kept serving it and counting its traffic. The query returns every enabled inbound again; each renderer skips an excluded inbound's links but still counts its clients, the same rule the Clash renderer already applies to external links it cannot express. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
33a469315a |
feat(clients): preserve traffic counters in portable export/import (#6469)
* feat(clients): preserve traffic counters in portable export/import ExportAll now attaches client_traffics up/down (plus resetCount and last-seen fields) on each portable payload, and ImportClients restores them only for newly created emails so skipped/existing clients keep their live counters. Fixes #5858. * fix(clients): restore imported traffic only onto rows the import created Review of the portable-traffic export/import (#5858) found four defects: - An orphan's restored row was hand-built, dropping reset_weekday and forcing enable=true; a row kept by a keepTraffic delete kept the old client's limits. depletedClientsClause then matched a weekly-renewing over-quota orphan and DelDepleted deleted it. Orphan rows now go through AddClientStat, whose upsert refreshes config and keeps counters, so the unused traffic.total field is dropped from the export. - Created clients were inferred from Skipped emails, so a duplicate email in the file left the created copy with zero counters. bulkCreate now reports which payloads inserted a record, and only those are restored. - Each client took its own serialized-writer commit: 2000 clients spent 3.66s instead of 0.52s. Counters now apply in batched transactions (0.51s). - importClients discarded needRestart when the late restore step failed after clients were committed; it now flags and notifies first, as create already does. The /clients/export and /clients/import API docs now describe traffic. * fix(groups): keep imported traffic out of group totals Group totals keep a deleted client's usage (#5675), and the portable import restores that same usage onto the re-created client. Export, delete, re-import therefore counted it twice in ListGroups, and a fresh panel showed the migrated usage as consumption of its groups. Restored counters are usage from before the import, so the import now shifts each group's baseline up by what it restored, in the same transaction. A group total no longer moves at import time; only traffic consumed afterwards counts. The baseline shift reuses the #5675 helper, now signed. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
71e38367c1 |
feat(sub): add Incy app-management parameters (#6650)
* feat(sub): add Incy app-management parameters The panel already pushes a set of Happ headers, but INCY documents its own lowercase header names and its own value domains, so a Happ-shaped payload gets ignored by the client (per-app mode is bypass|proxy, not on|bypass, and per-app-proxy-enable has no Happ counterpart at all). Add a sibling Incy path that emits exactly the documented headers. Covered, per https://docs.incy.cc/en/app-management/: - profile-description, sort-order, support-email, announce-url, premium-url - banner text/button/URL and the two hex colours - hide-url, hide-check, no-limit-enabled - per-app split tunnelling (enable/mode/list) - TCP fragmentation (enable/length/interval/packets) - UDP noise packets (enable/type/packet/delay) - DoH pre-resolution (enable/domain/IP) Each string setting is tri-state: an empty value omits the header, so an untouched panel never overrides the subscriber's own choice in the app. Values are validated against the documented domains and dropped when they do not match, and non-ASCII text is base64-wrapped the way the docs require for Cyrillic. INCY identifies itself as INCY/<version>/<platform>, which gates the headers behind the same auto-detect switch the Happ path uses. Headers the panel already emits for every client (Profile-Title, Support-Url, Profile-Web-Page-Url, Announce, Profile-Update-Interval, Subscription-Userinfo) and Incy's routing line are left as they are. The Premium API (theme, defaultPingProtocol, fallbackHosts, ...) is a separate encrypted endpoint and stays out of scope here. * fix(sub): keep Incy per-app list entries separate on the wire The Incy settings textarea takes one package per line, as Incy documents for per-app-proxy-list, but the header path ran the value through sanitizeHeaderValue, which deletes CR/LF. "com.google.chrome\norg.telegram.messenger" reached the client as the single bogus package "com.google.chromeorg.telegram.messenger", so per-app split tunnelling silently matched no app. Join comma- or line-separated entries as CSV instead. Also drop three tests that could not fail: TestIncyExcludesHappOnlyHeaders (ApplyIncyHeaders has no path that emits Happ headers, and the non-Happ UA gate is already pinned by TestApplyHappHeaders_Gating) and two UI tests that only asserted updateSetting received the key the JSX passes it. --------- Co-authored-by: DIMFLIX <dimflix@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
bd9ccde1f4 |
feat(sub): make external subscription fetch User-Agent configurable (#6613)
* feat(sub): make external subscription fetch User-Agent configurable Some providers reject fetches that do not send a known client User-Agent. Expose externalSubUserAgent as a panel setting (default v2rayNG/1.8.5) and use it when fetching client external subscription URLs. Fixes #6383 * ci: retrigger frontend after npm registry maintenance The frontend job failed solely on `npm audit` while registry.npmjs.org returned 503 (Service Under Maintenance). Lint, typecheck, vitest, vite build, and storybook all passed. Local `npm audit --omit=dev --audit-level=high` now reports 0 vulnerabilities. * fix(sub): fall back to the default UA when the DB is not initialised externalSubUserAgent read the setting through SettingService.getSetting, which calls Model() on database.GetDB() and panics on a nil *gorm.DB. The fetch path's other DB read, service.ExternalSubscriptionHwid, already treats a nil DB as unreachable and sends no header; the new UA lookup did not, so any fetch before InitDB panicked instead of sending the historical v2rayNG/1.8.5. Production initialises the DB before the sub server starts, but the internal/sub fetch tests run without one: under make test-go's -shuffle=on, whenever one of them ran before the first InitDB test the panic aborted the whole package. Reproduced deterministically with go test -run '^TestDoFetchSubscriptionLinks_RejectsOversizedBody$'. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
9672249edb |
feat(clients): add calendar weekly renewal and schedule previews (#6524)
* feat(clients): add calendar weekly renewal and schedule previews Expose fixed-day, calendar-weekly, calendar-monthly, and disabled renewal through one shared selector in individual and bulk client forms. Store the weekly weekday separately (Monday 1 through Sunday 7) and use panel-local calendar dates rather than a fixed 168-hour duration. Resolve skipped or repeated midnights to the first valid instant of the selected date, and skip an entirely nonexistent calendar date rather than changing the weekday. Reuse the existing renewal writer and share its boundary alignment and per-period catch-up calculation with an authenticated, read-only preview. Keep monthly precedence for legacy records, fixed-day interval semantics, maximum renewal allowances, first-use durations, and operator-disabled settings unchanged. Selecting a mode does not rewrite an existing cutoff; an unset calendar cutoff requires an explicit action to choose the first. The last-valid-second preview uses the stored exclusive expiry, even when the billing calculation aligns a legacy last-second cutoff up to midnight. Carry weekly schedules through client persistence, paging, enable toggles, inbound settings, and node traffic reconciliation. Migrate missing or nullable weekday columns to disabled by default without altering existing limits, and include the new isolated-schema PostgreSQL regression in the live CI gate. Regenerate API contracts and reference documentation, add lifecycle and form regressions, and document timezone, quota-reset, and upgrade considerations. All participating nodes must be upgraded before weekly mode is enabled; older binaries ignore the new field. Independent periodic traffic resets and the optional month-end subscription-header display are not changed. * fix(clients): validate renewal schedules across inbound write paths Reject conflicting weekly/interval/monthly schedules and out-of-range weekdays on inbound creation and edits, legacy one-client apply paths, record/link synchronization, and traffic metadata writes. Validate imported traffic snapshots as well, before any inbound or client is persisted, so an inbound API cannot create a client that the clients page cannot toggle. Merge a weekly-related schedule as one timestamp-selected tuple rather than filling its zero fields from another renewal mode. Preserve empty migration snapshots and the existing non-weekly monthly/interval merge semantics. Renewal caps, counters, credentials, and deadlines are unchanged. Add regressions for nine write paths, unchanged records and runtime calls after rejection, valid inbound clients remaining editable, and duplicate record merges between individually valid renewal modes. * docs(clients): clarify depleted-client deletion risks on downgrade Explain in English and Chinese that older versions not only stop weekly renewal: their depleted-client cleanup can delete a weekly-only client once its expiry or quota is exhausted. This is conditional on cleanup, not an automatic deletion caused by downgrade itself. Recommend backing up and converting weekly schedules to a mode supported by every participating version before rollback, and avoiding cleanup while mixed versions or unconverted clients remain. Merely disabling weekly renewal does not restore the old binary's missing purge protection. * fix(clients): bound weekly renewal date searches Limit the search for a valid weekly calendar date to eight candidates so an unusual timezone cannot monopolize the single traffic writer. Exhaustion returns the original instant, allowing the existing catch-up forward-progress guard to stop without advancing expiry, consuming an allowance, resetting traffic, or falling back to a fixed-duration schedule that can drift. Reject a non-future calendar suggestion in the read-only preview instead of offering an immediately expired initial cutoff. Also report failed weekly catch-up as a search error when allowances remain, not as cap exhaustion. Existing preview errors use the form's current warning; no API schema or locale changes are needed. Exercise exhaustion with a synthetic valid TZif containing twelve skipped Sundays. This fault-injection case was red without the bound; it is not a claim that a production IANA timezone was observed hanging. Keep the Havana and Apia regressions for real skipped/repeated midnights and absent dates. * fix(tests): isolate weekly renewal preview timezone Stop the weekly search regression from replacing process-global time.Local. CI caught that assignment and its cleanup racing with background timer reads through time.Now, even though the top-level tests do not use t.Parallel. Pass the timezone and current instant into the unchanged preview calculation. The public service still validates the request and resolves the panel timezone; API responses, renewal accounting, and persisted client data are unchanged. Use fixed dates for both suggestion and catch-up exhaustion, removing the test's dependency on today's date and its unnecessary database setup. Keep a bounded-lifetime background clock reader to expose future global-timezone mutations under the existing race gate rather than disabling that check. * ci: retrigger PR checks Create an empty commit to request a fresh pull-request CI run after release dependency downloads failed with network errors. No source, dependency, or workflow changes are included. Retry the existing checks without bypassing them. * ci: retry PR checks and record deferred download hardening Request another CI run after the amd64 release job compiled successfully but failed during dependency fetching with exit code 4 (network failure). Record possible follow-up improvements for the Linux release fetch helper: - Print each download URL and destination, and preserve error details. - Reuse the existing curl configuration with up to five retries; add connection and per-attempt timeouts and a bounded retry window. - Download to a temporary file and promote it to the final filename only after a successful, non-empty transfer. Keep the job failing if downloads ultimately fail. - Validate successful downloads, recovery after a temporary failure, and correct failure after persistent errors before shipping such a change. These improvements are intentionally deferred, not implemented or tested by this commit. This commit is empty: renewal logic, dependencies, workflow configuration, check requirements, and TLS verification remain unchanged. --------- Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com> |
||
|
|
5e15120cec |
feat(happ): add routing editor, optional ad blocking, and LAN bypass preset (#6545)
* feat(happ): make ad blocking optional in routing presets Add an independent AdBlock toggle for Iran, China, and global presets, applied only when generating routing rules. Update the China preset to Bypass-CN and cover preset behavior and localized controls. * feat: add visual routing editor with JSON support and localization updates - Implemented a new modal for editing routing profiles with basic and advanced tabs. - Added functionality to load, parse, and generate routing profiles in JSON format. - Enhanced user experience with validation and error handling for JSON input. - Updated translations for Russian, Turkish, Ukrainian, Vietnamese, Chinese (Simplified and Traditional) to include new routing editor terms. - Created helper functions for managing routing profiles and generating deep links. - Added unit tests for routing editor functionalities and JSON handling. * feat: update routing editor to preserve null lists in profiles and enhance validation messages |
||
|
|
6ab718f813 |
fix(sub): make Happ require auth on its local SOCKS/HTTP proxy (#6628)
Happ ships its local SOCKS5 (127.0.0.1:10808) and HTTP inbounds with authorization disabled by default. Any app on the same device can then connect to that proxy, bypassing Android's per-app VPN routing, and learn the VPN server address - the leak publicly described in March-April 2026 for Happ, v2rayNG and other VLESS clients. Happ fixed its Xray API exposure, but the unauthenticated local proxy remained. Happ exposes a standard subscription header for this (no Provider ID required): socks-auth-mode / http-auth-mode = auto|manual|from-json| disable. A new subscription setting, subHappLocalProxyAuth (default "auto"), sends both headers to Happ clients. Like every other Happ header it is emitted only when Happ auto-detect is enabled and the User-Agent is Happ, so panels that never opted into the Happ integration see no change. An empty value sends nothing and keeps the client's own setting. Verified on Happ Android 4.4.1 (Xray 26.7.28): a subscription carrying socks-auth-mode manual + a test user/password switched the client's Inbounds screen to Manual with those credentials on "refresh subscription", and "auto" switched it to Auto with generated credentials. Co-authored-by: Kirill Rudenko <rudenko@npp-energy.ru> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
8979072bd9 |
fix(amneziawg): bound S1-S3 by the receive buffer, reject overlapping H (#6642)
* fix(amneziawg): bound S1-S3 by the receive buffer, reject overlapping H The native AmneziaWG validator, both Zod schemas, both forms and the docs now follow the rules amneziawg-go actually enforces. S1-S3. A padded handshake message is 148+S1, 92+S2 or 64+S3 bytes (device/send.go). The peer reads each datagram into a [MaxMessageSize]byte buffer, where MaxMessageSize = MaxSegmentSize (device/pools.go, constants.go). MaxSegmentSize is 65535 on Linux/Android, 2016 on Windows and 1700 on iOS (device/queueconstants_*.go). The limits are therefore S1 <= 1552, S2 <= 1608 and S3 <= 1636. Before, S1/S2 allowed 65535, which iOS peers silently drop, and S3 was capped at 64, a number inherited from the coinman-dev/3ax-ui port in #6105 with no stated reason. That cap blocked real configs such as Amnezia Premium's S3=1045. RandomTrailers only tops a packet up to 500 bytes (DefaultUdpWindow), so it never pushes a message past these limits. H1-H4. amneziawg-go refuses the whole device when the header ranges overlap ("headers must not overlap", device/uapi.go mergeWithDevice), and so does the kernel module (src/netlink.c). The panel did not check this, so an inbound with overlapping ranges saved and then failed to apply. A blank H is never sent, so the engine keeps its default, WireGuard's own type 1-4; the check treats blank fields that way. The docs said 1-4 "must not be used". They are valid and are the engine default, only unobfuscated without a HeaderProtectionKey. The docs also said amneziawg-go rejects S1+56 == S2. It does not (IpcSet accepts it). The panel keeps that rule as a fingerprint guard, and the docs now say so. Tests: the new params_test cases and the Zod bounds fail on the old code. TestValidatedObfuscationAlwaysApplies runs every accepted set through a real amneziawg-go IpcSet and now covers overlap, blank-H defaults, H=1-4, the exact S bounds and the full Amnezia Premium set. Before this fix it failed with "headers must not overlap". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(amneziawg): bound only inbound padding by the iOS receive buffer The 1700-byte iOS buffer limits what an inbound's clients can receive, but ValidateObfuscation also runs for outbounds, and the Xray template save re-validates every AmneziaWG outbound. An outbound whose remote server uses S1 above 1552 would have blocked every Xray settings save, though its values come from that server and are received on Linux. ValidateObfuscation keeps amneziawg-go's uint16 UAPI width for S1-S3; ValidateServerObfuscation adds the receive-buffer bounds and is what inbounds call. The outbound schema and form follow the same split. --------- Co-authored-by: Kirill Rudenko <rudenko@npp-energy.ru> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
b3a5be9da4 |
fix(amneziawg): stop AAAA fallback on v4-only tunnels and expose I2–I5 (#6611)
* fix(amneziawg): stop AAAA fallback on v4-only tunnels and expose I2–I5 Gate tunnel DNS queries to address families the device can actually dial, reject undialable literal IPs early, and surface I2–I5 on the outbound form. Fixes #6570 * ci: retrigger frontend after npm registry maintenance The frontend job failed solely on `npm audit` while registry.npmjs.org returned 503 (Service Under Maintenance). Lint, typecheck, vitest, vite build, and storybook all passed. Local `npm audit --omit=dev --audit-level=high` now reports 0 vulnerabilities. * style(amneziawg): keep the tunnel DNS family comments to two lines CLAUDE.md caps a comment block at two lines. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
fd7b3559bc |
feat(panel): add sponsor slots fed from sponsors.sanaei.dev
Monthly sponsor placements need to change without cutting a panel release. Panels now read 3X/sponsors.json from the MHSanaei/sponsors repo (GitHub Pages on sponsors.sanaei.dev) and show active sponsors in four slots: an overview banner, a rotating sidebar card (max three), the login page and a new Sponsors page that also lists open placements. An entry shows only while enable is not false and until is in the future; links must be https and logos are png/webp/jpg by name only. The list is cached for an hour and the last good copy survives upstream failures; logos are proxied through /sponsors/logo/:name with failures cached, so CSP stays 'self' and admin browsers never reach a third party. Admins can hide a slot for 24h. Under XUI_DEBUG the panel reads a sibling ../sponsors/3X checkout so edits can be previewed before push. |
||
|
|
89e200ead4 |
fix(frontend): key geo entries by page position and clear test-suite noise
Zod 4: use the `error` param instead of the deprecated `message`. lint:deprecated missed these because tsgolint's no-deprecated does not resolve object-literal properties on a `string | Params` union. Geodata: key geo entry rows by page position. antd deprecates rowKey's index argument, and kind:value repeats within a page because the reader drops domain attributes (22 pairs in geosite_IR.dat, 108 in geosite_RU). Nord/PIA: the "All cities/regions" option used a null value, which antd warns on. Map it through a sentinel at the Select boundary so form state stays null, with tests that fail when the sentinel is not mapped back. Tests: - Run the oxlint guard through node; .bin/oxlint is a sh shim Windows cannot spawn, and the swallowed error left both guard cases vacuous. - Start unit workers with --no-experimental-webstorage; msw's localStorage probe made Node 25+ warn once per forked worker. - Set IS_REACT_ACT_ENVIRONMENT, which RTL never sets with globals: false, and settle the async updates it exposed inside act(). The row-cells memo test now fails when memo is removed. - Disable antd's click wave in Storybook; it re-rendered inside the next story's act() and tripped "not configured to support act". - Assert InboundFormModal's validation log instead of leaking it, and give the rule-form test a well-formed clients/list response. |
||
|
|
17e89db979 |
feat(hosts): add a cipher suites override and accept custom suites
The inbound TLS form offered cipherSuites as a closed single-choice list, but xray reads the value as a colon-separated list and accepts any name Go knows, so several suites or one missing from the list could not be set. Both the inbound and the new host field now use a tag picker that keeps the stored value as the colon-joined string xray expects; old single values open unchanged. A host's cipher suites replace the inbound's in the JSON subscription stream, and a blank field inherits them. Share links and Clash carry no cipher suite parameter, so their output is unchanged. |
||
|
|
b78dd82869 |
fix(nodes): chart node net throughput in KB/s, not percent
The node history panel passed its Net Up / Net Down series to Sparkline without valueMax or yFormatter, so they inherited the percentage defaults: a fixed 0-100 scale and a "%" label. Any node above 100 KB/s drew off the top of the chart and every axis tick and tooltip read as a percentage. A Sparkline fed non-percentage data has to declare its own scale and unit; every other call site already did, only the two node net series did not. |
||
|
|
e26cf1d3ed |
feat(sub): redesign the subscription page around usage, tabs and app imports
The info page was a long key/value table followed by every link and two app dropdowns, and it rendered left-to-right even for Persian and Arabic. It now leads with a usage ring, the remaining quota and a stats grid, and splits the rest into Subscription / Apps / Configs tabs. - Status tells expired, data-used-up and disabled apart instead of one "Inactive", replacing the hard-coded English expiry chip. - The Apps tab keeps every Android and iOS app with its existing deep link, preselects the visitor's platform and adds Windows: Hiddify and Clash Verge Rev import directly, v2rayN copies the link. - fa-IR and ar-EG render right-to-left; URLs, IDs and sizes stay LTR. - The footer shows the support link and the client refresh interval, so subPageContext now carries subUpdates (also in ?format=info). - Status, days-left and app deep-link logic lives in subPageModel.ts, with unit tests pinning the deep links the page already shipped. |
||
|
|
01ce2bcecb |
feat(api-docs): split the API docs page into tabs
The page stacked the WebSocket event cards above every Panel API operation
in one long scroll. The WebSocket events and the 3X-UI Panel API now sit in
separate tabs, and the Panel API shows one OpenAPI tag at a time through
section tabs placed between the Authorize bar and the operations.
The section tabs replace Swagger UI's FilterContainer and wrap the
taggedOperations selector, so all sections share one Swagger instance and
keep authorization and try-it-out state. Swagger's own filter matches tags
by substring ("Settings" would also show "Xray Settings") and does nothing
until set, so the wrapper matches the exact tag and defaults to the first.
Tag names come from the loaded spec rather than importing endpoints.ts,
which would have grown the page chunk from 23 kB to 119 kB.
|
||
|
|
5008906c4c |
feat(clients): filter the client list by clicking a summary stat card
Each card on the Clients page now toggles its status bucket as the sole filter, and the Clients card clears it. The bucket filters used to be wider than the card counts: "active" still included clients near depletion and "deactive" included disabled clients that had run out, so a filtered list could disagree with the number on the card. Both filters now reuse the summary expressions, and a test pins each card's count to the size of its filtered list. |
||
|
|
5fe4f241c1 |
style(logs): widen the row-count selector in the log modals
At 70px the selector truncated its larger values, so the chosen row count was hard to read in the panel, Xray and AmneziaWG log modals. |
||
|
|
e8bab17c2f |
fix(clients): stop the Edit Client modal showing a stray light scrollbar
The client form body is capped at the viewport and scrolls internally
(
|
||
|
|
1d85ef138e |
fix(sub): prevent default profile page URL disclosure (#6538)
* fix(sub): prevent default profile page URL disclosure Add explicit none, builtin, and custom profile page modes. Preserve existing custom URLs and warn before exposing the built-in page. Cover mode selection, legacy settings, and subscription response headers. * fix(subscription): add profile page link options and upgrade notes |
||
|
|
3fa44915c1 |
perf(nodes): keep the node table element across unrelated re-renders
rc-table re-runs every cell renderer whenever the Table re-renders, and NodeList rebuilt its columns and table props on every render (the relative time formatter was a fresh function each time). Any re-render of the Nodes page therefore re-rendered all rows even when no node had changed: about 390ms per re-render for 150 nodes in jsdom. The formatter is now stable and the table element is memoized on its inputs, so a re-render that leaves the nodes untouched costs 0.5ms. A heartbeat push that does change the nodes still re-renders every row. |
||
|
|
7fc86f87de |
perf(inbounds): keep unchanged rows and online sets across websocket pushes
Every client_stats push carries the totals of all inbounds, and applyClientStatsEvent rebuilt each row it listed, so every push replaced all rows, re-ran the client rollup (a JSON parse of every inbound's settings) and re-rendered the whole table even when no number moved. Every traffic push also built new online and active maps, re-running the same rollup. Rows are now rebuilt only when their totals or a client's numbers change, and the previous maps are kept when a push repeats the same sets. Measured in jsdom with 450 inbounds of 50 clients each: an unchanged client_stats push went from 7.9ms to 0.6ms with no row rebuilt, and a repeated traffic push from 13.5ms to 8.3ms without the rollup. |
||
|
|
baef3cdd07 |
fix(xray): refuse a config the running core cannot bind (#6547)
* fix(xray): refuse a config the running core cannot bind RestartXray stopped a working core before handing it a config whose listens collide, so the failed bind exited the whole process (main/run.go:94) and the one-second watchdog retried it in a loop: every protocol down, cause only in the logs. The save-time port guards cannot cover this -- SetInboundEnable, the AmneziaWG relay created on the first peer, template and bridge edits all reach a colliding config with no guard on that path. Probe the generated config at the single restart funnel instead. Collisions the running core already serves are excused, so an established setup is never refused by a static read being wrong about it, and the port-bucketed pass costs nothing on a clean config. * fix(xray): surface a refused config and re-key the bind excuse set Round-1 findings on this PR. Refusing the swap left the running core on its previous config with nothing but a log line to show for it, so the status response now carries the reason while the core runs and the overview marks it; the node list picks the same field up through that response. The excuse set is keyed on the two listens, the port and the shared transports instead of the tag pair, so a pair whose listen moves onto the other's address is refused again, while the same two sockets stay excused however the generator orders them. TestBindConflicts/excused_pair_whose_listen_changed_into_a_real_collision fails without the key change -- watched red first. |
||
|
|
c0271e231d |
fix(panel): read the outbound protocol id in the Outbounds row like the core (#6528)
* fix(panel): read the outbound protocol id in the address column like the core outboundAddresses switched on the raw id, so a row the core runs normally but spelled "VMess", "Trojan" or "WireGuard" fell through to default and rendered an empty Address column in the outbounds table, the card view and the subscription table -- a populated server that looks absent, which is what sends an operator to recreate a correct outbound. The id is folded once before the switch, the way isUdpOutbound already folds the transport name. * fix(panel): fill the outbound address column from one protocol-id rule outboundAddresses folded the id inline while isUntestable, two functions below it in the same file, reads it through isOutboundProtocol — so the "the core lowercases the id" rule lived in two places and two tests. It now routes through the shared helper, which keeps the rule with the module that owns it. Two further gaps in the same switch, reported in the same review: hysteria and amneziawg are both selectable in the outbound form but had no case, so a canonically spelled row rendered a blank Address cell that case folding could not reach; and the VLESS branch returned a bare ":" for a row whose servers sit in vnext, which this change newly reached for a "VLESS" spelling. Tests: the hysteria/amneziawg cases and the bare-separator case are red on the pre-fix switch. * fix(panel): read the vnext shape of a vless outbound in the address column The vless branch read only the flat settings.address/port, so a row whose servers sit in vnext — the shape the probe's extractor reads first (internal/web/service/outbound/outbound.go:259-269) — rendered a bare ":" separator, or nothing at all before this branch folded the id. It now reads vnext first and falls back to the flat pair, the order the extractor uses, which also makes it agree with what a probe of that row would say. Test: "reads the vnext server of a vless row" is red on the pre-fix branch. * fix(panel): read the protocol id of the outbound stream tags like the core The identity cell gated the network and security tags on an exact-match includes() over four ids, so the same "VMess" row whose address this branch now shows still rendered without its ws/tls tags — the row was half-readable. It now asks the shared isOutboundProtocol, the rule every other reader on the page uses. Test: "renders the stream tags and the address of a VMess row" is red without this change (['VMess'] vs ['VMess','ws','tls']). |
||
|
|
efcf152950 |
fix(outbound): read the probe testability gate's ids like the core (#6527)
A direct, DNS, loopback or blackhole outbound is not a proxy, so the probe must reject it instead of measuring the panel host's own reachability. The gate compared the protocol id exactly while the core lowercases it in LoadWithID before resolving the handler, so "Freedom" and "DNS" were not recognised: the HTTP probe ran through the direct outbound and returned Success=true with a full egress block, and the row's Test button stayed enabled because isUntestable compared exactly as well. The operator reads the panel host's own country and delay as a working tunnel. The batch gate now folds the id once before its switch, and isUntestable goes through the shared isOutboundProtocol helper. |
||
|
|
84c5aef4a1 |
fix(panel): probe UDP outbounds and hide the block outbound from the mtproto egress picker (#6525)
* fix(panel): match the probe and egress readers to what the core loads Two readers left over from the case-sensitivity sweep still disagreed with the core, both raised reviewing #6523. isUdpOutbound compared the protocol id and the transport name exactly. The core lowercases both before it resolves them (infra/conf/loader.go:46 for the id, TransportProtocol.Build at infra/conf/transport_internet.go:16-17 for the name), so an outbound spelled "WireGuard" or a stream named "KCP" still built a UDP handler but was probed with a dial-only TCP request, and Test All Outbounds reported a working outbound as down. The mtproto egress picker asked for outbound tags without excludeBlackhole, so the block outbound stayed selectable there. Choosing it looks like a working selection and discards that inbound's Telegram traffic. * fix(panel): recognise the mkcp transport alias and pin the picker's field id Review findings on #6525. TransportProtocol.Build resolves both "kcp" and "mkcp" to the same mKCP transport, so comparing the transport name against "kcp" alone left a template spelling "network": "mkcp" in the TCP lane and reported a working outbound as down — the same trigger this PR already fixed for the "KCP" capitalisation. The egress picker now carries an explicit id, the way the inbound form's protocol select does, so the test addresses that field rather than the first searchable select on the page and reuses the shared dropdown helper instead of duplicating it. |
||
|
|
a5a4c9cd83 |
fix(panel): read an outbound protocol id the way the core does (#6522)
* fix(panel): read an outbound protocol id the way the core does
xray-core lowercases a protocol id before it resolves the handler, so a
template that spells the direct outbound "Freedom" is that outbound. The
outbound editor fell through to the vless default and rendered it as an
empty vless server, and the Basics tab did not find it and appended a
second "direct", which the core refuses to load with "existing tag found".
* fix(panel): never leave the direct tag on two outbounds
A "direct" tag held by a non-freedom egress made both Basics-tab setters
push a fresh freedom outbound, and the core refuses a config whose tags
repeat ("existing tag found: direct"). The tag is now checked on its own
before anything is added, matching setDefaultOutboundTag.
* fix(panel): disable the freedom controls when direct is held elsewhere
When a non-freedom outbound holds the "direct" tag, both Basics setters
drop the edit so the core never sees the tag twice, but the Freedom
Strategy select and the Happy Eyeballs switch stayed enabled and snapped
back with no sign of why. isDirectTagTaken now disables both controls in
that state.
The find-or-create-plus-guard was also copied into BasicsTab's happy
eyeballs setter with no test of its own; ensureDirectFreedomOutbound now
owns the lookup, the guard and the creation for both setters, so the
existing helper tests cover that path too.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
|
||
|
|
c0c2dd274c |
fix(panel): read outbound protocol ids case-insensitively everywhere (#6523)
Six more readers compared an outbound's protocol id exactly while the core lowercases it, so an outbound spelled "Blackhole" passed every excludeBlackhole filter (offered as an mtproto egress, a dialerProxy target and the geodata download egress, all of which then drop the traffic) and one spelled "Freedom" was queued by Test All Outbounds. They now share isOutboundProtocol. |
||
|
|
c90996eda3 |
feat(sub): add opt-in month-end expiry presentation (#6517)
Offer monthly calendar subscriptions an explicit last-valid-second display without moving their real billing boundary or spending renewal allowances. Keep the option off by default and limit conversion to a shared fixed day-1 midnight cutoff at an actual month transition in the panel timezone. Use the authoritative client calendar mode when aggregating node traffic, and share the header formatter across raw, JSON, and Clash exports. Expose the setting in the existing settings API/UI, regenerate its schemas, and document that clients may report expiry one second early or format the date differently in another timezone. Add HTTP, settings, and DST coverage. Stored deadlines, access enforcement, info/remark expiry values, and renewal accounting remain unchanged. Refs: #6516 Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com> |
||
|
|
826e29e2de |
fix(xray): place the freedom domain strategy where the core reads it (#6515)
* fix(xray): place the freedom domain strategy where the core reads it freedom resolves through the socket layer, so xray-core reads sockopt.domainStrategy and treats both other placements as legacy: it warns on every config load for the outbound-root targetStrategy it migrates itself, and again for the settings-level domainStrategy it deprecates. The panel wrote exactly those two keys from its Freedom Protocol Strategy select, the outbound form card, and the IPv4 routing helper, so any install that had configured a strategy logged a deprecation warning on every start. The strategy now travels in streamSettings.sockopt everywhere the panel emits it: the Basics select, the outbound form (including the JSON tab, which shares the same adapter), the shipped default template, and the IPv4 outbound the routing helper injects. Reading mirrors the loader's own order — root targetStrategy, then the settings keys, then sockopt — so the card keeps showing the value the core would actually run with, and saving drops the legacy keys instead of leaving them behind. A seeder moves the keys for configs already stored in the database, following OutboundRemovedKeysFix. The shared outbound-root Target Strategy field is hidden for freedom, since the core migrates that key into the very sockopt value the card writes and two knobs for one value would race. Tests: placement round-trips and the migration table run through the real vendored core (a captured log handler proves the warning is gone after the rewrite and present before it), and the modal asserts freedom offers a single strategy field. * test(database): seed the template row the seeder test needs A fresh InitDB creates no xrayTemplateConfig row — the panel's setting defaults live in the service layer — so the test has to insert the legacy template itself and then assert the seeder's history gate stops a second pass from rewriting it. * fix(xray): keep one strategy control per outbound, seed the row in tests Review findings: the Transport tab's Sockopts block renders for freedom too, so its Domain Strategy select and the freedom card wrote one sockopt value between them and the card won on save — the field is hidden for freedom now, leaving the card as the single control. The seeder is also pre-marked on a fresh install so it does not run on the second start, and the seeder test seeds the template row itself (a fresh InitDB has none) and asserts the rewrite structurally instead of grepping for a key name that sockopt also uses. |
||
|
|
939c470698 |
feat(inbounds): show linked host remarks in inbound list (#6468)
* feat(inbounds): show linked host remarks in inbound list Join Host Group remarks from the existing hosts list onto each inbound row client-side so multiple endpoints (IPv4/IPv6/CDN) are visible without opening the inbound. Truncate long lists with a tooltip for the full set. Fixes #6026 * fix(inbounds): skip disabled host groups in inbound list remarks buildHostRemarksByInboundId joined every host group from /hosts/list onto its inbounds, so a group toggled off on the Hosts page still read as a live endpoint in the inbound remark cell and matched the search box. A disabled group serves nothing: internal/sub/host_sub.go filters it out of subscription output and withMtprotoHostEndpoints skips it for MTProto share links. Skip it here the same way, and drop the unread `truncated` field from formatHostRemarksLabel. --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
435ed976c0 |
fix(web): restart panel after ImportDB so subPath routes match (#6446) (#6456)
* fix(web): restart panel after ImportDB so subPath routes match (#6446) ImportDB only restarted Xray, leaving the subscription HTTP server on startup-registered paths. Schedule the same in-process restart hook used by restartPanel so restored subPath (and related) routes take effect without relying on a browser follow-up that can fail after session invalidation. * fix(web): schedule the post-import panel restart once, via PanelService ImportDB grew a private copy of PanelService.RestartPanel (same hook check, same Windows bail-out, same SIGHUP fallback, already diverging in log severity) while BackupModal kept POSTing restartPanel after a successful import, so one restore bounced the panel and the public sub server twice back to back. The service package cannot reuse PanelService (panel imports service), so the importDB controller now calls the existing RestartPanel(3s) after ImportDB succeeds, the duplicated helper is dropped, and the browser follow-up is removed; it waits out the restart and reloads. Test drives the importDB handler against a stub xray binary and fails when no restart is scheduled through the global restart hook. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |