Commit Graph

28 Commits

Author SHA1 Message Date
Sanaei 37c5e0bfd2 feat(node): node hardening — mTLS, hashed+zstd reconcile transport, per-node net metrics (#5382)
* fix(api-docs): document clientIpsByGuid route

Restores a green `go test ./...` baseline: TestAPIRoutesDocumented
flagged POST /panel/api/clients/clientIpsByGuid (added in 9385b6c6)
as undocumented in endpoints.ts.

* test(node): characterize current node TLS + API auth behavior

Phase 0 regression net for the mTLS work. These pass on unchanged
production code and lock the pre-mTLS contracts so later phases can be
proven additive:

- tlsConfigForNode: skip -> InsecureSkipVerify (no VerifyConnection);
  pin -> VerifyConnection installed.
- checkAPIAuth: bearer match -> Next + api_authed; unauthenticated ->
  401 (XHR) / 404; valid session -> Next.
- panel HTTPS listener with no ClientAuth accepts a client that presents
  no client certificate (the browsers-keep-working invariant).

* feat(crypto): node-auth CA + client-cert minting (TDD)

Stdlib-only ECDSA P-256 helpers for the node mTLS work:
- GenerateNodeCA: self-signed CA (IsCA, CertSign, path len 0)
- IssueClientCert: client-auth leaf (ExtKeyUsageClientAuth) signed by CA
- LoadCAFromPEM: parse a CA cert+key for issuing / trust-pool building

Tests assert the contract (leaf verifies against the issuing CA with
ExtKeyUsageClientAuth), seen failing on the assertion before impl.

* feat(node): lazy node mTLS CA + client cert in settings (TDD)

SettingService gains opt-in mTLS material, all stored as Setting rows
with empty defaults and kept out of entity.AllSetting (so private keys
never reach the settings UI/export):
- EnsureNodeMtlsCA: mint+persist the node-auth CA once, reuse thereafter
- EnsureMasterClientCert: issue the master client cert from the CA, idempotent
- NodeMtlsClientCAPool: ClientCAs trust pool for the listener; nil when
  unconfigured so the no-mTLS path is unchanged

Tests assert idempotency and that the client cert verifies against the CA
for client auth; seen failing on the assertion before impl.

* feat(node): mtls client TLS config + master-cert provider (TDD)

tlsConfigForNode gains an 'mtls' branch that presents the master client
certificate and verifies the node server against system roots (no
InsecureSkipVerify, no custom RootCAs). The cert is supplied via an
injected MasterClientCertProvider so runtime need not import service;
it fails closed when unconfigured. skip/pin contracts unchanged.

* feat(node): allow tokenless mtls nodes in remote do() (TDD)

mtls nodes authenticate with a client certificate, so the bearer token
becomes optional for them: do() no longer rejects an empty ApiToken when
TlsVerifyMode is mtls, and the Authorization header is omitted when no
token is set. Every other mode still requires a token (regression kept).

* feat(node): authenticate verified client certs in checkAPIAuth (TDD)

A completed mTLS handshake (non-empty r.TLS.VerifiedChains) now
authenticates an API request, equivalent to a valid bearer token, and
sets api_authed so the CSRF middleware lets cert-authed mutations
through. Bearer/session/reject paths unchanged. The accept-path assert
was mutation-checked (guard flipped -> test red -> reverted).

* feat(node): opt-in mTLS on the panel listener (TDD; mutation-checked)

web.go now applies VerifyClientCertIfGiven + ClientCAs to the HTTPS
listener when a node trust CA is configured, and wires the master client
cert provider for outbound mtls calls. With no CA the listener is
byte-identical to before (browsers unaffected).

applyNodeMtls is covered end-to-end: no-cert client handshakes (browsers
keep working), a CA-signed client cert verifies, a foreign-CA cert is
rejected at the handshake. Mutation-checked:
- RequireAndVerifyClientCert -> no-cert client rejected (red) -> reverted
- drop ClientCAs -> master cert no longer trusted (red) -> reverted

* feat(node): accept mtls verify-mode + CA reveal endpoint (TDD)

- model.Node.TlsVerifyMode validator now accepts 'mtls'
- normalize() preserves mtls and requires the node scheme to be https
  (fail closed), instead of clamping mtls back to verify
- NodeService.NodeMtlsCaCert + POST /panel/api/nodes/mtls/ca return this
  panel's node-auth CA cert (public) to paste into a node, minting the CA
  + master client cert on first call
- endpoints.ts documents the new route (doc-sync test)

No model column added (enum is a string), so no migration/codegen.

* feat(node): node mTLS UI + trust-CA setter (TDD)

Backend:
- NodeService.SetNodeMtlsTrustCA + POST /panel/api/nodes/mtls/trustCA
  store the CA this panel trusts for incoming node-API client certs
  (validates PEM, empty clears); applied on next restart
- endpoints.ts + regenerated openapi.json document both mtls routes

Frontend:
- node form: 'mtls' TLS-verify option + setup hint (zod enum updated)
- Nodes page 'Node mTLS' card: copy this panel's CA, and paste/save the
  trusted parent CA
- en-US i18n keys (other locales fall back to en-US)

Gates green: go build (native+windows), vet, go test ./...; frontend
typecheck, lint, vitest (541).

* style(node): gofmt web_mtls_test doc comment

* feat(node): hashed+zstd reconcile transport (TDD, negotiated, mixed-version safe)

Adds an integrity + compression envelope to node config pushes:
- internal/util/wirecodec: shared zstd codec (bomb-capped decode) +
  SHA-256 hashing + the header/capability constants
- Remote.do(): always attaches X-Config-Sha256 of the uncompressed body;
  zstd-compresses only when the node advertised support (learned from its
  X-3x-Node-Caps response header) and the body is >=1KiB
- ConfigEnvelopeMiddleware on /panel/api: advertises the cap, decompresses
  and verifies the hash (handler not invoked on mismatch) before binding

Mixed-version safe: old nodes never advertise the cap -> plain bodies;
the hash header is verify-if-present so any panel/node mix interoperates
(existing reconcile tests stay green). klauspost/compress promoted to a
direct dep. Hash-mismatch reject was mutation-checked (compare defeated
-> test red -> reverted).

* feat(node): per-node network throughput metrics (TDD)

The node status response already carries gopsutil netIO.up/down (summed
non-virtual interfaces), so no node-side change is needed:
- probe() parses netIO.up/down into HeartbeatPatch.NetUp/NetDown
- Node gains net_up/net_down columns (AutoMigrate); UpdateHeartbeat
  persists them and appends netUp/netDown to the per-node metric history
- NodeMetricKeys whitelists netUp/netDown so the history endpoint serves them
- NodeHistoryPanel renders Net Up/Down sparklines (KB/s, no 0-100 clamp)
- regenerated frontend types + openapi.json for the new Node fields

* feat(node): move node mTLS controls into a toolbar button + modal

The Node mTLS panel was an always-visible card cluttering the nodes
page. Replace it with a 'Node mTLS' button beside 'Add node' that opens
a modal with the same copy-CA + trusted-parent-CA controls; the modal
closes on a successful save. No backend/i18n changes.

* i18n(node): translate mTLS + net-metrics keys for all locales

Adds the node mTLS strings (tlsMtls, mtlsFormHint, mtls.* dialog + the
saveMtls toast) and the netUp/netDown chart labels to all 12 non-English
catalogs (ar, es, fa, id, ja, pt, ru, tr, uk, vi, zh-CN, zh-TW), matching
each catalog's existing terminology. Technical tokens (mTLS/TLS/CA/API/
KB/s) kept verbatim.

* fix(node): address Copilot review on node-hardening PR

- setting_mtls: fail closed on a half-present CA/master-cert pair instead of
  silently regenerating (which would rotate the CA and break fleet trust).
- config_envelope: reject non-zstd Content-Encoding on the envelope path
  rather than hashing/forwarding a still-encoded body to the handler.
- node mTLS: support tokenless mTLS end-to-end — apiToken is now
  required_unless tlsVerifyMode=mtls (model) with matching conditional
  validation in NodeFormSchema, so the runtime allowance is actually reachable.
- NodesPage: add a catch block to onSaveTrustCa so save failures surface.
2026-06-16 12:19:33 +02:00
MHSanaei d882d6aa74 feat(inbounds): add Real client IP presets to capture visitor IP behind CDN/relay
Surface the existing sockopt knobs (acceptProxyProtocol, trustedXForwardedFor) as a guided 'Real client IP' preset selector in the inbound form, so the real visitor IP is recovered behind Cloudflare CDN or an L4 tunnel/relay instead of recording the intermediary address. Presets are mutually exclusive, warn on incompatible transports, and add tooltips, docs, and translations for all locales.
2026-06-15 23:50:04 +02:00
Sentiago eec030f86f feat(notifications): event bus architecture with Telegram and SMTP subscribers (#5326)
* feat(notifications): event bus architecture with Telegram and SMTP subscribers

- Event bus core with buffered channel, fan-out, panic recovery
- Telegram subscriber with HTML formatting and rate limiting
- Email subscriber with SMTP/TLS/STARTTLS support and stage diagnostics
- 5 event types: outbound.down/up, xray.crash, cpu.high, login.attempt
- CPU threshold checks per subscriber (tgCpu for TG, smtpCpu for Email)
- SystemMetricData struct for raw metric values in events
- i18n keys for en-US, ru-RU, and English defaults for other locales

* fix

* fix(notifications): repair crash/CPU alerts, harden secrets, add node alerts

Bug fixes:
- Xray crash notifications were permanently suppressed after the first crash:
  XrayStateTracker latched state="down" with no reset and no recovery event,
  so only the first crash per process lifetime ever notified. Removed the
  tracker; the existing 1/min rate limiter already dedupes crash-loop spam.
- Email CPU alerts could never fire unless Telegram was also enabled, because
  the CPU job was registered only inside the tgbot block. Register it whenever
  either Telegram or SMTP wants cpu.high (new cpuAlarmWanted gate) and relax
  the cadence to @every 1m (cpu.Percent already samples over a full minute).
- SMTP password (and, pre-existing, all other secrets) were shipped to the
  browser in plaintext: GetAllSettingView was dead code and /setting/all
  returned the raw model. Wire getAllSetting -> GetAllSettingView, redact
  smtpPassword with a hasSmtpPassword presence flag, and preserve it on blank
  save. Closes the leak for tgBotToken/ldapPassword/2FA token too.

Polish:
- email Send: use nil SMTP auth when no credentials (Go refuses PlainAuth over
  the unencrypted "none" transport).
- Remove unused EventClientDepleted; fix inaccurate bus.go doc comments; drop
  stale tgBotLoginNotify from the frontend schema; gofmt alignment.

Feature - node online/offline alerts:
- Emit node.down/node.up from the heartbeat job on a real status transition
  (with a startup-spam guard), reusing NodeHealthData. Formatted by both the
  Telegram and email subscribers and selectable in the settings UI.

Regenerated frontend types (hasSmtpPassword). New i18n keys added to en-US;
other locales fall back to English (bundle default) until translated.

* fix(settings): use antd Space orientation instead of deprecated direction

Ant Design 6 deprecated Space's `direction` prop in favor of `orientation`,
which logged a console warning from the Telegram/Email notification tabs. Brings
these two tabs in line with the rest of the codebase, which already uses
`orientation`.

* i18n(notifications): translate the notification feature into all locales

The notifications PR shipped ~99 new strings (SMTP settings, event labels,
Telegram/email message templates) as English placeholders in every non-English
locale. Translate them — plus the node-alert keys added during this review —
into all 12 locales: Arabic, Spanish, Persian, Indonesian, Japanese,
Portuguese-BR, Russian, Turkish, Ukrainian, Vietnamese, and Simplified/
Traditional Chinese.

Go-template placeholders ({{ .Tag }}, {{ .Name }}, etc.) are preserved exactly;
tgbot message values carry no leading status emoji (the bot/email code adds
those, so an emoji in the value would duplicate it); product/protocol names
(SMTP, STARTTLS, TLS, CPU, Xray, Telegram) are kept as-is.

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-15 21:03:41 +02:00
Nikan Zeyaei 05ad7f417c feat(node): per node outbound routing (#5275)
* feat: add per-node outbound routing for panel-to-node connections

* feat(ui): add outbound tag selector to node form with i18n

* fix(xray): avoid potential overflow warning in node egress rule allocation

* chore: run "npm run gen"

* fix

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-14 23:10:52 +02:00
Nikan Zeyaei f4bbaf40f0 feat(ui): show per-inbound live speed (#5261)
* feat(utils): add speedFormat utility and tests

* feat(inbounds): add InboundSpeedEntry type

* feat(inbounds): add speed column to inbound list

* feat(inbounds): show speed in inbound stats modal

* feat(inbounds): compute inbound speed from traffic deltas

* feat(inbounds): wire inbound speed through page

* feat(i18n): add speed translation for all locales

* refactor(inbounds): dedupe live-speed UI and harden formatting

Extract a shared InboundSpeedTag component and isActiveSpeed guard used by the speed column and stats modal, unify InboundSpeedEntry into a single type, and route speedFormat through sizeFormat.

Also guard sizeFormat against non-finite input (no more "NaN PB/s") and clear stale per-inbound speeds when a traffic poll returns no deltas.

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-14 22:39:40 +02:00
MHSanaei 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.
2026-06-14 20:57:14 +02:00
MHSanaei 08bc481ae3 refactor(settings): reorganize subscription settings into clearer tabs
- Split the Happ and Clash/Mihomo routing sections out of Information into
  their own dedicated tabs.
- Extract the profile/branding fields (title, support URL, profile page,
  announcement, theme dir) out of the mislabeled "Subscription Title"
  divider into a new Profile tab.
- Move the Update Interval setting into Information and drop the
  single-field Intervals tab.
- Add the "profile" tab label across all locales.
2026-06-12 19:41:02 +02:00
MHSanaei c7a0188772 feat(settings): schedule picker, toggle placement, sub-theme docs link
- Replace the Telegram "Notification Time" free-text field with a guided
  cron builder: @every + number + unit (s/m/h), the @hourly/@daily/@weekly/
  @monthly macros, and a Custom option that seeds a valid 6-field crontab
  (cron runs with seconds enabled) as an escape hatch.
- Move "Restart Xray After Auto Disable" from the External Traffic tab to
  Panel Settings, where it belongs.
- Add a "Template guide" link to the Sub Theme Directory setting pointing at
  docs/custom-subscription-templates.md.
- Localize all new strings across every locale.
2026-06-12 19:19:31 +02:00
MHSanaei 5eec178483 feat(mtproto): route Telegram egress through Xray routing rules
Add a per-inbound "Route through Xray" toggle (off by default) plus an
optional outbound picker on MTProto inbounds. mtg only supports a SOCKS5
upstream, so when enabled the panel injects a loopback SOCKS bridge into
the generated Xray config — tagged with the inbound's own tag — and mtg
dials Telegram through it via a [network] proxies upstream. The router
then governs Telegram egress: matchable in the Routing tab, or forced to a
chosen outbound/balancer via the picker.

- mtproto: Instance carries RouteThroughXray + XrayRoutePort (in the
  fingerprint); InstanceFromInbound parses them; renderConfig emits the
  socks5 [network] upstream; freeLocalPort exported as FreeLocalPort.
- xray.go: injectMtprotoEgress appends the loopback SOCKS bridge and
  prepends an optional inboundTag->outbound/balancer rule, hot-appliable
  like injectPanelEgress.
- inbound.go: backend-owned egress port persisted in settings, allocated
  once and carried across edits (stored value wins); stripped with the
  inert outboundTag when routing is off; allocation failure fails the save;
  routed add/update/del force a config regen.
- mtproto_job: skip folding mtg metrics for routed inbounds (the bridge,
  carrying the inbound tag, is metered by xray_traffic_job) to avoid
  double-counting.
- frontend: toggle + outbound/balancer Select (useOutboundTags) on the
  MTProto form; i18n keys for all locales.
2026-06-12 17:58:45 +02:00
MHSanaei 5716ae5987 feat(outbound): batched connection tester with direct timed HTTP probes
Replace the per-outbound burstObservatory polling (one temp xray spawn +
up to 15s of /debug/vars polling per outbound, serialised) with one
shared temp xray instance per batch: every tested outbound gets its own
loopback SOCKS inbound plus an inboundTag->outboundTag routing rule, and
the panel times a real HTTP request through each one in parallel. The
probe returns as soon as the response lands and records the HTTP status
plus an httptrace breakdown (proxy connect / TLS via outbound / first
byte) shown in the result popover.

New POST /panel/api/xray/testOutbounds endpoint (array in, results in
input order, max 50); the legacy /testOutbound endpoint now delegates to
the same engine. Test All chunks HTTP probes 16 per request, and a batch
whose shared process never comes up (one structurally-broken outbound
poisons the config) retries each item in an isolated instance so the
broken outbound reports xray's real error while the rest still test.
2026-06-12 16:55:53 +02:00
MHSanaei 85983eec1a refactor(groups): restyle traffic summary into upload/download + usage cards
Split the group traffic summary into two inbound-style cards: a "Total
upload / download" card with up/down arrow icons and a "Total Usage" card
with the pie icon. Add the totalUpDown label across all locales.
2026-06-12 16:22:30 +02:00
MHSanaei 1c5cb84492 feat(groups): show upload/download breakdown in group traffic
Add per-group up/down to GroupSummary (backend + schema), surface them
as Upload/Download columns in the groups table, and fold upload/download
into the Total traffic summary card. Rename the group "Clients in group"
column to just "Clients" across all locales.
2026-06-12 15:30:41 +02:00
MHSanaei 7c698c4bcf feat(inbound): support abstract unix sockets (@ prefix) in Address field
Accept the @-prefixed abstract socket form (e.g. @xray/in.sock) for an
inbound listen address, not just path-based sockets. The form now allows
Port 0 for both, and the Address help text documents the @ form across
all locales. The backend already treated both prefixes as unix sockets.
@
2026-06-12 14:34:02 +02:00
MHSanaei f1a4286e2f feat(sub): per-inbound sort order for subscription links
Add a subSortIndex field to inbounds that controls the order of links
in subscription output only: the raw sub body, the HTML sub page, and
the JSON/Clash formats (all served from the same query). Lower values
come first; ties keep id order. The panel inbound list is unaffected.

The value is editable in the inbound form next to the share-address
fields, propagates to nodes via wireInbound, and follows the usual
node-sync rules (copied on import, mirrored while not dirty, never a
structural change).

Rescoped from #5214 by @Ponywka.
2026-06-12 12:03:22 +02:00
MHSanaei 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
2026-06-12 10:38:26 +02:00
MHSanaei 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.
2026-06-12 09:33:35 +02:00
MHSanaei 7e87b7dc60 i18n: point API token hint at the Authentication page in all locales
The remote panel's API token moved from Settings to the Authentication
page; update the node form hint accordingly.
2026-06-12 01:32:00 +02:00
MHSanaei a5e5640804 fix(inbound): explain how to unlock fallbacks on the inbound form (#5014)
The fallbacks card only renders for VLESS/Trojan over RAW with TLS or
Reality security, and a new inbound starts at security=none — so the Add
Inbound page looked like it had lost fallback support entirely. Show an
inline hint in that state pointing at the Security tab.
2026-06-12 01:21:38 +02:00
MHSanaei cc65f37164 fix(sub): honor per-inbound share address strategy in subscription output (#5208)
Subscriptions resolved a node-managed inbound's address to the node's
panel address unconditionally, so an inbound bound to a specific public
IP advertised an endpoint clients could not reach. The shareAddrStrategy
field added in #5162 only applied to panel share/QR links by design.

resolveInboundAddress now follows the same order as the panel's link
builder: 'listen' prefers a routable bind, 'custom' prefers shareAddr,
and the default 'node' keeps the existing node-first behavior, so output
is unchanged for inbounds that never set the field. Applies to raw,
JSON, and Clash subscriptions, which all resolve through this path.
Help text in all locales updated to drop the 'subscriptions are not
affected' caveat.
2026-06-11 21:31:27 +02:00
animesha3 554d85c2f7 feat: allow selecting inbounds synchronized from nodes (#5178)
* feat: select node inbounds for synchronization

Allow node owners to import either all remote inbounds or an explicit tag-based selection. Add remote inbound discovery, persistence, snapshot filtering, API documentation, tests, and localized UI labels.

* fix

* fix: scope node reconcile and orphan sweep to selected inbound tags

In 'selected' sync mode unselected inbounds never enter the panel DB, so
ReconcileNode treated them as undesired and deleted them from the node the
first time it went config-dirty. Reconcile now only sweeps remote tags that
are part of the selection; everything else on the node is unmanaged.

Panel-created or renamed inbounds on a selected-mode node also vanished:
their tag was outside the selection, so the next traffic pull filtered them
out of the snapshot and the orphan sweep silently dropped the central row.
AddInbound/UpdateInbound now allow the tag on the node before committing.

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-11 20:48:26 +02:00
iYuan 2a7342baa9 feat: add inbound share address strategy (#5162)
* feat: add inbound share address strategy

Allow node-managed inbounds to choose whether exported share links use the node address, routable listen address, or a custom endpoint. Preserve locally configured share address fields during remote node traffic sync.

Refs #5161

Refs #4891

* fix: preserve inbound share address settings

Forward share address fields to remote nodes, keep existing values when older update payloads omit them, align localhost handling between frontend and subscriptions, and preserve share address settings when cloning inbounds.

* fix: keep share address strategy out of subscriptions

Limit the new share address strategy to direct exported share links and QR codes. Restore subscription address resolution to the existing panel-owned behavior and update the UI help text accordingly.

* fix: address share address review feedback

* fix: validate custom share address

* fix

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-11 20:24:15 +02:00
MHSanaei 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.
2026-06-11 19:42:03 +02:00
Nikan Zeyaei 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>
2026-06-11 13:09:58 +02:00
Nikan Zeyaei ffde2f7ebf feat(sub): add Copy All Configs button to subscription page (#5163)
* feat(sub): add Copy All Configs button to subscription page

* fix(sub): include links in copyAll dependency array

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* chore: fmt

* fix(sub): drop module-level links from copyAll deps to satisfy exhaustive-deps

links is derived from window.__SUB_PAGE_DATA__ at module scope, so listing it in the useCallback dependency array triggers a react-hooks/exhaustive-deps warning (outer-scope value). Matches the existing single-link copy callback's deps.

---------

Co-authored-by: nikan <nikan>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-11 13:00:37 +02:00
MHSanaei ca4f32e3da feat: replace panel proxy URL with outbound-based egress bridge
Instead of requiring a manual SOCKS5/HTTP URL, the panel now lets the
admin pick an Xray outbound from a dropdown (same UX as Geodata
Auto-Update). At runtime, injectPanelEgress appends a loopback SOCKS
inbound (tag: panel-egress) and prepends a routing rule so the panel's
own HTTP traffic — version checks, Telegram, normal geo-file updates —
is routed through the chosen outbound. Xray-native Geodata Auto-Update
is unaffected (it uses its own geodata.outbound inside Xray). Blackhole
outbounds are excluded from both picker dropdowns since routing any
download through one just drops it. Translations updated for all 13
locales.
2026-06-10 23:52:20 +02:00
MHSanaei 6b16d8c37a feat: apply inbound/outbound/routing changes live via Xray gRPC API
Add a hot-apply layer that computes a diff between the old and new
generated config and applies only the changed parts through the Xray
gRPC HandlerService and RoutingService, avoiding a full process restart
whenever possible. A restart is still performed when sections that have
no reload API (log, dns, policy, observatory, ...) actually change.

Key additions:
- internal/xray/hot_diff.go: ComputeHotDiff with canonical-JSON
  comparison (sorted keys, null=absent, full number precision) so UI
  reformatting never triggers a spurious restart
- internal/xray/api.go: AddOutbound/DelOutbound, ApplyRoutingConfig,
  GetBalancerInfo, SetBalancerTarget, TestRoute gRPC wrappers
- internal/web/service/xray.go: tryHotApply, ensureAPIServices,
  GetBalancersStatus, OverrideBalancer, TestRoute service methods
- internal/web/controller/xray_setting.go: balancerStatus,
  balancerOverride, routeTest API endpoints
- frontend: BalancersTab live-status/override columns, RouteTester
  component, Restart button removed (Save now hot-applies)
- balancer-helpers.ts: syncObservatories never creates observatory
  sections for random/roundRobin balancers (no reload API → restart)
- i18n: balancerLive/Override/routeTester keys added to all 13 locales
2026-06-10 23:01:33 +02:00
MHSanaei 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
2026-06-10 18:27:12 +02:00
Sanaei 41645255f1 refactor: focused service files, leaf subpackages, and an internal/ layout (#5167)
* refactor(service): split client.go into focused files

client.go had grown to 4455 lines mixing ~10 responsibilities. Split it
verbatim into cohesive same-package files (no behavior change):

  client.go            foundation: ClientService, ClientWithAttachments,
                       ClientCreatePayload, ErrClientNotInInbound, sqlInChunk
  client_locks.go      inbound mutation locks, delete tombstones, compactOrphans
  client_lookup.go     read-only lookups (GetByID, List, EffectiveFlow, ...)
  client_link.go       inbound association sync (SyncInbound, DetachInbound, ...)
  client_crud.go       single-client CRUD + validation + protocol defaults
  client_inbound_apply.go  low-level inbound-settings mutators + by-email setters
  client_bulk.go       bulk attach/detach/adjust/delete/create + DelDepleted
  client_traffic.go    traffic-reset paths
  client_groups.go     client group management
  client_paging.go     paged listing, filtering, sorting, summary

Every declaration moved unchanged (verified: identical func/type/const/var
signature set before vs after). Imports redistributed per file via goimports.
go build ./..., go vet, and go test ./web/service/... all pass.

* refactor(service): split inbound.go into focused files

inbound.go was 4100 lines. Split it verbatim into cohesive same-package
files (no behavior change):

  inbound.go             core inbound CRUD + InboundService (keeps pkg doc)
  inbound_protocol.go    protocol / stream capability helpers
  inbound_node.go        node/runtime/remote coordination + online tracking
  inbound_traffic.go     traffic accounting, reset, client stats
  inbound_client_ips.go  per-client IP tracking
  inbound_clients.go     client lookups within inbounds + copy-clients
  inbound_disable.go     auto-disable invalid inbounds/clients
  inbound_migration.go   DB migrations
  inbound_sublink.go     subscription link providers
  inbound_util.go        generic slice/string helpers

Identical func/type/const/var signature set before vs after; package doc
comment preserved on inbound.go. Imports redistributed via goimports.
Build, vet, and go test ./web/service/... all pass.

* refactor(service): split tgbot.go into focused files

tgbot.go was 3738 lines dominated by a 1246-line answerCallback. Split it
verbatim into cohesive same-package files (no behavior change):

  tgbot.go           lifecycle, bot setup, caches, small utils
  tgbot_router.go    incoming update / command / callback dispatch
  tgbot_send.go      outbound messaging primitives
  tgbot_client.go    client views, actions, subscription links
  tgbot_inbound.go   inbound listing / pickers
  tgbot_report.go    server usage, exhausted, online, backups, notifications

Identical func/type/const/var signature set before vs after. Imports
redistributed via goimports. Build, vet, and go test ./web/service/... pass.

* refactor(client): dedupe single-field by-email setters

ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, and
ResetClientTrafficLimitByEmail shared an identical ~50-line body that
resolves the inbound by email, confirms the client exists, rewrites a
single-client settings payload, and delegates to UpdateInboundClient.

Extract that into applyClientFieldByEmail(inboundSvc, email, mutate) and
reduce each setter to a 3-line wrapper. Behavior is unchanged: same checks
and error strings, same single-client payload contract, same totalGB guard.

SetClientTelegramUserID (resolves by traffic id, different error text) and
ToggleClientEnableByEmail/SetClientEnableByEmail (different return shape and
a pre-read of the old state) intentionally keep their own bodies.

* refactor(service): extract panel/ subpackage

Move the panel-administration leaf services out of the flat service
package into web/service/panel/ (package panel):

  user.go         UserService (auth / 2FA / LDAP)
  panel.go        PanelService (restart / self-update) + version helpers
  panel_other.go  non-unix RestartPanel
  panel_unix.go   unix RestartPanel
  api_token.go    ApiTokenService
  websocket.go    WebSocketService
  panel_test.go   version/shellQuote unit tests

These are leaves: they depend on core (SettingService, Release) but no
core file references them, so the extraction creates no import cycle.
Core references are now qualified (service.SettingService, service.Release);
callers in main.go, web/web.go, and web/controller/* updated to panel.*.
Build, vet, and go test ./web/... pass.

* refactor(service): extract integration/ subpackage

Move the external-provider integration leaves into web/service/integration/
(package integration):

  warp.go        WarpService (Cloudflare WARP)
  nord.go        NordService (NordVPN)
  custom_geo.go  CustomGeoService (custom geo asset management)
  *_test.go      custom_geo / panel-proxy tests

These depend on core (SettingService, ServerService, XraySettingService) but
no core file references them. xray_setting.go stays in core because it calls
the unexported SettingService.saveSetting. The shared isBlockedIP SSRF helper
(used by core url_safety.go and by custom_geo) now has a small copy in each
package rather than being exported. Core references qualified; callers in
web/web.go, web/job/*, and web/controller/* updated to integration.*.
Build, vet, and go test ./web/... pass.

* refactor(service): extract tgbot/ subpackage

Move the Telegram bot (6 files + test) into web/service/tgbot/ (package
tgbot). It is a leaf: it embeds five core services (Inbound/Client/Setting/
Server/Xray) and the core never references it, so no import cycle.

To support the package boundary without changing behavior:
  - core exposes XrayProcess() *xray.Process so tgbot keeps calling the
    exact same running-process methods it used via the package-level `p`;
  - three core methods tgbot calls are exported: ClientService.checkIs-
    EnabledByEmail -> CheckIsEnabledByEmail, InboundService.getAllEmails ->
    GetAllEmails (callers updated in-package);
  - tgbot's embedded-field types and the few core type refs (Status,
    ClientCreatePayload, SanitizePublicHTTPURL) are now service-qualified.

Callers in main.go, web/web.go, web/job/*, and web/controller/* updated to
tgbot.*. Build, vet, and go test ./web/... pass.

* refactor(service): extract outbound/ subpackage

OutboundService (outbound.go) imports only neutral packages (config,
database, model, xray) and its production code is referenced by no core or
sibling service file — only by web/controller/xray_setting.go and
web/job/xray_traffic_job.go. Move it to web/service/outbound/ (package
outbound); no core qualification needed inside. Callers updated to outbound.*.

The one coupling was a tiny pure test helper, outboundsContainTag, used by
both outbound.go and the core outbound_subscription_test.go; it now has a
small copy in that test file rather than being shared across the boundary.
Build, vet, and go test ./web/... pass.

* refactor(util): move wireguard into its own subpackage

util/wireguard.go was the lone file of the root `util` package (24 lines,
one exported func GenerateWireguardKeypair), while every other util concern
lives in a focused subpackage (util/common, util/crypto, util/netsafe, ...).
Move it to util/wireguard/ (package wireguard) for consistency; its only
importer, web/service/integration/warp.go, is updated. The root `util`
package no longer exists.

* refactor(sub): drop redundant sub prefix from filenames

Inside package sub the subXxx.go prefix just repeats the package name
(like client_*.go did inside service). Rename for consistency; content and
type names are unchanged:

  subController.go    -> controller.go
  subService.go       -> service.go
  subClashService.go  -> clash_service.go
  subJsonService.go   -> json_service.go
  (+ matching _test.go files)

* refactor(controller): rename xui.go -> spa.go

XUIController serves the panel's single-page-app shell; spa.go names that
role plainly (the other controller files are domain-named). File rename only
— the type stays XUIController. api_docs_test.go keys route base paths by
filename, so its "xui.go" case is updated to "spa.go".

* refactor: move backend packages under internal/

Adopt the idiomatic Go application layout: the backend packages now live
under internal/ (a boundary the toolchain enforces), signalling private
implementation instead of a library-style flat root. No runtime behavior
changes — only import paths and a few build/config paths move.

Moved: config, database, logger, mtproto, sub, util, web, xray -> internal/.
main.go stays at the repo root and tools/openapigen stays under tools/ (both
still import internal/* because the internal rule keys off the module root).
The module path github.com/mhsanaei/3x-ui/v3 is unchanged; 149 .go files had
their import prefix rewritten to .../internal/<pkg>.

Couplings the Go compiler can't see, updated to the new layout:
  - frontend i18n imports of web/translation (react.ts, setup.components.ts)
  - vite outDir + eslint/tsconfig ignore globs -> internal/web/dist
  - Dockerfile COPY paths for web/dist and web/translation
  - locale.go os.DirFS("web") disk fallback -> "internal/web"
  - .gitignore and ci.yml go:embed stub for internal/web/dist
  - api_docs_test.go repo-root relative walk (one level deeper)
  - tools/openapigen filesystem package paths; ApiTokenView repointed to the
    web/service/panel subpackage and codegen regenerated (clears a stale
    type the ci.yml codegen check was failing on)

Verified: go build/vet/test (all packages), and frontend typecheck, lint,
vitest (478 tests), and production build into internal/web/dist.

* fix(config): keep test runs from writing logs into the source tree

GetLogFolder() returns a CWD-relative "./log" on Windows. Under `go test`
the working directory is each package's own folder, so InitLogger (called by
tests in web/job, web/service, xray, web/websocket) created stray log/
directories scattered through the source tree (e.g. internal/web/job/log/).

Redirect to a shared temp folder when testing.Testing() reports a test run.
Production behavior is unchanged: Windows still uses ./log next to the binary
and Linux /var/log/x-ui. The log files were always gitignored (*.log) and
never committed; this just stops the noise at the source.

* docs: move subscription-template guide out of root into docs/

sub_templates/ was a top-level folder holding only a README and no actual
templates (3x-ui ships none by design), referenced nowhere and unlinked from
any doc — it read like an empty placeholder cluttering the repo root.

Move the guide to docs/custom-subscription-templates.md (a proper docs home),
reword its intro to read as documentation rather than a folder note, link it
from the Features list in README.md, and drop the empty sub_templates/ folder.

* fix: update stale web/ path references after the internal/ move

The internal/ migration rewrote Go import paths but left some references to
the old top-level layout in docs, comments, and a few runtime disk paths.

Functional (dev-mode only): the disk-serving fallbacks that read the Vite
build from disk when running from source still pointed at web/dist/, which
moved to internal/web/dist/ — so `os.DirFS`/`os.Stat`/`os.ReadFile` in
internal/web/web.go and internal/sub/{sub,controller}.go are corrected.
Production was unaffected (it serves the embedded FS; verified by the Docker
build), but `go run` with a live frontend build silently fell back to embed.

Docs/comments: frontend/README.md, CONTRIBUTING.md, the claude-issue-bot and
release workflows, the openapigen -root help text, and assorted Go comments
now reference internal/web, internal/database, internal/sub, internal/xray,
etc. Package-name mentions (the "web" package), root paths (main.go,
frontend/, install scripts, /etc/x-ui), routes (/panel/api/xray), and the
historical "web/assets no longer exists" note were intentionally left as-is.

* refactor(web): remove the legacy /xui -> /panel redirect middleware

RedirectMiddleware existed only for backward compatibility with the old
`/xui` URL scheme (301-redirecting /xui and /xui/API to /panel and
/panel/api). That cutover was long ago, so drop the middleware, its
registration in initRouter, and the now-inaccurate "URL redirection"
mention in the middleware package doc. Old /xui URLs now 404 like any other
unknown path. HTTPS auto-redirect and auth redirects are unrelated and stay.

* build: fix .dockerignore for internal/ layout and exclude runtime dir

- web/dist -> internal/web/dist: the embedded frontend moved under internal/,
  so the stale exclude no longer matched and the locally-built dist could be
  sent to the build context (the frontend stage rebuilds it fresh anyway).
- exclude x-ui/: the local runtime directory (SQLite db, geo .dat files, xray
  binaries, certs — ~150MB) was being shipped into the build context for no
  reason. Verified the pattern excludes only the directory and still keeps
  x-ui.sh, which the Dockerfile copies to /usr/bin/x-ui.
2026-06-10 15:19:22 +02:00