mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-20 21:05:02 +03:00
814cda3fb4d08c26ea2c22a9b2b2de24251899c9
83 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
814cda3fb4 |
feat(xray): update xray-core to v26.7.11 and adapt panel
Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins (DockerInit.sh, release.yml x2) in lockstep. Adapt the panel to the upstream changes: - Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from the core. A migration rewrites stored none/plain SS methods to a supported cipher and none/zero VMess security to "auto" (on both the clients column and inbound settings JSON); the SS build-time heal does the same so a row injected after boot cannot brick startup. The removed values are dropped from every frontend option list, schema and adapter, and coerced to "auto" at the Go link/sub/Clash emit sites and both link importers. Fix the CipherType_NONE sentinel that no longer compiles. - Unencrypted vless/trojan outbounds to a public address are now refused by the core. Validate outbounds through the vendored config loader when saving the xray template and when storing/merging outbound subscriptions, so one such outbound cannot keep the core from starting. - New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub link allowlist, the frontend enum and the FinalMask form (hostname, usernames, required password), and document it. - streamSettings gained a "method" alias for "network"; canonicalize it to "network" at inbound save time and in the form adapters/schema so a method-keyed config keeps its transport. - New root "env" config key is passed through xray.Config, compared in Equals, and forces a restart in the hot diff. - REALITY now defaults minClientVer to 26.3.27; update the form placeholder. |
||
|
|
61e12e4c29 |
Frontend dev tooling (Husky, lint-staged, MSW, Storybook) + full React Hook Form migration (#5859)
* chore(frontend): add husky + lint-staged pre-commit gate Wire a local pre-commit gate that runs eslint --fix on staged frontend TypeScript via lint-staged. Because the only package.json lives in frontend/ while the git root is one level up, the prepare script installs husky hooks at frontend/.husky from the repo root (cd .. && husky frontend/.husky), and the pre-commit hook cd's into frontend/ before invoking lint-staged so node_modules resolves. * test(frontend): add MSW request mocking Add Mock Service Worker so tests can exercise the real http-init.ts request pipeline (CSRF acquisition, 403 refetch-and-retry, body parsing) instead of only stubbing HttpUtil. A node setupServer is started for the vitest unit project with onUnhandledRequest bypass so the existing HttpUtil spies and 55 component tests are untouched; the browser worker is copied to public/ for Storybook and dev use. * chore(frontend): add Storybook + component stories Set up Storybook 10 on the React-Vite builder (compatible with the pinned Vite 8.1.3 and React 19). The preview decorator mirrors the vitest component harness: an Ant Design ConfigProvider with a light/dark toolbar toggle and an en-US i18next instance. main.ts neutralizes the app vite config bits that do not belong in a component workshop (the three-entry rollup input, renderBuiltUrl, and the shared dist outDir) so build-storybook can never clobber internal/web/dist. Seeds stories across the presentational library (viz, ui, clients, feedback). build-storybook is a local tool and is not wired into the CI gate. * feat(frontend): add React Hook Form primitives Introduce the shared RHF layer that AntD inputs bind through, ahead of migrating the forms off Ant Design's Form store: - FormField wraps a Controller in an Ant Design Form.Item shell, reconciling the value/onChange shapes of Input, Switch, InputNumber, Select and friends via normalizeAntdOnChange, with input/output transforms and Zod-issue-key error messages resolved through t(). - useZodForm wires zodResolver (Zod 4) with the AntD-matching modes (validate on submit, then live) and shouldUnregister false so hidden and unmounted-tab fields keep their values. - rhfZodValidate covers the rare per-field rule sites. Covered by a FormField test exercising normalization, transforms, and resolver error surfacing. * refactor(frontend): migrate Pattern-B leaf forms to React Hook Form Move the controlled-useState leaf forms onto RHF via the FormField primitive, keeping Ant Design components and each form's exact submit behaviour (same safeParse, same toast on the first Zod issue, same payload building): - clients: ClientBulkAdjustModal, BulkAddToGroupModal, ClientBulkAddModal - xray: RuleFormModal, BalancerFormModal, WarpModal, NordModal Multi-control widgets that don't fit a single input (inbound dual select, subId regen, expiry branches, the balancer tag warning) stay as explicit Controller/setValue. Derived visibility now reads live values through useWatch. FormField gains a required prop so migrated fields keep their required-asterisk affordance. Settings tabs are intentionally excluded: they are control-panel components that live-patch a parent AllSetting via SettingListItem, not Ant Design Form submit-forms. * refactor(frontend): migrate LoginPage to React Hook Form Replace the Ant Design Form store + antdRule per-field validation with useForm + FormField. The AntD Form stays as the layout/submit wrapper, now driving methods.handleSubmit(onSubmit) via onFinish. Username and password validate through rhfZodValidate(LoginFormSchema.shape.*); the two-factor field keeps its conditional required rule (only registered when 2FA is enabled). Submit posts the same values to /login. * refactor(frontend): migrate ClientFormModal to React Hook Form Move the client add/edit form off controlled useState onto RHF while preserving exact submit behaviour (same ClientFormSchema / ClientCreateFormSchema safeParse, same toast, same payload + attach/ detach diff + external-links build). expiryDate is stored as an epoch number (never a Dayjs) to survive RHF's value cloning, converted at the DateTimePicker boundary. externalLinks uses useFieldArray with stable ids. inboundIds and the derived show*/ss2022 visibility read live via useWatch. Space.Compact button-group widgets stay manual Controllers so the joined borders keep working. * refactor(frontend): migrate Node and DNS modals to React Hook Form Both are self-contained Pattern-A forms (no shared fragments). Replace Form.useForm with useForm + FormProvider, Form.useWatch with useWatch, setFieldValue with setValue, and partial validateFields([...]) with methods.trigger([...]). Per-field antdRule becomes rhfZodValidate rules; the Node scheme->tlsVerify cascade moves to FormField onAfterChange; the DNS domains/expectIPs/unexpectIPs string arrays are driven by useWatch + setValue. Submit runs through handleSubmit on the modal OK button, preserving each form's exact validation, payload build, and save/onConfirm behaviour. * refactor(frontend): migrate HostFormModal to React Hook Form The host external-proxy editor's outer form moves to useForm + FormProvider. Security/tab visibility reads via useWatch; the three json-form editors (HostMuxForm/HostSockoptForm/HostFinalMaskForm) are bound as value/onChange black boxes through a Controller (their own internal forms are unchanged). remark/inboundId keep their validation via rhfZodValidate; submit runs through handleSubmit and builds the same payload (isDisabled = !enable) and save call. * refactor(frontend): migrate OutboundFormModal + fragments to React Hook Form Move the outbound form cluster off Ant Design's Form store onto RHF. The parent uses useForm + FormProvider with a watch() subscription for the protocol reseed cascade and setValue-based network/security/xmux cascades; the JSON<->Basic bridge and the formValuesToWirePayload submit are preserved exactly. Every outbound transport/protocol/security fragment now binds through FormField/useWatch via context. The shared config editors stay untouched and are bound through small value/onChange adapters (src/lib/xray/forms/fields: FinalMaskField, SniffingField, SockoptCustomField) via Controller; HeaderMapEditor binds directly. The host json-form wrappers that reuse the outbound MuxForm/ SockoptForm (HostMuxForm, HostSockoptForm, OutboundSubtreeJsonForm) move to a local RHF provider to match. Outbound render/link tests pass unchanged. * refactor(frontend): migrate InboundFormModal + fragments to React Hook Form Move the inbound add/edit form (the largest form in the panel) and its transport/protocol/security fragments off Ant Design's Form store onto RHF, mirroring the outbound migration. The parent uses useForm + FormProvider with a watch() subscription for the protocol reseed cascade (type==='change' guard so programmatic resets don't reseed) and setValue-based network/security cascades; useSecurityActions drives the TLS/Reality keypair + scan through setValue. Hidden pass-through Form.Items are dropped (their values ride in the reset object and survive via shouldUnregister:false), so getValues() still returns the settings.clients subtree untouched. accounts / certificates / tun lists use useFieldArray; the shared FinalMask/Sniffing/Sockopt editors bind through the value/onChange adapters. Submit keeps the manual InboundFormSchema.safeParse + formatInboundValidation toast + formValuesToWirePayload exactly. The golden link/full fixtures pass byte-for-byte, confirming identical wire output. inbound-form-blocks test harness rewritten from a Form.useForm harness to an RHF provider. * refactor(frontend): retire antdRule; document the RHF form pattern All forms now build on React Hook Form, so the AntD-Form Zod adapter antdRule (src/utils/zodForm.ts) has no remaining callers — remove it. Update frontend/CLAUDE.md: forms use useZodForm + FormField from components/form/rhf with zodResolver/rhfZodValidate validation; AntD <Form> is layout-only; the shared FinalMask/Sniffing/Sockopt editors stay AntD islands wrapped as value/onChange adapters bound via a Controller. * chore(frontend): cover esbuild in the allowScripts allowlist esbuild (pulled in transitively by Vite/Vitest/Storybook) ships a postinstall that npm's allow-scripts flags as uncovered on every install. Its platform binary is delivered through the @esbuild/<platform> optionalDependencies, so the postinstall isn't needed here; deny it like the other entries to silence the warning. * fix(frontend): restore label layout in Sniffing/FinalMask field adapters The value/onChange adapters that wrap the shared SniffingFields and FinalMaskForm editors put them in their own isolated AntD Form, but that Form was missing the label layout the fields used to inherit from the inbound/outbound parent form. Their labels rendered full-width instead of the compact right-aligned column, so the Sniffing tab and the TCP Masks / QUIC Params sections looked broken. Give both adapter forms the same colon=false, labelCol/wrapperCol span 8/14, labelWrap layout. * ci: add least-privilege permissions to Docs CI workflow The docs-ci workflow had no explicit permissions block, so it inherited the repository default for GITHUB_TOKEN. The build job only checks out and builds the docs, so restrict it to contents: read, resolving the CodeQL actions/missing-workflow-permissions alert. |
||
|
|
6e75938c61 |
[Feature]: Add a tooltip/hint to the "Password" field in the client form clarifying which protocols use it (#5809)
* feat(clients): clarify which protocols use the Password and Hysteria Auth fields Add tooltips to the Password and Hysteria Auth Form.Items in the client form, explaining that Password is only consumed by Trojan and Shadowsocks (ignored for VLESS, VMess, Hysteria, WireGuard) and that Hysteria Auth is the credential Hysteria actually uses. Adds passwordDesc/hysteriaAuthDesc keys to all 13 locale files, following the existing limitIpDesc/totalGBDesc tooltip convention. Closes #5803 * test(clients): assert Password/Hysteria Auth tooltip hints render |
||
|
|
43500a5470 |
feat(mtproto): per-client ad-tags, management-API auth, and record secret sync
Catch the panel up to the mtg-multi README (v1.14.0): - Each client can now carry its own 32-hex advertising tag overriding the inbound-level one. The tag lives on the client (settings JSON is the source of truth, clients.ad_tag is the UI projection), is rendered into the fork's [secret-ad-tags] section for active secrets only (mtg rejects a config whose override names an unknown secret), is pushed per entry through PUT /secrets, and is part of the reload fingerprint so a tag edit hot-applies without dropping connections. - The loopback management API can replace the whole secret set, so every mtg process now gets a random per-process api-token; the manager sends it as a bearer token on PUT /secrets and GET /stats and reuses it across config rewrites, because mtg reads the token only at startup. - Malformed tags are rejected at every save path and additionally dropped in InstanceFromInbound: one bad tag would otherwise fail the whole generated config and take every client of the inbound down with it. - SyncInbound never copied a re-keyed mtproto secret into the canonical clients table, so the clients page and subscription links kept serving the old secret, which mtg then rejects. It is now guarded-copied like the other credentials. |
||
|
|
d97bd8643e |
feat(mtproto): adopt dolonet/mtg-multi and make MTProto inbounds multi-client
Replace the upstream 9seconds/mtg sidecar with the dolonet/mtg-multi fork so a single MTProto inbound can serve many per-user secrets. Each panel client is now one named FakeTLS secret in the fork's [secrets] section: clients are first-class (attach/detach, limits, expiry, per-client tg:// links) exactly like every other protocol, mirroring the WireGuard multi-client model. Per-client traffic and online status come from the fork's /stats JSON API (its Prometheus output has no per-user label), fed into the existing email-keyed client_traffics accumulator; an optional throttle caps concurrent connections. A one-time seeder converts each legacy single-secret inbound into a one-client inbound. The fork ships only linux/darwin amd64/arm64 binaries but is pure Go, so provisioning builds it from source for every supported platform (release.yml, DockerInit.sh) while keeping the panel-expected mtg-<os>-<arch> filename and the 'run' verb, so process.go is untouched. Also fixes a pre-existing update.sh gap that never renamed the mtg binary for armv6/armv7 updates. |
||
|
|
b177e30714 |
feat(ui): client-realtime-speed (#5687)
* refactor(inbounds): extract TRAFFIC_POLL_INTERVAL_S to shared util * feat(clients): derive per-client live speed from traffic WebSocket deltas * feat(clients): render speed column and mobile card line * i18n(clients): add pages.clients.speed key to all 13 locales |
||
|
|
052dd85ad3 |
feat(clients): hide disabled inbounds in the client form selector
The attach-inbounds select in the client add/edit modal listed every inbound, so panels with many disabled inbounds had to scroll past dead entries. InboundOption now carries the inbound's enable flag and the form drops disabled inbounds from the options, keeping ones the client is already attached to so edit mode still renders existing assignments. Closes #5645 |
||
|
|
f90e4a6962 |
fix(panel): use the hosting node address for WireGuard client configs (#5679)
* fix(panel): use the hosting node address for WireGuard client configs The clients page rendered a node-managed WireGuard inbound's config with the master panel's host in Endpoint instead of the hosting node's address, so the copied/QR config pointed at the wrong server. The subscription path already resolves this via resolveInboundAddress; the UI generator did not. Expose the share-host resolution inputs (node address, listen, share-address strategy/address) on InboundOption and route buildWireguardClientConfig through the same canonical resolver the inbounds-page share links use, extracted as resolveShareHost. This also brings local inbounds with a shareable listen or a listen/custom share strategy into parity with the subscription Endpoint; the common listen=0.0.0.0 case still falls back to the panel host. * fix(frontend): keep a raw fallback host and refresh node-fed inbound options Code review of the WireGuard node-endpoint change surfaced two gaps. resolveShareHost normalized its last-resort fallbackHostname, so a panel reached via a hostname the share-host grammar rejects (underscore label, trailing-dot FQDN) emitted a broken 'Endpoint = :51820'; the fallback now stays verbatim when normalization empties it. Node mutations only invalidated the nodes query, leaving the staleTime-Infinity inbound options cache serving an edited node address until the sync job broadcast (never, for disabled/offline nodes); they now invalidate the options key too. Also folds the ShareHostFields projections into direct structural passes, elides the default node shareAddrStrategy so omitempty drops it, and replaces the nullable node-address scan with COALESCE. --------- Co-authored-by: STRENCH0 <17428017+STRENCH0@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
1afab47f04 |
feat(frontend): show client group in the client info modal
The group label was already on ClientRecord but the info modal never displayed it. Add a conditional row next to the comment, rendered as a geekblue tag to match the group column in the clients table. |
||
|
|
64c306037f |
feat(wireguard): make client allowedIPs editable with validation
The WireGuard peer address was allocated server-side and shown read-only in the client editor, so changing it required hand-editing the inbound's raw settings JSON (#5715). The backend add/update paths already honored a submitted allowedIPs; only the form withheld it. Make the field editable (comma-separated, empty still auto-assigns) and validate submissions server-side: entries must parse as an IP or CIDR, bare addresses normalize to single-host prefixes, and an address already used by another peer on the inbound is rejected. Closes #5715 |
||
|
|
71aca2018a |
feat(a11y): screen-reader & keyboard accessibility across the panel (#5486) (#5652)
* feat(a11y): label list, toolbar & dashboard actions for screen readers Phase 1 of #5486 (Android TalkBack support). Icon-only controls across the management surfaces previously announced only their untranslated icon name (e.g. "edit", "ellipsis") or nothing at all. - Add aria-label to icon-only row-action and toolbar buttons across inbounds, clients, groups, hosts, nodes and xray (outbounds/routing/dns/balancers) lists, plus the dashboard cards. - Make clickable bare icons and AntD Card actions keyboard-operable via role/tabIndex + Enter/Space (new activateOnKey helper); convert mobile dropdown triggers to buttons so they open from the keyboard. - Fix the sidebar hamburger's mislabeled aria-label (was the dashboard label) and translate previously-hardcoded outbound menu labels. New i18n keys in all 13 locales: sort, menu.openMenu, pages.xray.outbound.moveToTop. * feat(a11y): label modal, QR and copy/download controls for screen readers Phase 2 of #5486. Modal and overlay controls relied on tooltips (not a reliable accessible name) or were bare clickable icons with no keyboard or screen-reader support. - Add aria-label to copy/QR/download/info icon buttons in the inbound and client info modals, sub-links modal, QR panel, backup/log modals, and to the bare search/select inputs of the attach/detach client modals. - Make click-to-copy QR codes and the IP-log refresh/clear, geofile reload and log refresh icons keyboard-operable (role/tabIndex + Enter/Space) with translated labels. - Label the 2FA code input; drop the QrPanel download-image string fallback now that the key exists. New i18n key in all 13 locales: downloadImage. * feat(a11y): label form fields and shared form components for screen readers Phase 3 of #5486. Form controls and shared form widgets were largely unlabelled, and several remove controls were not keyboard-operable. - SettingListItem now ties its title to the control via aria-labelledby, giving accessible names to the ~90 settings-tab inputs at once. - InputAddon gains button semantics (role/tabIndex/Enter+Space) and an ariaLabel prop when used as an interactive remove control. - Sparkline charts expose a role="img" summary of their latest values. - Add aria-label to add/remove/regenerate icon buttons and bare inputs/selects across inbound, client and xray (dns/routing/balancer/ outbound) forms; make clickable remove icons keyboard-operable; mark decorative help/target icons aria-hidden; label the JSON editor, date-time clear button, header-map remove, notification select-all and remark token chips. New i18n keys in all 13 locales: regenerate, jsonEditor, pages.xray.balancer.{costMatch,costValue,costRegexp}. * chore(a11y): add eslint-plugin-jsx-a11y harness and fix flagged interactions Phase 4 of #5486. Adds eslint-plugin-jsx-a11y (recommended ruleset, scoped to .tsx) so screen-reader/keyboard regressions fail lint. - Make the mobile node-card header a proper keyboard disclosure (role=button, aria-expanded, Enter/Space activation that ignores clicks on the nested action buttons) and drop the now-redundant stop-propagation click handlers the linter flagged on card-action wrappers in the node, client and inbound mobile cards. - Disable jsx-a11y/no-autofocus: the autofocus on the login field and modal primary inputs is intentional focus management that helps screen-reader and keyboard users land on the right control. make lint passes with the a11y ruleset enforced. * feat(a11y): cover remaining deferred spots (settings tabs, sockopt, API docs) Completes the panel sweep for #5486 by labelling the spots previously left out of phases 1-4: - NotifyTimeField (Telegram notifications): the mode, interval, unit and custom-cron inputs now carry aria-labels. - The Sockopt toggle in transport options. - Settings category tabs in icons-only (mobile) mode now expose the tab name as the icon's aria-label instead of the raw icon name. - The Swagger API-docs view is wrapped in a labelled region landmark. New i18n keys in all 13 locales: pages.settings.notifyTime.{interval,unit}. * feat(a11y): label shared xray form components and remark field Code review surfaced frontend/src/lib/xray/forms/ — shared form components used by the host and inbound JSON forms — which the initial audit missed. - FinalMaskForm (TCP/UDP final-mask editor): label the icon-only add and regenerate buttons and make all six remove icons keyboard-operable (role/tabIndex/Enter+Space); adds useTranslation to its sub-components. - CustomSockoptList: the remove icon is now keyboard-operable. - SniffingFields: aria-label on the otherwise label-less destOverride select. - RemarkTemplateField: aria-label on the remark-variable picker button. New i18n key in all 13 locales: pages.inbounds.sniffingDestOverride. * feat(a11y): label client info modal and WireGuard config block After rebasing onto the WireGuard client-config feature, re-apply the ClientInfoModal copy/QR/IP-log aria-labels (the modal was restructured upstream, so the original labels did not carry over) and label the new ConfigBlock component's copy/download/QR actions. ConfigBlock's action wrapper keeps its stop-propagation handler (a non-interactive guard for the Collapse header) under a scoped jsx-a11y exception. * fix(frontend): let npm install jsx-a11y under ESLint 10 eslint-plugin-jsx-a11y@6.10.2 declares a peer range that stops at ESLint 9, but the panel is on ESLint 10, so `npm ci` aborts with ERESOLVE even though the plugin runs fine on ESLint 10 with flat config. Add an npm override so jsx-a11y accepts the project's ESLint version. This keeps normal peer resolution (recharts' react-is peer still auto-installs) — no global legacy-peer-deps and no manual react-is pin needed. * fix(a11y): size mobile row triggers and move node expand role to chevron Address automated review on #5652: - add size="small" to the inbound/client/node mobile-card "more" dropdown triggers so they match the adjacent small Switch and the established desktop RowActions pattern. - move the node card-head disclosure semantics (role/tabIndex/aria-expanded/ keyboard) onto the chevron affordance so the expand control is no longer a role="button" wrapping the Switch, info button and dropdown. Mouse click-anywhere-to-expand is preserved on the header div. |
||
|
|
6c71b725da |
fix(clients): hide WireGuard config after detaching the WG inbound
The client info and QR modals rendered a WireGuard config whenever the client still carried leftover WG key material (privateKey / publicKey / allowedIPs / preSharedKey / keepAlive), regardless of whether a WireGuard inbound was actually attached. After detaching the WG inbound the config kept showing, built with an empty endpoint port and public key. Gate wgConfigText on an attached WireGuard inbound (wgInbound) being present, not just isWireguardClient(client), in both ClientInfoModal and ClientQrModal. Also rename the i18n key pages.clients.conf -> config and add the missing pages.clients keys (wireguardConfig, config, bulkFlow, bulkFlowNoChange, bulkFlowDisable) to all 12 non-English locales so each one matches en-US. |
||
|
|
a329882e0e |
feat(wireguard): client config UX, collapsible config card, configurable DNS
Land the WireGuard client-config UX work on main (the upstream PR #5642 branch could not be pushed to). - Reusable collapsible ConfigBlock (copy/download/QR, actions aligned right) for the client .conf, used by client info and the public sub page. - Correct .conf: canonical PresharedKey casing and DNS sourced from the inbound (configurable per-inbound, default 1.1.1.1, 1.0.0.1). - Configurable per-inbound DNS for WireGuard (schema + form + backend hint via InboundOption.WgDns); inert at the Xray layer. - Public sub page now shows the WireGuard config, rebuilt from the share link; the Go wireguard:// link carries dns/presharedkey/keepalive for completeness. - QR enabled for the wireguard:// link; link rows are compact like other protocols. - Client information order is subscription, copy URL, WireGuard config; the redundant config tab is removed from the add/edit client modal. - Drop the Inbound Information and QR Code row actions for WireGuard inbounds. |
||
|
|
9c8cd08f90 |
feat(wireguard): multi-client support
WireGuard inbounds now manage per-client peers using xray-core's native WireGuard users (AddUser/RemoveUser). Each client lives in settings.clients (canonical, like every other protocol) and is projected to peers[] only when emitting the xray config, at level 0 so the dispatcher's per-user traffic/online counters work with no extra plumbing. Backend: internal/util/wireguard gains KeyToHex (base64 to hex for the gRPC path), PublicKeyFromPrivate and GenerateWireguardPSK; xray/api.go builds a wireguard account in AddUser with hex keys (RemoveUser already worked); client CRUD generates a keypair and allocates a unique tunnel address per client and never rotates keys on edit; an idempotent migration converts legacy settings.peers into managed clients; WireGuard is included in the raw subscription. Frontend: WireGuard in the add-client modal with keys on the credential tab, client schema, per-client QR/link/.conf, inbound form reduced to server settings; i18n added across 13 locales. Fix: guard the settings[clients] assertion in add/update so a legacy WireGuard inbound stored without a clients key no longer panics. |
||
|
|
6964d84742 |
feat(reality): add live REALITY target scanner with IP/CIDR discovery
Replace the static reality-targets list with a server-side TLS 1.3 probe that checks TLS 1.3 + HTTP/2 + X25519 + a trusted certificate. - Single-domain validate auto-fills target and serverNames from the cert SAN - Discovery scans an IP/CIDR without SNI to find new targets from their certificates, deduped and ranked by feasibility then latency, private-IP guarded via netsafe - New endpoints scanRealityTarget and scanRealityTargets with RealityScanResult, plus openapigen and api-docs entries - Add scanner strings to all 13 locales - Replace deprecated AntD Alert message prop with title across the panel |
||
|
|
e64e998194 |
feat(clients): add bulk enable/disable and move selection actions into More menu
Add bulkEnable/bulkDisable named endpoints backed by a shared internal impl, and consolidate the per-selection actions (attach, detach, add to group, ungroup, enable, disable, adjust, sub links) into the clients table's More dropdown so the toolbar only shows the selection count and delete. Translate the new enable/disable confirm dialogs and toasts across all 13 locales. |
||
|
|
23e73cd4a3 |
fix(clients): use new email after rename and de-duplicate save toast
On client edit the post-update calls (attach/detach/externalLinks) keyed by the original email, so renaming a client made setExternalLinks fail with record-not-found. Key them by the updated email instead. Each of those sub-step POSTs also auto-toasted its own success, so a save fired the 'Inbound client has been updated' toast twice (or more). Add a silentSuccess HttpUtil option that suppresses the redundant success toast while still surfacing errors and the node-offline warning, and apply it to the attach/detach/externalLinks mutations. |
||
|
|
b0c1156dd6 |
fix(sub): drive display remarks from the template and split multi-host subpage links
Unify remark generation around the Remark Template. Display contexts (Clients-page QR/Info modals and the HTML sub info page) now render the template name-only client/identity part instead of a hardcoded fallback; the subscription body keeps the full template on a client first link and name-only thereafter. The default template gains the email token so the client email shows by default again (#5532). BuildPageData now splits each multi-link entry (one link per host of an inbound) into a separate row, so the sub page no longer collapses several host links onto a single mangled line. QR captions on the Clients QR modal and the sub page reuse the link fragment remark. |
||
|
|
5dbd5b1d12 |
fix(sub): restore client email in panel copy/QR link remark (#5532)
Display-context links (Clients page QR + Information modals and the sub info page) dropped the client email from the link fragment in 3.4.0, showing only the inbound remark. Append the email back so the imported profile keeps its per-client label: inbound-host-email when a host is set, inbound-email otherwise. The usage template stays bypassed in display context, so no traffic or expiry data leaks. |
||
|
|
14de0557f9 |
feat(clients): bulk-set XTLS flow from the Adjust dialog (#5524)
* feat(clients): bulk-set XTLS flow from the Adjust dialog Add a "Set flow" dropdown to the bulk Adjust dialog so an admin can set or clear the XTLS flow on all selected clients at once, alongside the existing days/traffic bumps. Empty by default (no effect on save); "Disable" clears flow, and the two vision values mirror the per-client credential tab. Flow rides the existing inbound-JSON -> SyncInbound path (ClientRecord.Flow + client_inbounds.flow_override), so no new endpoint, DB column, or migration. Setting a vision flow is gated by inboundCanEnableTlsFlow: ineligible inbounds are left untouched and reported as skipped; clearing is always allowed. A real flow change requests an xray restart (local) or a node reconcile (remote). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(clients): keep days/traffic write when bulk flow is ineligible Address review on the bulk-flow-adjust PR: - Blocking: a client adjusted with both a days/traffic delta and a flow directive on a flow-ineligible inbound had the flow-ineligibility recorded into the same skip set that gates the ClientTraffic write, so the inbound JSON / ClientRecord advanced but ClientTraffic did not — divergent stores, and the client misreported as skipped. Track flow ineligibility in its own map (bulkInboundAdjustResult.flowIneligible) so it only feeds the final Skipped report and never suppresses the expiry/total persistence. - Drop the broad delete(skippedReasons, email): flow reasons no longer enter skippedReasons, so honoring a flow can no longer erase an unrelated skip reason (unlimited expiry, a real persistence error on another inbound). - Drop the inline comment block from ClientBulkAdjustModal.tsx (file had none); move the whitelist-sync note next to bulkFlowAllowed, the source of truth. - Document the optional flow field in the bulkAdjust API-docs example (endpoints.ts) and regenerate openapi.json. - Add a regression test covering days+flow on an ineligible inbound. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ce8b1bed77 |
feat(iplimit): gate IP limit on fail2ban and reset stale limits
Per-client IP limit only enforces where fail2ban is installed, so the panel now reports enforceability and disables the field otherwise: - Add GET /panel/api/server/fail2banStatus (enabled/installed/usable/windows), cached 30s. - ClientFormModal and ClientBulkAddModal disable the IP Limit input when not usable and show a hover tooltip; Windows gets a platform-specific message instead of the bash-menu hint. - One-time migration ResetIpLimitNoFail2ban zeroes existing client limitIp (inbound settings JSON + clients table) on hosts without fail2ban, where the limit never applied. - Drop the recurring '[LimitIP] Fail2Ban is not installed' warning. - Add limitIpFail2banMissing/limitIpFail2banWindows/limitIpDisabled across all 13 locales. |
||
|
|
0b0b6250d6 |
feat(clients): orphan cleanup + export/import via CodeMirror modals
Add three client-management actions to the Clients page More menu:
- Delete unattached clients: removes every client with no inbound
attachment, cascading its traffic rows, IP log, and external links
(POST /clients/delOrphans).
- Export clients: shows the {client, inboundIds} list in a read-only
CodeMirror viewer with copy/download (GET /clients/export returns the
array in the standard envelope).
- Import clients: pastes that JSON into an editable CodeMirror editor,
mirroring Import an Inbound (POST /clients/import takes a { data }
body). Attached clients go through the create-and-attach path; items
with no inboundIds are restored as bare records; existing emails are
never overwritten and are reported as skipped.
Document the new endpoints in api-docs and translate the new strings
into all supported languages.
|
||
|
|
709b332d17 |
feat(hosts): managed Hosts for per-host subscription link overrides (#5409)
* test(sub): characterize current link output (externalProxy + single-link baselines)
Phase 0 of the Hosts feature. Locks current subscription-link output for the
externalProxy paths (vless/vmess/trojan/ss exact, reality/hysteria by Contains)
so the upcoming ShareEndpoint refactor can be proven behavior-preserving. These
must stay green and unedited through every later phase.
* refactor(sub): unify external-proxy link building behind ShareEndpoint (TDD, snapshot-locked)
Phase 1 of the Hosts feature. Collapse the duplicated externalProxy link
builders (param-form for vless/trojan/ss, object-form for vmess) onto a single
ShareEndpoint abstraction so Phase 4 can add Host-driven links with ~zero new
branching.
Design: an externalProxy-derived endpoint carries the original entry map and
applies it through the UNCHANGED applyExternalProxyTLS{Params,Obj} helpers, so
output is provably byte-identical. buildExternalProxyURLLinks /
buildVmessExternalProxyLinks become thin adapters; the genVless/Trojan/SS/Vmess
call sites are untouched. genHysteriaLink is deliberately left on its own path
(hex pinSHA256, not pcs). The no-externalProxy default tails are unchanged.
TDD: N1-N4 (externalProxyToEndpoint, inboundDefaultEndpoint, buildEndpointLinks,
buildEndpointVmessLinks) written failing-first against stubs, then implemented.
Mutation sanity (performed + reverted): dropping the ep-carry in
externalProxyToEndpoint makes the Phase-0 C1/C2 characterization snapshots go
red (TLS overrides vanish), proving the snapshots guard the emitted output.
Gate: go test ./internal/sub/... and go test ./... green with ZERO edits to the
Phase-0 snapshots; go build ./... green on linux and windows; go vet clean.
* feat(model): Host entity + automigrate + openapi codegen (TDD)
Phase 2 of the Hosts feature. Adds the Host GORM model: an override endpoint
attached to an inbound (address/port + TLS/transport/clash overrides + sub
scoping), superseding the legacy externalProxy array functionally while leaving
it intact.
- model.Host with snake_case column tags, json serializer for slices, text for
free-JSON (mux/sockopt/xhttp), validate tags (remark 1-40, port 0-65535,
security + mihomoIpVersion enums); TableName "hosts". NodeGuids column is added
now but unused (host->node scoping deferred to v2).
- Registered in BOTH initModels() (db.go) and migrationModels() (migrate_data.go);
the latter is required for cross-DB migration and is easy to miss. PG sequence
resync iterates the initModels slice, so it is covered automatically.
- pruneOrphanedHosts() deletes hosts whose inbound_id has no inbound, called
alongside pruneOrphanedClientInbounds().
- openapigen manifest: Host added to StructAllow with MuxParams/SockoptParams/
XhttpExtraParams -> KindAny; regenerated frontend/src/generated/* + openapi.json.
TDD: TestHostTableName, TestHostValidation, TestHostAutoMigrateCreatesColumns
(+ _Postgres), TestPruneOrphanedHosts written failing-first against a wrong-name,
untagged, unregistered stub, then implemented.
Gate: go test ./... green on SQLite AND a real Postgres DSN (local container);
go build/vet/gofmt clean; npm run gen succeeds with the new Host type/schema/
example/zod; npm run typecheck + npm run test (542) green.
* feat(api): Host CRUD service + controller + routes (TDD)
Phase 3 of the Hosts feature.
- service/host.go (HostService, empty struct + database.GetDB() like
ClientService): GetHosts, GetHostsByInbound, GetHost, AddHost (verifies the
inbound exists — no hard FK), UpdateHost (inbound + sort order immutable here),
DeleteHost, SetHostEnable, SetHostsEnable, DeleteHosts, ReorderHosts (single
driver-safe transaction), GetAllTags.
- controller/host.go mirrors NodeController: routes under /panel/api/hosts
(list/get/byInbound/tags + add/update/del/setEnable/reorder + bulk/setEnable,
bulk/del), binds via middleware.BindAndValidate so the model validate tags are
enforced, {success,msg,obj} envelopes.
- Wired the hosts group into api.go after nodes (inherits checkAPIAuth + CSRF).
- DelInbound now cascades: deleting an inbound deletes its hosts.
- Documented all 11 routes in api-docs endpoints.ts (referencing the generated
Host schema) and regenerated openapi.json; extended TestAPIRoutesDocumented's
controller->basePath switch for host.go. Backend en toast keys added.
TDD: service tests (Add/GetByInbound, RejectsUnknownInbound, Reorder, Set/Bulk
enable, DeleteHosts, DeleteInboundCascadesHosts, GetAllTags) written failing-
first against a nil-returning stub; controller test (AddListGetDelete envelope
round-trip + AuthInherited 401) added.
Gate: go test ./internal/web/... + go test ./... green; npm run gen + typecheck
+ lint + test (542) + build green.
* feat(sub): render subscription links from hosts; legacy fallback when none (TDD, mutation-checked)
Phase 4 of the Hosts feature. Inserts host resolution between inbound and link
across all three subscription formats.
Mechanism: hostEndpoints(inbound, format) loads the inbound's enabled hosts
(filtered by ExcludeFromSubTypes, ordered by sort_order then id) and projects
each onto the externalProxy entry shape the raw/json/clash renderers already
consume. So a host fans out one link/proxy reusing the exact existing rendering
(address/port/security/sni/fp/alpn/pins/ech) with zero new TLS code. Host header
and path overrides are applied additively in the raw builders (no-op for legacy
externalProxy, which never carries those keys — characterization snapshots stay
green). Clash ip-version (MihomoIpVersion) is set last on the proxy.
Integration points:
- getSubs (raw): per inbound, hostEndpoints AFTER projectThroughFallbackMaster;
len>0 -> linkFromHosts (renders only the hosts), else legacy GetLink.
- GetJson/GetClash: inject the host endpoints into the inbound's externalProxy
before the existing getConfig/getProxies loop.
- Precedence: hosts win over any legacy externalProxy (injection replaces it).
Backward compat: a zero-host inbound takes the legacy path -> byte-identical
output (all Phase-0 characterization snapshots unchanged).
TDD: 9 cycles (zero-hosts identical, N-links-ordered with host/path override,
disabled skipped, host-vs-externalProxy precedence, no-dedup, sort composes with
SubSortIndex, host-over-fallback, resolve-via-client-inbounds, ExcludeFromSubTypes
per format) written failing-first against unwired helpers, then wired green.
Mutation sanity (performed + reverted, documented here):
- zero-hosts fallback: flipping the len(hostEps)>0 guard to >=0 makes
TestSub_ZeroHosts_IdenticalOutput go red (host path yields "" for no hosts).
- no-dedup: adding a remark-dedup in hostEndpoints makes TestSub_NHosts_NoDedup
go red (two distinct hosts collapse to one link).
Gate: go test ./internal/sub/... + go test ./... green with ZERO edits to the
Phase-0 snapshots; go build green on linux and windows; go vet + gofmt clean.
* feat(migration): seed hosts from inbound externalProxy (TDD, idempotent, dual-driver)
Phase 5 of the Hosts feature. One-time migration so existing installs surface
their legacy externalProxy entries as first-class Host rows.
- seedHostsFromExternalProxy() is self-gated on a HistoryOfSeeders
"HostsFromExternalProxy" row (run-once) and wired into runSeeders. For each
inbound it parses StreamSettings, reads externalProxy[], and creates one Host
per entry: forceTls->Security (unknown->same), dest->Address, port->Port,
remark->Remark (generated when blank, capped at 40), sni/fingerprint/alpn/
pinnedPeerCertSha256/echConfigList copied; SortOrder=index; InboundId set.
- Additive: externalProxy is left intact in StreamSettings (rollback-safe; the
sub layer prefers hosts when present, §Phase 4).
- Postgres: GORM db.Create advances hosts_id_seq via the sequence, so no extra
resync is needed beyond the existing startup resync.
TDD: field-mapping, idempotency (second run no-op), no-externalProxy->no-hosts,
externalProxy-kept-intact written failing-first against a stub; plus a
Postgres counterpart that skips without XUI_DB_DSN.
Gate: go test ./internal/web/service/... ./internal/database/... green on SQLite;
the *_Postgres tests green against a real Postgres container; go build green on
linux and windows; go vet + gofmt clean. (Running the whole database package
under XUI_DB_TYPE=postgres is not supported — the SQLite-path tests share the one
DSN — so only the t.Skip-gated *_Postgres tests run with the env set.)
* feat(ui): Hosts page + schema + query hooks + link preview helper (TDD on schema/helpers)
Phase 6 of the Hosts feature — the admin UI.
- schemas/api/host.ts: HostFormSchema (validation: remark 1-40, tags ^[A-Z0-9_:]+$
≤10×≤36, port 0-65535, security/mihomoIpVersion enums, alpn/fingerprint reused
from the shared primitives) + a loose HostRecordSchema/HostListSchema for reads.
- lib/hosts/host-link.ts: hostToExternalProxyEntry — the frontend mirror of the
backend hostToExternalProxyMap (security->forceTls, sni override rules, port
inherit), for share-link previews.
- api/queries/useHostsQuery.ts + useHostMutations.ts (mirror the node hooks):
list/get + add/update/del/setEnable/reorder/bulk; queryKeys.hosts.* added;
mutations invalidate keys.hosts.root().
- pages/hosts/{HostsPage,HostList,HostFormModal}.tsx (+CSS) mirroring pages/nodes:
list with remark · address:port · inbound · security · tags · enable Switch ·
per-inbound move up/down (reorder) · bulk enable/disable/delete; form grouped
into Basic / Advanced / Clash / Subscription-scope sections.
- Route '/hosts' + sidebar item (Global icon); menu.hosts + pages.hosts.* added to
the en-US bundle (other locales fall back to English until translated).
TDD: HostFormSchema (10 cases) and hostToExternalProxyEntry (6 cases) written
failing-first, then implemented. UI verified by lint/typecheck/test/build.
Deferred (documented enhancement): the live in-form share-link preview (needs
inbound+client context) and a per-host host/path override in JSON/Clash output
(raw already overrides; JSON/Clash inherit the inbound's host/path).
Gate: cd frontend && npm run lint && npm run typecheck && npm run test (557) &&
npm run build all green; go build ./... + go test ./... still green.
* refactor(ui): remove the External Proxy form from the inbound stream settings
Hosts supersede the legacy externalProxy: the subscription renders from hosts
(hosts win when both exist) and the migration converts existing externalProxy
entries to hosts. externalProxy's only real consumers were the subscription
(now covered) and this form's preview — the backend per-client copy-link never
used it — so removing the editor has no functional regression.
- Drop ExternalProxyForm + toggleExternalProxy from InboundFormModal and delete
the orphaned form component + its export; remove its block test + snapshot.
- KEEP the externalProxy schema field and backend parsing/link-generation: an
existing inbound's externalProxy still round-trips through the form (not
silently destroyed on edit) and still renders if a host was removed.
Gate: cd frontend && npm run typecheck + lint + test (556) + build green.
* fix(ui): use Alert `title` instead of deprecated `message` (antd 6)
Ant Design 6 deprecated <Alert message=> in favor of <Alert title=>; the panel
was mid-migration (21 Alerts already on title). Renamed the 7 remaining stragglers
across 5 files (SubLinksModal, InboundFormModal, sockopt, EmailTab, TelegramTab),
silencing the runtime deprecation warning. description= is unchanged.
Pre-existing warning, surfaced while testing Hosts — not introduced by it.
Gate: npm run typecheck + lint + test (556) + build green.
* style(ui): align Hosts page with Clients/Inbounds cards + reorder columns
- page-shell.css never listed .hosts-page, so the Hosts page got no content
padding / transparent-layout / summary-card spacing. Add a .hosts-page shell
block (background, dark/ultra vars, content-area + summary-card padding). This
is the actual "card spacing" bug.
- HostList: match the Clients/Inbounds list card — hoverable + the toolbar moved
into the card title as a .card-toolbar (Add when nothing selected; selected
count + bulk enable/disable/delete on selection). Re-declare .card-toolbar in
HostList.css since the shared rule lives in a lazily-loaded page stylesheet.
- Reorder table columns as requested: Actions, Enable, then Remark, Endpoint,
Inbound, Security, Tags. Added scroll x for narrow screens.
- HostsPage: add a summary card (Total / Enabled / Disabled) like the other
pages. New i18n keys: pages.hosts.selectedCount + pages.hosts.summary.*.
Gate: npm run typecheck + lint + test (556) + build green.
* style(ui): use Tabs instead of Collapse in the Add/Edit Host form
The Basic / Advanced / Clash / Subscription-scope sections are now tabs. Each
pane sets forceRender so all fields stay mounted — required because the form
uses preserve=false, so an unmounted tab's values would otherwise be dropped on
submit (and a required field on a hidden tab still blocks submit).
Gate: npm run typecheck + lint + test (556) + build green.
* style(ui): split Host form into Security + Advanced tabs; drop unused JSON fields
- Remove the Mux/Sockopt/XHTTP raw-JSON fields from the Host form: they were not
wired into link generation and the inbound's structured editors are inbound-
specific (not reusable). The DB columns + read schema + generated type stay, so
they can get proper editors later. (HostFormSchema drops them; HostRecordSchema
keeps them.)
- Reorganize tabs to Basic / Security / Advanced / Clash / Subscription scope:
Security holds the TLS/cert fields (security, sni, sni-overrides, alpn,
fingerprint, pins, verify-by-name, ech); Advanced now holds the transport
overrides (host header, path).
- i18n: add pages.hosts.sections.security; drop the 3 unused field labels.
Gate: npm run typecheck + lint + test (556) + build green.
* style(ui): restore Mux/Sockopt/XHTTP fields in the Host Advanced tab
Put the three free-JSON override fields back, in the Advanced tab next to host
header / path (as JSON inputs — the inbound's structured editors aren't reusable
here). Re-added to HostFormSchema + defaults + the i18n labels.
Gate: npm run typecheck + lint + test (556) + build green.
* feat(hosts): add allowInsecure (rendered) + serverDescription/mihomoX25519/vlessRouteId fields
Closes most of the Remnawave-host gap analysis.
- model.Host: + allowInsecure, serverDescription (≤64), vlessRouteId (0-65535),
mihomoX25519. Auto-migrated (SQLite + Postgres verified); openapi regenerated.
- allowInsecure is fully RENDERED into subscription output (TDD):
- raw link: allowInsecure=1 (TLS/Reality, skipped for none) via the endpoint
builder;
- JSON/Clash: applyExternalProxyTLSToStream writes tlsSettings.settings.
allowInsecure, and clash applySecurity now emits skip-cert-verify for the tls
case (it previously only did so for Hysteria — a pre-existing gap, so inbound
allowInsecure now renders for vless/trojan/ss clash too).
- Frontend: the four fields added to the Host form (allowInsecure → Security,
serverDescription → Basic, vlessRouteId → Advanced, mihomoX25519 → Clash);
serverDescription shown under the remark in the list. Schema + i18n updated.
serverDescription / vlessRouteId / mihomoX25519 are stored + editable; their
deeper rendering (and per-host mux/sockopt/xhttp into JSON/Clash, plus a per-host
xray JSON template) are tracked as follow-ups.
Gate: go test ./... green (SQLite + Postgres for the host schema/migration);
go build linux+windows; go vet + gofmt clean; npm run gen + typecheck + lint +
test (556) + build green; generated files in sync.
* feat(sub): render host sockopt + xhttp-extra params into JSON/Clash output (TDD)
A host's sockoptParams and xhttpExtraParams (free-JSON) now take effect:
applyHostStreamOverrides injects sockopt into the per-host stream (re-added since
the base stream strips it) and merges xhttpExtraParams into xhttpSettings, called
in both getConfig (JSON) and getProxies (Clash) right after the per-host TLS
apply. No-op for legacy externalProxy entries (keys absent) — characterization
snapshots unchanged.
mux rendering is outbound-level (overrides outbound.Mux) and needs a genVless/
genVnext/genServer signature change — deferred, along with the per-host xray
JSON template.
Gate: go test ./internal/sub/... + go test ./... green (snapshots unchanged);
go build + vet + gofmt clean.
* feat(sub): render host muxParams as a per-host JSON outbound mux override (TDD)
genVnext/genVless/genServer take a muxOverride: a host's muxParams (when valid
JSON) overrides the global mux on its JSON outbound; empty falls back to the
panel mux (behavior unchanged for non-host configs). Completes the host
mux/sockopt/xhttp trio. Test call sites updated for the new signature.
Gate: go test ./internal/sub/... + go test ./... green (snapshots unchanged);
go build + gofmt clean.
* style(ui): show Host security fields conditionally per security (like externalProxy)
* feat(sub): apply host SNI + fingerprint override for reality (TDD)
A reality host now overrides SNI and fingerprint while inheriting publicKey/
shortId from the inbound (reality keys can't be host-supplied). Previously the
reality link kept the inbound's serverName because the TLS appliers are gated to
security=="tls".
- raw: applyEndpointRealityParams sets sni/fp on the params for reality;
- JSON/Clash: applyHostStreamOverrides sets realitySettings.serverName +
serverNames from the host SNI.
Gated to host endpoints via an isHost marker on the synthesized ep, so the legacy
externalProxy path stays byte-identical (characterization snapshots unchanged).
The marker is internal and never emitted.
Gate: go test ./internal/sub/... + go test ./... green; go build + vet + gofmt clean.
* fix(ui): start the Host inbound select unselected instead of showing 0
A new host left inboundId defaulting to 0, so the Select rendered "0". inboundId
is now optional in the form (undefined until chosen), so it shows its
placeholder ("Select an inbound"); the required rule still enforces a choice on
save. Port keeps 0 (means "inherit the inbound's port").
Gate: npm run typecheck + lint + build green.
* fix(ui): drop redundant :port suffix from the Host inbound select label
The inbound tag (e.g. in-59303-tcp) already carries the port, so the appended
":59303" was duplicated. Show just the remark/tag.
Gate: npm run typecheck + lint + build green.
* style(ui): apply the shared card hover shadows to the Hosts page
page-cards.css scoped its card styling + hover shadows to each page class but
not .hosts-page, so Hosts fell back to antd's default hoverable (a larger/blurry
shadow + pointer cursor). Add a .hosts-page block matching the other pages.
Gate: npm run build green.
* feat(hosts): move Tags to Basic tab, add Nodes field, accept VLESS route ranges
- Move the Tags field into the Host form's Basic tab and add a Nodes
multi-select (visual-only assignment, backed by the existing node_guids
column) so the Basic tab matches the reference layout.
- Replace the single-port vlessRouteId integer with a free-form vlessRoute
string that accepts comma-separated ports/ranges (e.g. 53,443,1000-2000);
format-validated on the frontend, stored verbatim on the backend.
- Regenerated frontend types/openapi from the changed model.
* feat(hosts): structured editors for Mux/Sockopt/XHTTP + new Final Mask
Replace the raw JSON textareas in the Host form's Advanced tab with the same
structured editors used elsewhere, under a nested tabbed layout (General / Mux /
Sockopt / XHTTP / Final Mask), mirroring the Sub-JSON settings tab:
- Mux: the Sub-JSON mux editor (enable + concurrency/xudpConcurrency/xudp443).
- Sockopt + XHTTP: reuse the outbound SockoptForm / XhttpForm, wrapped in an
isolated form that serializes the edited subtree back to the host's JSON
string (pruned so the override stays sparse).
- Final Mask: new host field (model + column + JSON-render wiring that merges
the masks into the host's JSON-subscription stream), edited via the shared
FinalMaskForm like the Sub-JSON Final Mask editor.
Each editor stays a controlled value/onChange component bound to its existing
host JSON string field; backend rendering of mux/sockopt/xhttp is unchanged.
* feat(hosts): drop XHTTP + Xray-JSON-template overrides; fix mobile form layout
Remove the host's XHTTP extra-params and Xray-JSON-template overrides entirely
(model fields + columns, JSON-subscription render paths incl. hostTemplateOutbound,
schema, form tab/field, i18n, openapi codegen, and their tests) — they did not
fit the host model. Mux, Sockopt and Final Mask stay as structured editors.
Mobile fixes for the Edit Host modal:
- responsive width (95vw on mobile, was a fixed 760px that overflowed the
viewport and clipped the tabs/labels) + a scrollable body so the footer stays
on screen;
- Mux fields use responsive Row/Col (stack on mobile) instead of a fixed-width
label grid.
* fix(hosts): hide the spurious horizontal scrollbar in the Edit Host modal
Setting overflowY:auto on the modal body forced overflow-x to auto too (CSS
rule), so antd Row's negative gutter margins triggered a horizontal scrollbar.
Pin overflowX:hidden.
* feat(hosts): inbound-style responsive field layout + icon empty state
- Host form (main form + Mux/Sockopt/Final Mask editors) now use the inbound
form's label layout: label beside the input on desktop (labelCol sm span 8 /
wrapperCol sm span 14, right-aligned), stacked label-above-input on mobile.
Rewrote HostMuxForm onto an internal antd Form so it follows the same layout
instead of a manual grid.
- Empty hosts table now shows the host icon + the shared 'Nothing here yet'
(noData) text, matching Nodes/Inbounds/Clients, replacing the bespoke
'No hosts yet…' string.
* fix(hosts): avoid nested <form> in the Edit Host modal
The Mux/Sockopt/Final Mask editors each render their own antd Form inside the
host's main Form, producing an invalid nested <form> DOM node (hydration
warning). Render those inner forms with component={false} so they keep the form
instance/context but emit no <form> element.
* fix(hosts): make the Mux enable toggle work
The Switch's checked state came from Form.useWatch('mux'), but the mux object
field had no registered Form.Item while disabled, so setFieldValue never
notified the watcher and the toggle stayed off. Bind the Switch to a real
name='enabled' field (antd drives its checked state directly) and keep the
sub-fields registered via hidden={!enabled}, serialized to the flat mux JSON.
* refactor(hosts): reuse the outbound MuxForm instead of a bespoke Mux editor
The Mux fields duplicated the outbound MuxForm. Reuse it through the same
wrapper as Sockopt: generalize OutboundSubtreeJsonForm with defaultSubtree
(pre-fill on enable) and a serialize hook, and have HostMuxForm render MuxForm
at the ['mux'] path. The host keeps its inherit-when-off semantics by storing ''
unless mux.enabled. Also drops the now-unused enableSwitch path from the
wrapper (only the removed XHTTP editor used it).
* style(hosts): use default-width Port input like the inbound form
The host Port used width:100% (full width); the inbound's numeric inputs use
antd's default width. Drop the override so Port matches. The Mux number inputs
already use the default width via the reused MuxForm.
* refactor(sockopt): readable customSockopt editor as a shared component
The customSockopt rows were a single cramped Space.Compact line and duplicated
verbatim in the inbound and outbound sockopt forms. Extract a shared
CustomSockoptList that renders each entry as a titled group of labeled fields
(System / Level / Opt / Type / Value), matching the rest of the form, and use it
in both (and thus the host Sockopt editor).
* fix(finalmask): drop the empty Custom Tables tag on a new sudoku mask
The sudoku TCP-mask default seeded customTables: [''] (one empty string), which
rendered as a blank removable tag. Seed [] instead.
* fix(sockopt): make the outbound (and host) Sockopt client-only
Per the XTLS sockopt docs, tproxy / acceptProxyProtocol / V6Only /
trustedXForwardedFor only apply to an inbound (listening socket); they are
meaningless on an outbound/dialer. Drop them from the outbound SockoptForm
(which the host reuses). The Sockopt default object still seeds those keys, so
the host also strips them on serialize, keeping its override honest to the
server/client split. The inbound SockoptForm is left unchanged.
* fix(sockopt): make the inbound Sockopt server-only
Complete the server/client split: drop the outbound/dialer-only fields from the
inbound SockoptForm — dialerProxy, domainStrategy, interface, addressPortStrategy,
happyEyeballs, tcpMptcp (client-only since Go 1.24 auto-enables MPTCP on listen).
mark stays (xray applies SO_MARK on inbound sockets too). Update the form-blocks
snapshot to the server-side field set (intentional spec change).
* feat(hosts): populate Sockopt dialerProxy with the panel's outbound tags
The host Sockopt editor reused the outbound SockoptForm with outboundTags=[],
so the dialerProxy dropdown was empty. Feed it the panel's outbound tags via
the existing useOutboundTags hook (shares the cached xray-config query;
blackhole excluded), so a host can chain through a subscription outbound by tag.
* fix(hosts): empty-state styling on direct load + exclude balancers from dialerProxy
- .card-empty was only defined in lazily-loaded Clients/Inbounds/Nodes
stylesheets, so a direct /hosts refresh rendered the empty table state
unstyled (faint + uncentered) until another page was visited. Re-declare it
in HostList.css so it's correct on first load.
- The Sockopt dialerProxy dropdown listed balancer tags (useOutboundTags merges
them in for mtproto egress). dialerProxy chains a single outbound, so balancers
aren't valid — switch to useOutboundTagGroups and use only the outbound group.
* fix(outbounds): icon + 'Nothing here yet' empty state; stop fading other pages
The Outbounds empty state was a faint '—', and OutboundsTab.css set the global
.card-empty to opacity:0.4 — which leaked onto whichever page's empty state was
shown after the Outbounds CSS had loaded (e.g. Hosts went faint after visiting
Outbounds). Render the icon + noData ('Nothing here yet') like the other lists,
and align .card-empty to the shared centered/secondary style (no opacity).
* fix(outbounds): custom empty state on the desktop table too
The desktop Outbounds Table had no locale.emptyText, so it showed antd's
default 'No data' box. Add the same ExportOutlined + noData empty state as the
card (mobile) view.
* style(sidebar): use ExportOutlined for the Outbounds nav item
The Outbounds sidebar item used UploadOutlined (an upload tray). Switch to
ExportOutlined, matching the outbound icon now used in the routing target and
the outbounds empty states.
* feat(hosts): icons on the form tabs (icon-only on mobile)
Wrap every Host form tab label (Basic/Security/Advanced/Clash/Subscription
scope and the nested General/Mux/Sockopt/Final Mask) with catTabLabel, so the
tabs show icon + text on desktop and just the icon (with a tooltip) on mobile,
matching the Settings/Xray tab bars.
* refactor(hosts): fold Exclude-from-formats into Advanced, drop the one-field tab
The Subscription scope tab held only excludeFromSubTypes after Tags moved to
Basic — a niche per-format scoping knob. Move it into the Advanced > General
sub-tab and remove the standalone tab (and its now-unused subScope label/icon).
* feat(sub): per-client remark template variables; drop the remark model & Show Usage Info
* fix(migration): cap seeded host remark at the model's 256-char limit, not 40
|
||
|
|
9385b6c609 |
feat(nodes): per-node client IP attribution for IP-limit
Record each panel's own Xray IP observations under its panelGuid and merge each node's guid-keyed report on the master, so the panel can tell which node a client IP is connecting through (the flat inbound_client_ips union is pushed back to every node and cannot attribute). Adds the NodeClientIp model + migration, the clientIpsByGuid endpoint and node-sync merge, node-name labels in the client IP log, and cleanup on node deletion. |
||
|
|
bbab83db17 |
refactor(frontend): stack client credential fields and use label hints on inbound form
Stack UUID/password/subId/auth/flow/security fields vertically in the client modal instead of two-column rows, and replace the inbound form's 'extra' help lines with hover tooltip hints on field labels. |
||
|
|
b5872af279 |
Frontend operation button size optimization (#5343)
* Update ClientsPage.tsx * Update GroupsPage.tsx * Update RowActions.tsx * Update NodeList.tsx * Update BalancersTab.tsx |
||
|
|
dcb923b4a1 |
feat(sub): per-client external links and remote subscriptions
Add a Links tab to the client form for attaching third-party share links and remote subscription URLs per client. They are merged into the client's raw/JSON/Clash subscription output: links are emitted verbatim and parsed for JSON/Clash; subscription URLs are fetched (cached, with a short timeout) and their configs merged in. i18n keys added across all 13 locales. |
||
|
|
355262e632 |
fix(clients): keep the client list live with a background poll (#5262)
The paged client list is sorted/paginated server-side but fetched with staleTime: Infinity, so the WS client_stats patch only refreshed traffic on already-visible rows — newly connected clients never appeared and the sort order went stale until a manual refresh. Add a 5s refetchInterval so the current page tracks reality, and drive the table overlay off isPlaceholderData so the background poll does not flash it. |
||
|
|
8f556fe2db |
fix(clients): centre the online dot inside the Online tag (#5238)
The .online-dot uses vertical-align: middle, which in inline layout aligns to baseline + half x-height — visibly off-centre inside the Ant Tag's line box. Add a .dot-tag utility (inline-flex, align-items: center) and apply it to the Online tag so the dot and label share one centred axis. Other dot usages (Nodes page Space, card heads, stat rows) already sit in flex containers and are unaffected. |
||
|
|
4eab37b66c | feat(clients): restore reset traffic button in edit client form | ||
|
|
6e20588236 |
style(ui): enlarge row action icons and rebalance clients table widths
Bump row action icons to 18px across the clients, inbounds, groups and nodes tables for better visibility. In the clients table, cap the Client column at 220px and give Duration a fixed width so the Traffic column becomes the flexible one that absorbs the remaining horizontal space instead of Client growing oversized. |
||
|
|
7ae3ea66d1 |
feat(ui): improve client form modal UX
- Rename tabs: "Basic" → "Basics", "Config" → "Credentials" - Move reverseTag field from Credentials tab to Basics tab - Move IP log button inline with limitIp field (tooltip button, edit mode only) - Hide random email button when editing an existing client - Add tooltips to totalGB and limitIp fields with descriptive hints - Rename labels: "Total Sent/Received (GB)" → "Traffic Limit (GB)", "Duration" → "Duration (days)" - Add renewDays translation key for auto-renew label with unit hint - Remove redundant filterOption and width style from AutoComplete group selectors - Update all 15 locale files with new and renamed translation keys |
||
|
|
253063b785 |
feat: filter inbounds and clients by node (#4997)
Multi-node panels had no way to narrow the inbounds or clients lists to a single node. Add a node filter to both pages: - Inbounds: a toolbar select (All / Local / each node) that filters the list client-side; shown only when the panel has nodes or node-attached inbounds. - Clients: a Nodes multi-select in the filter drawer. Node selections are mapped onto inbound IDs client-side and fed through the existing inbound CSV paging parameter, so the paging backend is untouched; an impossible id (-1) is sent when no inbound matches so the filter yields an honest empty result. InboundOption now carries nodeId to make the mapping possible. The local panel is selectable via a 0 sentinel (inbounds without a nodeId). New i18n keys in all 13 locales. |
||
|
|
7bcc5830c6 |
feat(online): use xray online-stats API for onlines and access-log-free IP limit
Adopt xray-core's statsUserOnline policy and GetUsersStats RPC so online detection is connection-based and IP limiting no longer requires an access log. Falls back to the legacy traffic-delta onlines and access-log parsing when the running core lacks the RPCs (Unimplemented), probed lazily per process so a panel-driven version switch re-evaluates automatically. Backend: - xray/api.go: GetOnlineUsers (one GetUsersStats call returns all online users and their source IPs) and IsUnimplementedErr. - xray/process.go: per-process OnlineAPISupport tri-state capability cache. - service/xray.go: ensureStatsPolicy injects statsUserOnline into every policy level of the generated config; XrayService.GetOnlineUsers probes and falls back. - job/xray_traffic_job.go: union API onlines into the delta-derived active set; bump last_online for idle-but-connected clients. - job/check_client_ip_job.go: API-first IP source with shared enforcement; live observations bypass the 30-min stale cutoff; access-log path unchanged for older cores. - service/setting.go: GetIpLimitEnable always true; new accessLogEnable default for features that genuinely read the access log. Frontend: - Client form split into Basic and Config tabs; IP Limit and IP Log no longer gated on access log; compact Auto Renew next to Start After First Use; tabBasic/tabConfig added to all 13 locales. - Xray logs button on the dashboard now gated on accessLogEnable. |
||
|
|
dc52e725b6 |
fix(ui): blink the online dot in mobile client cards like desktop
The mobile card rendered a static antd Badge for every bucket. When the client is enabled, online, and not depleted, render the same animated online-dot span the desktop Online column and the nodes list use. |
||
|
|
07e5e8498e |
feat(ui): add select all / clear all shortcuts for inbound multi-select (#5175)
* feat(ui): add select all / clear all shortcuts for inbound multi-select Adds 'Select all' and 'Clear all' buttons above the inbound multi-select in: - ClientFormModal (add/edit client) - BulkAttachInboundsModal (bulk attach clients to inbounds) - BulkDetachInboundsModal (bulk detach clients from inbounds) - ClientBulkAddModal (add bulk clients) Extracts the repeated button logic into a reusable SelectAllClearButtons component. Includes i18n keys for all 13 supported languages with proper translations. Closes #5144 * refactor(form): decouple SelectAllClearButtons labels and harden select-all Accept optional selectAllLabel/clearLabel props so the generic form component is not tied to the client-inbound i18n keys (defaults unchanged). Compute the all-selected state by checking every option is present and union the current value on select-all, so it stays correct if value holds ids outside options. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
8f408d2d6a |
feat(routing): show tag (remark) in routing rules list (#5151)
* feat(routing): show tag (remark) in routing rules list Rules table and mobile cards showed raw inboundTag while the form already used remarks. Display "tag (remark)" when a remark exists; saved rules still store tags only. Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com> * feat(inbounds): show "tag (remark)" consistently wherever an inbound is listed Add a shared formatInboundLabel/formatInboundTag helper and apply the "tag (remark)" format across the routing rules table, mobile cards, the rule form and route tester, plus the client attach/detach/filter modals and the attached-inbounds column. Falls back to the bare tag when no distinct remark exists. Also fix the routing rules list mis-rendering inbounds whose remark contains a comma: formatted entries are now carried as an array end to end instead of being joined and re-split on commas. --------- Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
941eba546d |
feat(clients): restore traffic usage progress bars on Clients page (#5150)
Bring back the v2.9.x traffic column UX: used amount, color-coded progress bar, limit/infinity label, and hover popover with upload/download/remaining breakdown. Adds a shared ClientTrafficCell component, traffic display helpers, and unit tests. |
||
|
|
3092326d9e |
refactor: replace custom geo manager with Xray-core native geodata auto-update
Remove the panel-side custom geo download feature (service, controller, /panel/api/custom-geo/* endpoints, CustomGeoResource model, UI tab) in favor of Xray-core's native geodata section (https://xtls.github.io/config/geodata.html). - pass the top-level "geodata" key through xray.Config so it survives the template round-trip into the generated config - add a Geodata Auto-Update section to the Xray Updates modal that edits geodata (cron schedule, download outbound, asset list) in the config template and restarts Xray on save - previously downloaded geo files in the bin folder keep working in ext: routing rules; the orphaned custom_geo_resources table is left in place so existing source URLs stay recoverable |
||
|
|
f9b275dd23 |
fix(ui): keep client IP log modal above edit modal (#5137)
* fix: keep client IP log modal above edit modal * refactor: name client modal z-index values |
||
|
|
83799d71b0 |
feat(api-docs): generate response examples from Go structs; fix SS2022 PSK regen (#4996)
Stop hand-writing OpenAPI response examples, which kept drifting from the real payloads (clients/traffic missing fields, inbounds/list exposing userId which is json:"-", the fictional inbound-443 tag instead of the real in-<port>-<transport> form).
tools/openapigen now emits frontend/src/generated/examples.ts: a per-struct example instance built from type defaults, validate oneof/min bounds, and example: struct tags, with nested-ref expansion and a cycle guard. build-openapi.mjs composes the {success,obj} envelope from it for any endpoint annotated with responseSchema (+ responseSchemaArray for lists); the hand-written response is dropped for those. Service DTOs InboundOption/ApiTokenView/ProbeResultUI are added to the walker.
#4996: client password regeneration now produces a valid Shadowsocks 2022 PSK (correct base64 length per cipher) when an SS2022 inbound is attached, in both the single and bulk client forms; backend surfaces ssMethod on /inbounds/options so the UI can pick the right length.
Also: Swagger UI persists the Authorization token across reloads (persistAuthorization).
|
||
|
|
f185d3315c |
perf(clients): scale add/delete and bulk client operations
Follow-up to the SyncInbound bulk rewrite, fixing the remaining O(M*N) and O(M)-round-trip behaviour in the add/delete and bulk paths that made them time out on large inbounds (worst case minutes), especially on PostgreSQL. - compactOrphans: chunk the "email IN (...)" lookup (400/batch) instead of binding every email at once. A single huge IN exceeded PostgreSQL's 65535-parameter limit (and SQLite's) and made the planner pathological, so add/delete failed outright past ~100k clients. - emailsUsedByOtherInbounds: new batched form used by delInboundClients (BulkDetach) and bulkDelInboundClients (BulkDelete), replacing a per-email global JSON scan (O(M*N)) with one scan, and skipped entirely when keepTraffic is set. - BulkCreate: rewritten to validate/dedup in one pass, then group clients by inbound and add them in a single addInboundClient call per inbound (one getAllEmailSubIDs, one settings rewrite, one SyncInbound) instead of running the full single-create pipeline per client. - Bulk delete/adjust: batch DelClientStat/DelClientIPs with IN deletes and wrap the settings Save + SyncInbound in one transaction, so the per-row writes share a single fsync instead of one per row. Measured on PostgreSQL 16 (one inbound, M=2000 affected clients): - create: 8m35s (M=500) -> ~1-5s - detach: 52s -> ~4s (flat in N) - delete: ~16s -> ~1-4s - adjust: ~20s -> ~7-10s add/delete of a single client on a 200k-client inbound stays in seconds. sync_scale_postgres_test.go adds skip-gated benchmarks (XUI_DB_TYPE= postgres) for the single add/delete and the five bulk operations. |
||
|
|
df7ccd3a64 |
fix(clients): use client_inbounds link to resolve inbound, not stale id
client_traffics.inbound_id is a legacy single-inbound pointer that goes stale when an inbound is deleted and recreated: the email-keyed traffic row survives but references a missing inbound. Code that resolved the owning inbound from it broke several client operations.
- adjustTraffics: 'Start After First Use' (negative expiry) never converted to an absolute deadline on first traffic, so the countdown never started. Now resolves inbounds via the client_inbounds link and computes the new expiry once per email so multi-inbound clients stay consistent.
- GetClientInboundByEmail / GetClientInboundByTrafficID: fall back to client_inbounds when the pointer is dead, fixing reset traffic ('record not found'), client info, and Telegram set-tgId.
- autoRenewClients: resolve renew targets via client_inbounds so scheduled renews are not silently skipped.
- clients page: allow resetting a client with no inbound attachment (the backend already zeroes counters by email).
Add regression test for the delayed-start conversion under a stale inbound_id.
|
||
|
|
d0998c1d6d |
feat(links): richer share-link labels across QR, client info and sub views
Show colored protocol/transport/security tags followed by the inbound remark and port for each share link in the client QR modal, client info modal and subscription page. The client email and the traffic/expiry decorations are stripped from the remark so only the inbound remark and port remain. Consolidate the duplicated per-page parseLinkMeta/trimEmail/PROTOCOL_COLORS into a shared lib/xray/link-label.tsx (parseLinkParts, LinkTags, linkMetaText) so the colours and the email/stats stripping stay identical across all three surfaces. |
||
|
|
91f325eca6 |
feat(clients): show filtered count in clients list
Surface a "Showing X of Y" counter in the clients filter bar that appears whenever a search term or any filter is active, using the server-provided filtered and total counts. Added the showingCount string across all 13 locales. Closes #4808 |
||
|
|
61105c2b1a |
feat(clients,routing): label inbounds by remark with tag fallback
Inbound pickers and chips across the Users area, the inbounds attach-clients modals, and the routing rule inbound-tags selector showed the auto-generated tag (in-443-tcp). Show the inbound remark when set, falling back to the tag. Only display labels change; option values keep using the inbound id (or tag for routing rules, which match inbounds by tag), so filtering, attaching, and saved rules are unaffected. Routing reads remarks via a shared useInboundOptions hook that reuses the existing options query cache. |
||
|
|
49ef1449f1 |
fix(clients): keep Add Client modal in viewport with internal scroll
Open the modal near the top (top: 20) and let the body scroll internally (maxHeight + overflowY auto, overflowX hidden) so the tall vertical-layout form no longer leaves a large gap above and runs off the bottom. |
||
|
|
cfd3b34362 |
feat(clients): show last-online tooltip on the depleted tag too
The Online column already surfaced last-online on the offline tag; extend the same tooltip to the depleted (ended) tag so a depleted client's last activity is visible without enabling it. |
||
|
|
b9cbc0c1e8 |
fix(ui): exit infinite spinner with a retry card on failed initial load
List pages wrapped content in <Spin spinning={!fetched}> where 'fetched' only flipped true once data arrived. With staleTime: Infinity + retry: 1, a transient network error on first load left the query in a permanent error state and the spinner stuck forever.
Now 'fetched' also settles on query.isError, and a failed load shows a Result error card with a Refresh button that self-heals when the backend returns, mirroring the existing XrayPage pattern. Applied to clients, inbounds, groups, nodes, and the dashboard.
Fixes #4723
|
||
|
|
971843f669 |
feat(nodes): bulk panel self-update with live online indicator
Adds the ability to update node panels to the latest release from the Nodes page: select online, enabled nodes (checkboxes) and trigger their official self-updater, or use the per-row Update action. A node whose reported panel version trails the latest GitHub release is flagged with an 'update available' tag (compared via lib/panel-version, mirroring the Go isNewerVersion). Backend: Remote.UpdatePanel calls the node's existing POST /panel/api/server/updatePanel; NodeService.UpdatePanels fans out over the selected ids, skipping disabled/offline nodes with a per-node reason; exposed as POST /panel/api/nodes/updatePanel (documented in endpoints.ts + openapi.json). The bulk request sends a JSON body, so it sets Content-Type: application/json explicitly — axios defaults POST to form-urlencoded, which made ShouldBindJSON fail with 'invalid character i'. Also reuses the clients-page online cue on the Nodes page: a pulsing green dot plus green label for an online node. The .online-dot style moved to the shared styles/utils.css so both pages load it. Translations for all new node keys added across every language file. |