mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-01 19:52:09 +03:00
8c023d13dc9d01a72bf8be6fa409c6af392b8088
210 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
5d41e65a3c |
fix(sub): preserve external VLESS encryption in Clash subscriptions (#6576)
* fix(sub): preserve external VLESS encryption in Clash subscriptions Copy non-empty, non-none encryption from parsed external VLESS settings, matching local proxy export. This prevents merged Clash/Mihomo subscriptions from losing the encryption parameters of externally added nodes. Cover encryption normalization and omission, plus merged YAML from pasted links and HTTPS subscriptions containing plain or Base64 share-link lists. Validation: regression cases fail before the fix and pass after it; the full subscription package and go build ./... pass. Four unrelated packages still fail on Windows, with the same failures reproduced using the original code. Refs: MHSanaei/3x-ui#6572 * test(nodes): wait for chart effects in history panel assertions The DOM can commit its accessible labels before Sparkline updates the refs used by uPlot range callbacks. Wait for the existing assertions together so the test does not read the empty-data range. Reproduced the original CI failure locally on attempt 5. The fixed test passed 12 consecutive runs; lint, format and TypeScript checks pass. The full frontend suite passed 1607 of 1608 tests, including Storybook. The unrelated input-number guard fails on Windows because execFileSync cannot launch the extensionless oxlint shim (ENOENT); invoking oxlint.cmd reports all three expected diagnostics. |
||
|
|
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. |
||
|
|
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
(
|
||
|
|
14b92fbcff |
fix(nodes): stop flagging a node on the other update channel as outdated
A node's "update available" tag compares its reported panel version with the master's latest, and any non-semver side fell back to string inequality. A dev build reports dev+<sha> (config.GetPanelVersion), so a node moved to the dev channel from a master on the stable channel kept the tag forever; the reverse, a stable node under a master on the dev channel, was flagged too and the tag's default stable update installed nothing new. A dev label and a release tag carry no order, so the comparison now only decides within one channel; dev-to-dev still compares commits, which keeps a node on the current dev-latest commit untagged as config.go intends. |
||
|
|
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. |
||
|
|
bc424f0968 |
fix(xray): stop a lone dns qType 0 from matching every query
The core reads a dns rule's qType as a PortList, which drops a bare numeric 0 (infra/conf/common.go: `if number != 0`), and a rule with no qTypes matches every query. A stored `"qType": 0` therefore does not target query type 0: it drops, refuses or hijacks all DNS through that outbound. A qType the panel writes has to be read by the core as exactly the query types it names. Four writers broke that: - DNSOutboundLegacyKeysFix rewrote a lone blockTypes [0] into "qType": 0, so "block type 0" became "block everything" on upgrade. - That seeder shipped in v3.8.0 and is recorded as done, so fixing it does not reach installs that already ran it. DNSOutboundQTypeZeroFix spells any stored numeric qType 0 as "0" once, protocol id matched like the core. - The outbound form adapter turned a typed "0" into the number 0. - The Xray template editor saves raw JSON past that adapter; the save now applies the same rewrite. Each writer is pinned by a test that fails without its part. The rewrite and the repair compare policies as the pinned core builds them, and the repair runs through runSeeders over a database whose legacy-keys seeder already ran, on SQLite and PostgreSQL 16. |
||
|
|
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. |
||
|
|
5ad9df69b9 |
fix(link): restore mKCP seed and headerType on share-link import (#6480)
* fix(link): restore mKCP seed and headerType on share-link import applyTransport / applyTransportParams ignored kcp query params that applyKcpShareParams emits, so re-imported outbounds lost seed and header and could not talk to the inbound. Mirror those fields (plus mtu/tti) into kcpSettings in both Go and TS importers. Fixes #6476 * fix(link): restore mKCP header/seed via finalmask mkcp-legacy * fix(link): split mKCP header and seed into separate masks on import Both importers folded a share link's headerType and seed into one mkcp-legacy mask {header, value}. xray-core's MkcpLegacy.Build ignores value once header is set (and reads it as the fake DNS domain for header=dns), so an imported outbound carried the header mask but no AES-128-GCM seed while the emitting inbound has both, and could not connect — the failure #6476 reports, now for every link carrying both params. Emit one mask per field, seed first: the finalmask array's first item is the innermost layer, which puts the header around the cipher as legacy mKCP did. Also bound mtu/tti to KCPConfig.Build's accepted ranges (mtu >= 21, tti 10..1000, decimal digits only on both importers) so a pasted link cannot fail the whole Xray config load, and look header types up as own properties so a prototype key such as "constructor" is not mapped. --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
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> |
||
|
|
d0ad773edf |
fix(clients): preserve traffic reset schedule when toggling enable (#6502)
Carry the hydrated reset cycle and day into the enable update payload so toggling a client does not normalize its schedule to never. Cover both toggle directions with a regression test. |
||
|
|
d600de2c2e |
feat(geodata): add standard source presets (#6504)
* feat(geodata): add standard source presets Expose the existing geofile allowlist to the Geodata editor so administrators can configure the supported scheduled downloads without copying URLs manually Tested with npm run test, npm run lint, npm run typecheck, npm run format:check, and go test ./internal/web/service ./internal/web/controller -run '^(TestStandardGeodataSources|TestGeodata)' -count=1 Assisted-by: OpenCode:openai/gpt-5.6-terra (mostly) * fix(geodata): preserve custom source entries Add missing standard sources instead of replacing existing custom entries. Assisted-by: OpenCode:openai/gpt-5.6-terra (mostly) |
||
|
|
8fc4fc0bf8 |
fix(link): rebuild shadowsocks tcp/http obfuscation on import (#6505)
* fix(link): rebuild shadowsocks tcp/http obfuscation on import genShadowsocksLink encodes tcp/http obfuscation only as the SIP002 plugin=obfs-local;obfs=http;obfs-host=... parameter, deleting type, headerType, path and host in the process, because SIP002 clients ignore those and read `plugin` alone. ParseLink read none of them, so importing a link the panel had just exported produced a plain tcp outbound with header.type none: the obfuscation the inbound requires was gone, and the client could not connect to the very inbound the link came from. The plugin is now mapped back onto the header it stands for. Credentials and every other parameter are untouched, and other plugin values are left as they were because Xray has no equivalent for them. * fix(link): map the SIP002 plugin in both importers The panel parses share links twice: link.ParseLink in Go, which the external subscriptions use, and parseShadowsocksLink in outbound-link-parser.ts, which the Add Outbound button calls. Mapping the plugin in Go alone left the UI path still saving header.type none for a link the panel had exported itself, so one panel answered the same link with two different outbounds. The unencoded plugin=obfs-local;obfs=http;... form maps as well now: stdlib drops any query pair whose value holds a literal semicolon, and that is the shape clients which skip percent-encoding emit, so the raw query is read as a fallback when the parsed parameter is missing. |
||
|
|
f3dba07e13 |
fix(link): read the vmess certificate checks on import (#6507)
* fix(link): read the vmess certificate checks on import applyVmessTLSParams writes ech, vcn and pcs into the vmess share object, but parseVmess only read sni, fp and alpn back. Importing a link the panel had just exported therefore dropped all three: no pinned certificate, no verify-by-name, no ECH. On a server whose certificate is only trusted through a pin, the imported outbound falls back to public-CA verification against the system roots and cannot connect to the inbound the link came from. The url-param protocols already read the same three in applySecurity, and the core takes pinnedPeerCertSha256 as one joined string there, so the vmess path now fills them the same way. * fix(frontend): read the vmess certificate checks on import The panel parses share links twice: link.ParseLink in Go and parseVmessLink in outbound-link-parser.ts, which is what the Add Outbound button calls. Reading ech, vcn and pcs in Go alone would have made the two sides disagree on one link, leaving the UI path — the one an operator uses by hand — still dropping the pin the panel had just exported. |
||
|
|
cba8f0672f |
feat(sub): refine Happ routing presets, serverDescription escaping, and auto-detect placement (#6488)
* feat(sub): refine Happ routing presets, serverDescription escaping, and auto-detect placement * fix(sub): address PR review findings on routing parity, agent regex, and i18n |
||
|
|
6a5b4fab6a |
feat(happ): generate Crypt5 subscription links locally (#6494)
* feat(clients): add stateless Happ link generator
Generate Happ provider links from the current effective subscription source without caching results. Reject unsafe provider responses and redact failure diagnostics.
* fix(clients): reject duplicate Happ provider fields
Parse Happ provider objects token by token so duplicate supported keys cannot be silently overwritten by encoding/json.
* feat(clients): expose on-demand Happ link API
Expose a no-store client endpoint backed by the Happ link generator and keep its generated OpenAPI contract synchronized.
* fix(openapi): exclude service interfaces from generated types
Keep dependency-injection interfaces out of the frontend API surface while preserving allowed response schemas.
* feat(clients): add stateless Happ QR presentation
Generate Happ links only for the active modal scope and retire late responses so Standard remains immediately available. Add focused component coverage and localized retry guidance across every locale.
* fix(clients): cover overlapping Happ generations
Prove the cancellation cleanup is required by resolving a retired request while its replacement remains pending. Also wait for Regenerate to leave loading state before exercising the existing action.
* fix(clients): harden Happ link handling
Validate generated responses before rendering and hide actions during unresolved requests. Strengthen route, redirect, timeout, and lint regression coverage with mutation-sensitive tests.
* fix(clients): gate Happ link generation behind operator opt-in
- add a fail-closed happLinkEnable setting
- enforce the gate before and after provider requests
- add locked Happ QR state with privacy disclosure and settings link
- cover backend, frontend, settings, and i18n regressions
* fix(frontend): guard oversized Happ QR codes
Keep valid long crypt5 links copyable while suppressing QR rendering and image actions above the encoder's UTF-8 byte limit. Add localized guidance and boundary coverage.
* fix(clients): log the sanitized transport error for Happ link failures
Every fail() call in HappService.Generate passed a string literal as the
detail, so the sanitizer written for provider errors only ever saw
constants, and an operator following the QR modal's "check Logs" hint
found nothing beyond reason=transport. Transport and body-read errors now
flow through sanitizeHappDetail, which also redacts cookie/session pairs.
Drop TestHappLinkEnableDefaultsOffWithoutPersistingRow: it pinned a getter
and its constant default, which the Generate gate test already drives.
* fix(frontend): size the Happ QR cap to level L and keep the QR modal mounted on close
HAPP_QR_MAX_BYTES was the level-M capacity (2331) while QrPanel encodes at
errorLevel "L", whose version-40 byte-mode capacity is 2953, so valid links
between 2332 and 2953 bytes lost their QR. The cap now matches the encoder
and a test renders the real QrPanel at the boundary.
Keying the modal content on `open` remounted it on every close, which cut
the Modal's exit transition and made the openSubId sync unreachable, so
`loading` never turned on for the subLinks fetch and a client without a
subscription link flashed noLinks on reopen. `open` leaves the key and the
sync block now also resets the Happ state.
* chore(clients): request Happ crypt5 links from api-v3
crypto.happ.su serves api-v2.php and api-v3.php side by side. Probed with
the same payloads, both take {"url"} over a JSON POST, answer
{"encrypted_link":"happ://crypt5/..."} of identical length with the same
crypt5 key marker, and fail the same way: 400 "No url provided.",
500 "Invalid URL format.", 405 on GET. Happ's own generator page is
branded "URL Encryption v3", so the panel follows it. The parser and the
link validator are unchanged.
* feat: add local generation of encrypted Happ links
- Implemented functionality to generate encrypted Happ links locally without network dependency.
- Added validation for URL length and format to ensure compliance with processing limits.
- Introduced new error handling for invalid URLs and control characters.
- Updated translations for various languages to reflect changes in Happ link generation.
- Created unit tests to validate the encryption process and ensure session keys and nonces are unique.
* fix(frontend): match the tuic memo deps to the non-optional subSettings
The Happ branch reads subSettings non-optionally in ClientQrModalContent
(happLinkEnable and the WireGuard/AmneziaWG publicHost memos), so React
Compiler infers subSettings.publicHost. The TUIC memo merged in from main
still listed subSettings?.publicHost, which fails oxlint's
preserve-manual-memoization rule and makes the compiler skip optimizing
the component. make verify stopped at lint-fe on the branch head.
* chore(happ): trim the pinned-key provenance comment to two lines
CLAUDE.md caps a comment block at two lines. The bare URL line repeated
the repository and file the next line already names, so it is folded
into that line (review LOW on happ_crypto.go).
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
|
||
|
|
503b5df4b9 |
fix(link): preserve Shadowsocks TLS query params on import (#6467)
* fix(link): preserve Shadowsocks TLS query params on import Mirror trojan/vless stream parsing so Xray-native type/security/sni/alpn/fp query params on ss:// links survive into streamSettings on both Go and TS importers. Fixes #6094 * fix(link): drop extra blank line so oxfmt passes --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> |
||
|
|
51e0afdd90 |
fix(inbounds): allow negative subSortIndex for subscription order (#6465)
* fix(inbounds): allow negative subSortIndex for subscription order Preserve explicitly set negative indices so primary inbounds can sort ahead of the default without renumbering peers; keep 0/omitted → 1. * fix(inbounds): gofumpt model.go and trim subSortIndex comments --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> |
||
|
|
b467d4c676 |
feat(reality): warn when target cert chain is too small for ML-DSA-65 (#6470)
* feat(reality): warn when target cert chain is too small for ML-DSA-65 Expose peer cert-chain DER size from the REALITY scanner and surface a UI warning when ML-DSA-65 is enabled but the chain is under xray-core's 3500-byte minimum, so silent fallback failures are easier to catch. Fixes #5973 * fix(reality): gate scanner ML-DSA tag and sync docs OpenAPI Only warn on short cert chains in the target scanner when ML-DSA-65 is enabled. Copy frontend/public/openapi.json to docs/public/openapi.json and fix oxfmt wrapping in the new test. --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> |
||
|
|
6d96accd63 |
Feature/tuic v5 (#6337)
* Feat(tuic): Implement native TUIC v5 protocol support via Rust sidecar daemon - Add internal/tuic package for official tuic-server sidecar lifecycle management, configuration generation, and graceful process control - Bridge decrypted TUIC QUIC traffic into loopback Xray SOCKS5 inbounds (63200+id) for traffic accounting, statistics, and routing rules - Implement periodic reconciliation job (cadence @every 10s) and immediate runtime synchronization on inbound/client mutations - Add TUIC inbound & multi-user client settings (UUID + Password authentication) in Web UI with SNI auto-fill and panel certificate loader - Integrate tuic:// subscription links and Clash.Meta (Mihomo) proxy generation for TUIC - Update install.sh to automatically download and install official tuic-server release for x86_64, aarch64, and armv7 - Add full localization for TUIC protocol across all 13 supported languages * Feat(install): Support custom repository and branch in install and update scripts * Ci(release): Enable publish-dev for feature branch and workflow dispatch * Feat(sub): Add TUIC to subscription resolution and client QR config generator - Add 'tuic' to getInboundsBySubId SQL allowlist to resolve TUIC inbounds in subscriptions and sub links - Enhance buildTuicProxy in Clash subscription generator with robust host and credentials resolution - Add tuicConfig.ts to generate standalone Clash/Mihomo YAML configuration - Add dedicated TUIC Config tab in ClientQrModal with QR code and .yaml download button - Add localization keys for TUIC config across all 13 supported languages * Fix(tuic): Exclude TUIC from native Xray inbounds and strip udp_relay_mode from server config - Exclude model.TUIC from native Xray inbounds in GetXrayConfig to prevent Xray startup failure - Remove udp_relay_mode from tuic-server JSON configuration builder - Update install.sh to install tuic-server binary to both xui_folder/bin and /usr/local/bin * Fix(install): Fallback to dev-latest when releases/latest is not present on fork * Feat(tuic): Add real-time online status and LastOnline tracking for TUIC clients - Track client activity by mapping client UUID in tuic-server logs to email - Integrate TUIC active clients into XrayTrafficJob to refresh local online clients - Bump LastOnline timestamp in database and broadcast live online status over WebSocket * Feat(tuic): Implement real-time traffic statistics and live speed reporting for TUIC - Collect precise I/O traffic deltas for tuic-server child processes via /proc/<pid>/io - Aggregate and attribute TUIC traffic deltas per client in tuic Manager - Integrate TUIC traffic deltas into XrayTrafficJob to update database and broadcast live speed * Feat(tuic): Finalize TUIC v5 integration with 1:1 traffic counting and orphan process cleanup - Use exact 1:1 byte delta accounting from /proc/<pid>/io - Add killStrayTuicProcesses to terminate orphan sidecars on panel startup - Fully integrate TUIC with subscriptions, live speed meter, and all 13 locales * Feat(frontend): Polish TUIC UI, support bulk operations, and update translations - Align TUIC inbound certificate form with standard 3X-UI layout (Set Default Cert, Clear) - Remove extra subtitle hint text from TUIC inbound form fields - Support TUIC in client bulk attach/detach and bulk add modals - Add TUIC badge color to client info modal, clients table, and host list - Update password tooltip across all 13 locales to include TUIC - Remove obsolete dead translation keys across all 13 locales * Chore(ci): Finalize TUIC v5 bundling across release workflow, Docker, and scripts * Feat(openapi): Update OpenAPI generator and schemas for TUIC types * Fix(backend): Address core review findings for TUIC types, port checks, and xray bridge * Refactor(traffic): Isolate proc reading with build tags and decouple TUIC metering into TuicJob * Feat(client): Add TuicServer to InboundOption, fix config export and clean share links * Fix(frontend): Register TUIC in multi-user helpers, tracked protocols, and tag derivation * Chore(openapi): Re-generate OpenAPI specification and sync Zod schemas * Chore(scripts): Add Alpine musl binaries, 386 and Windows packaging, and anchor pkill * Fix(review): Remove stale import, correct binary names, switch to musl, and drop unreachable relay gate * Feat(frontend): Show share link in Inbound Info and display UDP tag for TUIC * Docs: Add TUIC v5 configuration guide and link specifications * Docs(tuic): Correct Clash Meta configuration parameter to reduce-rtt * Fix(tuic): Generate client credentials on copy, enforce ID/password validation, and add i386 to DockerInit * Fix(tuic): drop unused relay, fix traffic accounting, and honor host endpoints - Drop unused loopback SOCKS relay and eliminate port collision with AmneziaWG - Correct inbound traffic calculation without double-counting - Drop heuristic client traffic division while retaining online tracking - Support externalProxy host fan-out and conditional parameters in share links - Scope orphan process termination to managed config directory * Fix(tuic): enforce client quotas, decouple Xray restart, and sync openapi schemas - Regenerate OpenAPI, Zod schemas, and TypeScript types without route_through_xray - Populate clientTraffics in TuicJob to enforce client quotas and first-use expiry - Split process I/O delta into up and down in Process.CollectTraffic - Remove SetNeedRestart from updateTuicInbound to prevent Xray session drops - Use InstanceFromInbound for default ALPN and UDP relay mode in tuic:// share links - Support allow_insecure on externalProxy host endpoints without parameter collision * Fix(tuic): attribute client traffic only on single-user inbounds and sync link defaults - Attribute I/O deltas to the client only when the inbound has exactly one configured client, avoiding false billing and disablings on multi-user inbounds - Aggregate client traffic by email in TuicJob so clients on multiple inbounds don't lose deltas - Match frontend genTuicLink defaults for alpn and udp_relay_mode with backend subscription links * Fix(tuic): gate client traffic by total sidecar clients and require client email * Fix(tuic): enforce inbound-only traffic limits and disable client totalGB * fix(tuic): restore delayed start, remove client totalGB rejection, and document linux-only limits * fix(tuic): anchor pkill, fix io baseline/split, escape yaml, and deduplicate start errors * fix(tuic): prevent traffic double-counting, ensure info log level for delayed start, and broaden pkill matching * fix(tuic): address review round 11 findings - internal/sub/json_service: skip tuic protocol in json subscription to prevent direct routing leak - internal/sub/clash_service: honor externalProxy/host row allowInsecure, sni, and alpn in buildTuicProxy - internal/web/runtime: decouple tuic inbound add/delete from xray restart - internal/tuic/config: restore user log-level options (warn, error) without forced info clamp - frontend/src/lib/xray/inbound-link: fix duplicate remark suffix and apply externalProxy TLS overrides - frontend/src/schemas/protocols/stream/external-proxy: propagate allowInsecure through host mapping - tests: add coverage for json sub skip, clash proxy overrides, and link generation * fix(tuic): meter inbound traffic through a UDP relay and bracket IPv6 binds Review repairs on the TUIC v5 sidecar integration: - Inbound traffic was read from the sidecar's /proc/<pid>/io rchar, but the kernel only counts read()/write() there and tuic-server moves its sockets with recvfrom/recvmmsg/sendmmsg/sendto, so an inbound's up/down stayed at 0 forever and inbound total limits never tripped (measured: 12 MiB relayed, rchar delta 0). The panel now owns the inbound's public UDP port with a small relay and runs tuic-server behind it on a loopback port, counting up/down exactly on every OS. tuic-server therefore logs 127.0.0.1 as every client's address; per-client attribution stays unsupported since QUIC is opaque. - Instance.BindTo formatted an IPv6 listen address as ":::8443", which tuic-server rejects with "invalid socket address syntax", so an inbound listening on "::" or any IPv6 literal never started. It now uses net.JoinHostPort; IPv4 output is unchanged. - The log level is passed to the sidecar as chosen. Online status, last-online and delayed start are read from its Info lines, so the Log Level field now says that Warn and Error switch them off for the inbound, and the docs say the same. - Drop two frontend tests that only exercised a getter and a set lookup, and strip the trailing blank line that made gofumpt fail on two of the new Go test files. * fix(tuic): harden tag updates, runtime routing, and relay stability --------- Co-authored-by: poise52 <equipoise52@gmail.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
89ee1242bd |
feat(sub): add read-only HWID device-slot status endpoint (#6380)
* feat(sub): add read-only HWID device-slot status endpoint Closes #6357 A client with an HWID limit had no way to tell a subscriber how many device slots were left: /{subPath}/{subId} only exposes the gate as a boolean through X-Hwid-* headers on a 404, and ?format=info carries no limitHwid or registered count. Every "why can't I connect on my new phone" case therefore had to be answered by the operator by hand. GET /{subPath}/{subId}/hwid-status now returns the aggregate counters: {"active":true,"limit":2,"registered":1,"remaining":1,"full":false} - SELECT-only. It never registers an hwid, never touches last_seen and never calls the enforcement path, so asking about a slot cannot spend one. - Counters only: no hwid value or hash, no email, no device metadata, no IP, no User-Agent, and none of the X-Hwid-* gate headers. - The subscription id is already the bearer secret for /{subPath}/{subId}, so no admin token and no new auth mechanism. - Unknown and disabled subscriptions both answer a bare 404, with identical status, headers and body, so the route cannot be used to probe which subscription ids exist. - No HWID limit configured returns {"active":false,"limit":0,...}. - No schema change and no migration. Scoped to enabled clients exactly like effectiveHwidLimitForSubID, so the reported limit is always the limit the gate enforces on a shared sub_id, and remaining clamps at zero when the effective limit drops below the number of registered devices. A separate route leaves /{subPath}/{subId}, ?format=info and the JSON/Clash routes byte-for-byte unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sub): document hwid-status as the bare object it returns The OpenAPI operation for GET /{subPath}/{subId}/hwid-status inherited the {success,msg,obj} panel envelope from build-openapi.mjs's default 200 response, while the handler writes the HwidSlotStatus struct bare. A client generated from the spec would read `obj` and never find the counters, and the description prose contradicted the schema with a hand-written example. HwidSlotStatus now sits in openapigen's StructAllow with example: tags, the entry references the generated schema through a `responses` block, and build-openapi.mjs attaches the generated example to any `responses` entry that $refs a generated schema, so no example is hand-written. The HEAD variant the controller registers is documented like its siblings, and the summary follows the "path prefix is configured by subPath" wording now that fresh panels randomise the prefix. Regenerated frontend/public/openapi.json, docs/public/openapi.json and the subscription-server MDX. openapi-runtime-contracts.test.ts pins the bare schema, the generated example and the HEAD operation. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
fc08b53395 |
feat(ui): add global command palette (Ctrl+K) for fast navigation and search (#6352)
* feat(ui): add global command palette (Ctrl+K) for fast navigation and search * fix(ui): address review feedback for shortcut listener, i18n parity, and search deep links * fix(ui): resolve search routing, translation keys, and palette state reset * fix(ui): improve command palette styling and sidebar transitions * fix(ui): address review feedback for typecheck, codegen, debouncing, and state reset * fix(ui): resolve effect state update warning and debounce reset in command palette * fix(ui): address review feedback for stale client search results and theme action * style(ui): apply oxfmt formatting to command palette and tests * fix(deps): update js-yaml override to resolve audit advisory * docs(api): sync the docs OpenAPI copy with the new InboundOption fields Adding Network/Security to InboundOption regenerated frontend/public/openapi.json, but docs/public/openapi.json is a hand-kept copy of that file and nothing checks it: make verify never reaches docs/, and docs-ci.yml fires only on docs/**. The two files were byte-identical on main and had diverged here, so the published API reference described a response shape the panel no longer returns. Regenerating the MDX under docs/content/docs/en/reference/api/ produced no change — the schema is read from the JSON at render time. * fix(ui): unnest the command palette row control and label its shortcut The palette row was a <button> wrapping the copy-subscription <button>. Nested interactive content is invalid HTML and React 19 logs two errors for it on every client result. The row is now a role="button" div using activateOnKey, the pattern the rest of the panel already uses, with line-height pinned so dropping the UA button style does not grow every row. Its keydown handler ignores events bubbling from the nested button: activateOnKey preventDefaults Enter, which would otherwise cancel the browser's Enter-to-click on the copy button and navigate instead. The sidebar chip hardcoded the Mac glyph while the handler accepts Ctrl as well, so Linux and Windows operators were shown a key they do not have; it now picks the modifier from the platform. Also restores the comment on ClientsPage's debouncedSearch that the deep-link change removed — the code it explains is unchanged. |
||
|
|
1456658028 |
feat(sub): add Happ client integration, routing presets, and app management (#6434)
* feat(sub): add Happ client integration, routing presets, and app management Implement comprehensive Happ proxy client integration according to official developer specifications. - Fix header emission on disabled routing and hidden settings to send explicit '0' headers rather than omitting, allowing Happ clients to reset cached settings. - Add support for 'happ://routing/off' deeplink in routing validation. - Preserve '?serverDescription=' query parameters in link fragments without escaping to support Happ server subtitles across VMess, VLESS, Trojan and SS. - Add Happ application management headers: ProviderID, New-Url, Fallback-Url, Sub-Info banners, Sub-Expire notifications, No-Limit mode, hardware ID enforcement, TUN modes/types, route exclusions, APNS exclusions, and per-app proxy settings. - Add curated routing presets (Iran Bypass, China Direct, AdBlock, Global) and interactive visual rule generator in frontend settings. - Synchronize all 13 translation locales with native Persian, Russian, and Chinese translations. * fix(sub): keep Happ header overrides behind the auto-detect opt-in The Routing-Enable/Hide-Settings off values were emitted on the User-Agent alone, so every panel that upgraded would push "Routing-Enable: 0" — documented by happ.su as disabling routing globally — to every Happ client without the operator enabling anything. They now ride subHappAutoDetect like every other Happ header. Two further mismatches against the vendor spec: - serverDescription was written as a key of the VMess base64 JSON object. happ.su documents it as a "#Title?serverDescription=<base64>" link parameter or a JSON "meta" entry, so the caption never reached Happ while every other VMess consumer received an unknown key. Dropped rather than moved: emitting the documented form is unsafe here because our own parser base64-decodes the whole VMess body (internal/util/link/outbound.go). - The TUN Mode dropdown stored the literal "default", forwarded as "Tun-Mode: default", where happ.su documents system|gvisor only. It now stores the unset value so no header is sent. TUN Type "default" is a documented value and is unchanged. Each fix carries a test that fails without it. |
||
|
|
d5ab84e8d5 |
feat(amneziawg): add AmneziaWG as an outbound protocol (#6320)
* feat(amneziawg): add AmneziaWG as an outbound protocol - AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI - Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test - Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions - Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test - Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go - Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients) - Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress) - Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts) - Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks * fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys - Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns - Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound - Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection - Trim comment blocks exceeding 2 lines across modified files - Fix Storybook test execution on environments with POSIX locale Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
d0edbcec81 |
feat(xray): update xray-core to v26.9.9 and follow the udpHop move
Bump xtls/xray-core to 52a412d9e2f5 (v26.9.9) and the three binary pins in DockerInit.sh and release.yml in lockstep. Upstream moved UDP port hopping out of finalmask.quicParams.udpHop and into a standalone "udphop" UDP mask with a different shape (mode / interval / remotePorts / remoteIPs). The old key is gone from QuicParams, and since the config loader ignores unknown fields it is now silently dropped rather than rejected — port hopping just stops. The panel adapts where that key was live: - Both link importers rebuilt quicParams.udpHop from the standard mport param, so an imported hysteria2 link produced an outbound that no longer hops. They now emit a udphop mask in intervalremote mode, which is what the old key did. The mode is required: UDPHop.Build() rejects an empty or unknown one. - validFinalMaskUDPTypes and UdpMaskTypeSchema learn "udphop", otherwise the Go link generator strips the mask from every link and sub, and Zod strips it on the next form round trip. - mport generation (Go and frontend) reads the mask first and keeps reading the legacy key, so inbounds stored before the upgrade still advertise their range. On an inbound the old key was always inert — only hysteria's dialer consumed it — so nothing regresses server-side and no migration is needed. udphop stays out of the mask dropdown on purpose: it is client-only in core, which refuses to wrap a server socket, and that form is shared with the inbound editor. |
||
|
|
c392f367e1 |
fix(dns): stop offering a port field that DoH entries discard
Xray ignores port for DoH/DoHL/DoQL, so valuesToWire deliberately stores none for an encrypted address and a non-standard port has to go inside the URL. The form kept offering the field anyway, pre-filled with the 53 from its own defaults: a port typed there was dropped on save and redrawn as 53 on reopen, which reads as the panel losing the value. Render the port field only where it is actually stored. DoT keeps it, since tls:// is not an encrypted-address scheme for this purpose. Closes #6403 |
||
|
|
47d2303334 |
fix(inbounds): reject missing TLS certificates before saving (#6429)
An inbound could be saved with security "tls" and a certificate row carrying neither a file path nor inline content. Nothing rejected it, so the row reached xray-core, whose readFileOrString fails with "both file and bytes are empty" and takes the whole config build down with it — every other inbound included. Validate the credentials on both sides of the wire. validateInboundTLSCertificates follows xray's file-over-inline precedence, requires a private key for every non-verify certificate and insists on at least one server certificate, so a verify-only CA list no longer passes as a server config. The inbound form's Zod schema enforces the same rules per field and serializes only the editor mode the operator actually used, and a failed save jumps to the Security tab naming the certificate row that broke. On update the guard is scoped to a real TLS edit. A row already stored incomplete is grandfathered: it stays editable, and only a save that breaks a previously valid block is refused. A sub-node stores whatever the master pushes, and Remote.UpdateInbound falls back to AddInbound when the node does not yet hold the tag, so a grandfathered row could otherwise never be deployed or re-seeded — the rejection is swallowed to a logger.Debug line and the node stays on a stale config while the panel shows the client as cut off. The controller now marks a node-sync request (mTLS or a node-sync token) on a per-request copy of InboundService, and the guard steps aside for it on both add and update: the row was judged where the operator acted, and a node that refuses it only falls out of sync. Operator and admin-token saves are held to the guard as before. The security union is parameterised on its tlsSettings branch instead of copied, and tlsCertUsesFiles is the one file-vs-inline inference shared by the form schema and the adapter, so the mode the editor opens in and the pair of fields the save serializes cannot drift apart. |
||
|
|
9f76a66dcf |
feat(sub): add dummy info node and status configs for subscriptions (#6412)
* feat(settings): add subInfoNodeEnable and status template settings * feat(sub): add dummy info node and status configs for raw links * feat(sub): support dummy info node in clash and json subscriptions * feat(ui): add subscription info node switch and status templates to settings * style: apply gofumpt formatting * fix(sub): address review feedback on subscription info node - Restore GetSubs contract to avoid unintended remark expansions on non-subscription-body calls. - Exclude dummy info node from Clash PROXY select group when active server nodes exist. - Track hasEnabledClient and set traffic.Enable in JSON and Clash paths so status tokens evaluate correctly. - Deterministically sort client emails across subscriptions before selecting primaryEmail. - Consolidate duplicated info-node evaluation logic into resolveInfoNodeRemark helper. - Remove redundant pure-getter test from setting_sub_info_node_test.go. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> |
||
|
|
b8597314f8 |
docs(api): mark collection responses nullable (#6430)
* docs(api): mark collection responses nullable Describe allLinks and panel log response objects as nullable string arrays so generated clients accept the existing nil-slice wire format. Pin both schemas with buildSpec regression assertions and regenerate the OpenAPI copies. * docs(api): include nullable Xray log responses Allow generated response arrays to opt into nullability while retaining their schema references and Go-derived examples. Apply this to Xray logs, whose nil slices already serialize as null, and pin the schema and example through buildSpec. |
||
|
|
3cd3836d77 |
fix(amneziawg): account for S4 junk in the default tunnel MTU (#6376)
* fix(amneziawg): account for S4 junk in the default tunnel MTU
amneziawg prepends S4 random bytes to every transport packet
(device.NewOutboundElement) and, unlike content padding and random trailers,
never clamps them against the tunnel MTU. A full-size packet therefore lands on
the wire at MTU + 60 + S4 bytes: 20 IPv4 + 8 UDP + S4 + 16 transport header +
16 poly1305 tag.
With the 1420 default that overflows a 1500-byte link once S4 exceeds 20, and
GenerateObfuscation31 draws S4 from 12..27 inclusive -- so roughly 44% of newly
created inbounds fragment every full-size packet they send.
Measured on a live pair of interfaces, predicted against observed:
MTU 1380 S4 12 -> 1452 on the wire (fits)
MTU 1420 S4 12 -> 1492 (fits)
MTU 1420 S4 20 -> 1500 (exactly at the limit)
MTU 1420 S4 21 -> 1501 (fragments)
MTU 1420 S4 27 -> 1507 (fragments)
EffectiveMTU now subtracts S4 from the default; an explicit MTU is untouched.
Client configs carry the same number. They previously omitted the MTU line
whenever the server had no explicit value, which left the client on its own
1420 default and fragmented the client-to-server direction even after the
server side was fixed -- silently, and only in one direction. All three
emitters (the Go subscription text and the two TypeScript ones) now agree,
which is what the existing parity test exists to protect.
* fix(amneziawg): rebuild the device when S4 changes the derived MTU
Addresses review feedback on the previous commit.
Deriving the default MTU from S4 made a construction-time-only property depend
on a hot-reloadable input, but addressFingerprint -- ensureLocked's only rebuild
trigger -- still hashed the raw inst.MTU. S4 is a UAPI field, so an S4-only edit
took the in-place IpcSet branch and the gVisor netstack kept the MTU derived
from the old S4 while all three client emitters already advertised the new one.
Every panel-created inbound leaves mtu unset, so that was the normal case, not
an edge one: with S4 raised far enough the fragmentation this fix exists to
remove came straight back, and stayed until a panel restart or an unrelated
address edit.
Folding EffectiveMTU into the fingerprint fixes it. An explicit MTU still takes
the in-place branch on an S4 edit, since it does not move the interface MTU.
Also trims four comment blocks to the 2-line cap in CLAUDE.md, and points
NewDevice's doc comment at EffectiveMTU instead of the deleted defaultMTU.
|
||
|
|
5a63d5d468 |
fix(mtproto): use hosts for public share links (#6369)
* fix(mtproto): use hosts for public share links Generate MTProto subscription, client, copy, QR, and export links from managed Hosts so reverse-proxied public ports are advertised correctly. Migrate the redundant legacy custom share address into a Host and keep old imports compatible. Closes #5126. * fix(mtproto): keep host share links lossless and consistent Address review on the MTProto hosts share-link change. The migration no longer drops a legacy custom share address: an unrelated (or disabled) Host stopped suppressing it, so only a Host already advertising the same address does. An imported address now clears the same validation the strict normalizer applies to every other protocol before it becomes a Host. Panel and subscription agree on the endpoint a Host advertises: a portless host string inherits the inbound port rather than the group's, and a port-only host inherits the inbound address instead of emitting server=%3A8443. LinksForClient prefers host endpoints for every protocol, the way getSubs and inboundLinks already do, so the client-links API no longer ignores managed hosts. * fix(mtproto): migrate legacy share address past unusable hosts The seeder skipped the conversion whenever any Host already carried the address, including one that is disabled or excludes the raw sub type. hostEndpoints drops those, so nothing advertised the address afterwards and the marker committed with no way back. The duplicate check now mirrors that same predicate. UpdateInbound cleared a legacy MTProto shareAddr without the Host conversion AddInbound runs, so re-applying an inbound definition through the API dropped the public address silently. Both paths share one capture helper now. Refresh the generated clients API reference for the summary reworded in the previous commit. * fix(inbounds): wait for the hosts list before building mtproto links The page destructured only `hosts` from useHostsQuery, and that list reads empty both while /panel/api/hosts/list is in flight and after it fails. withMtprotoHostEndpoints then returns the inbound untouched, so Copy, QR and Export advertise the internal listen port — the endpoint this branch exists to replace. It is worse than not fixing it: the seeder has already moved a legacy custom share address into a Host, so the fallback is the panel's own hostname instead of the operator's address, and the Go generators reading the same rows from the DB stay correct, so the two disagree for one inbound. Fold the query into the page's existing readiness gate, the same way useInbounds and HostsPage already consume that hook, so an empty list means "no hosts" rather than "not loaded yet". The error branch fires only when nothing is cached, so a refetch failing on window focus does not blank a page whose host rows are still perfectly usable. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
2ec6c73613 |
feat(xray): update xray-core to v26.9.8 and adapt panel
Bump xtls/xray-core to 37ceb8b4b6 (v26.9.8) and the three binary pins (DockerInit.sh, release.yml Linux + Windows) in lockstep. No deleted symbols; the impact is entirely on the JSON config surface. Outbound "proxySettings" is now refused by the config loader (moved to streamSettings.sockopt.dialerProxy) and a freedom outbound rejects sockopt.addressPortStrategy. Either key in a stored template would keep the core from starting after the upgrade, so a new OutboundRemovedKeysFix seeder rewrites xrayTemplateConfig once: proxySettings.tag becomes sockopt.dialerProxy (an existing dialerProxy wins) and addressPortStrategy is dropped from freedom outbounds. Template saves and outbound subscriptions already run through the vendored loader, so the new refusals surface there with the core's own message. REALITY no longer applies a built-in minClientVer (26.3.27) when the field is empty. The form placeholder and the min/max hints in all 13 locales now say that empty means no minimum. New upstream keys the Zod schemas would otherwise strip, with form support where a sibling field already had it: - blackhole response type "custom" with base64 customResponseData - realm finalmask ipMode (dual/v4/v6) and portMapping (UPnP / NAT-PMP) - quicParams brutalDisableLossCompensation, disableChromeParrot, disableGSO, disableStatelessReset - hysteria masquerade proxy xForwarded - wireguard outbound remoteDNS - routing rule localOS freedom.domainStrategy is only deprecated upstream (auto-migrated to sockopt.domainStrategy with a warning) and is left untouched. |