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.
This commit is contained in:
Sanaei
2026-06-16 12:19:33 +02:00
committed by GitHub
parent f3eba04ed8
commit 37c5e0bfd2
51 changed files with 3073 additions and 1014 deletions
+87 -88
View File
@@ -446,6 +446,7 @@
"inboundClientAddSuccess": "کلاینت(های) ورودی اضافه شدند",
"inboundClientDeleteSuccess": "کلاینت ورودی حذف شد",
"inboundClientUpdateSuccess": "کلاینت ورودی به‌روزرسانی شد",
"savedNodeOfflineWillSync": "به‌صورت محلی ذخیره شد. یک نود پشتیبان آفلاین یا غیرفعال است — تغییر پس از اتصال مجدد همگام‌سازی می‌شود.",
"delDepletedClientsSuccess": "تمام کلاینت‌های مصرف شده حذف شدند",
"resetAllClientTrafficSuccess": "تمام ترافیک کلاینت بازنشانی شد",
"resetAllTrafficSuccess": "تمام ترافیک‌ها بازنشانی شدند",
@@ -912,6 +913,8 @@
"status": "وضعیت",
"cpu": "CPU",
"mem": "حافظه",
"netUp": "آپلود شبکه (KB/s)",
"netDown": "دانلود شبکه (KB/s)",
"uptime": "مدت فعالیت",
"latency": "تاخیر",
"lastHeartbeat": "آخرین ضربان",
@@ -953,13 +956,29 @@
"probeFailed": "بررسی ناموفق",
"updateStarted": "به‌روزرسانی پنل آغاز شد",
"updateResult": "به‌روزرسانی روی {ok} نود آغاز شد، {failed} ناموفق",
"updateNoneEligible": "حداقل یک نود آنلاین و فعال انتخاب کنید"
"updateNoneEligible": "حداقل یک نود آنلاین و فعال انتخاب کنید",
"saveMtls": "ذخیره mTLS نود"
},
"tlsVerifyMode": "اعتبارسنجی TLS",
"tlsVerifyModeHint": "اینکه پنل گواهی HTTPS نود را چطور بررسی کند. Pin یا Skip برای گواهی‌های self-signed است (فقط نودهای https).",
"tlsVerify": "اعتبارسنجی (CA پیش‌فرض)",
"tlsPin": "Pin گواهی (SHA-256)",
"tlsSkip": "رد کردن اعتبارسنجی",
"tlsMtls": "TLS متقابل (گواهی کلاینت)",
"mtlsFormHint": "این نود با یک گواهی کلاینت، پنل را احراز هویت می‌کند. CA این پنل را از بخش mTLS نود به نود کپی کنید، CA مورد اعتماد آن را تنظیم کنید و سپس آن را راه‌اندازی مجدد کنید.",
"mtls": {
"title": "mTLS نود",
"intro": "TLS متقابل علاوه بر توکن API، یک عامل گواهی کلاینت برای ارتباط بین نودها اضافه می‌کند. اختیاری است: برای استفاده فقط از احراز هویت با توکن، آن را خالی بگذارید.",
"copyCa": "کپی CA این پنل",
"copyCaHint": "این CA را به نودهایی که این پنل مدیریت می‌کند بدهید، سپس حالت اعتبارسنجی TLS آن‌ها را روی TLS متقابل تنظیم کنید.",
"caCopied": "گواهی CA در کلیپ‌بورد کپی شد",
"caFailed": "دریافت گواهی CA ناموفق بود",
"trustLabel": "CA مورد اعتماد (پنل والد)",
"trustHint": "وقتی این پنل خود یک نود است، CA پنل مدیریت‌کننده را اینجا بچسبانید تا گواهی کلاینت آن الزامی شود. برای اعمال، پنل را راه‌اندازی مجدد کنید.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "ذخیره CA مورد اعتماد",
"saved": "CA مورد اعتماد ذخیره شد — برای اعمال، پنل را راه‌اندازی مجدد کنید"
},
"tlsSkipWarning": "رد کردن اعتبارسنجی محافظت در برابر حملهٔ مرد میانی را از بین می‌برد و توکن API ممکن است شنود شود. ترجیحاً به‌جای آن گواهی را Pin کنید.",
"pinnedCert": "SHA-256 گواهیِ Pin‌شده",
"pinnedCertHint": "SHA-256 گواهیِ نود به‌صورت base64 یا hex. برای خواندنِ همین حالا از نود، از دکمهٔ Fetch استفاده کنید.",
@@ -1210,55 +1229,60 @@
"getOutboundTrafficError": "خطا در دریافت ترافیک خروجی",
"resetOutboundTrafficError": "خطا در بازنشانی ترافیک خروجی"
},
"emailNotifications": "اعلان‌ها",
"smtpSettings": "تنظیمات SMTP",
"smtpEnable": "فعال‌سازی اعلان‌های ایمیلی",
"smtpEnableDesc": "فعال‌سازی اعلان‌های ایمیلی از طریق SMTP",
"smtpHost": "میزبان SMTP",
"smtpHostDesc": "نام میزبان سرور SMTP (مثلاً smtp.gmail.com)",
"smtpPort": "پورت SMTP",
"smtpPortDesc": "پورت سرور SMTP (پیش‌فرض: ۵۸۷)",
"smtpUsername": "نام‌کاربری SMTP",
"smtpUsernameDesc": "نام‌کاربری احراز هویت SMTP",
"smtpPassword": "رمز عبور SMTP",
"smtpPasswordDesc": "رمز عبور احراز هویت SMTP",
"smtpTo": "گیرندگان",
"smtpToDesc": "آدرس‌های ایمیل گیرندگان، جداشده با کاما",
"emailSettings": "ایمیل",
"eventCPUHigh": "بالا بودن CPU (٪)",
"emailNotifications": "اعلان‌ها",
"smtpEventBusNotify": "اعلان‌های رویداد ایمیلی",
"smtpEventBusNotifyDesc": "انتخاب کنید کدام رویدادها اعلان ایمیلی را فعال می‌کنند",
"tgEventBusNotify": "اعلان‌های رویداد تلگرام",
"tgEventBusNotifyDesc": "انتخاب کنید کدام رویدادها اعلان تلگرام را فعال می‌کنند",
"testSmtp": "ارسال ایمیل آزمایشی",
"testTgBot": "ارسال پیام آزمایشی",
"eventGroupOutbound": "خروجی",
"eventGroupSecurity": "امنیت",
"eventGroupSystem": "سیستم",
"eventGroupXray": "هسته Xray",
"eventLoginAttempt": "تلاش برای ورود",
"eventGroupSystem": "سیستم",
"eventGroupSecurity": "امنیت",
"eventGroupNode": "نودها",
"eventOutboundDown": "قطع",
"eventOutboundUp": "وصل",
"eventXrayCrash": "کرش",
"eventNodeDown": "قطع",
"eventNodeUp": "وصل",
"eventCPUHigh": "بالا بودن CPU (٪)",
"requestFailed": "درخواست ناموفق بود",
"smtpEnable": "فعال‌سازی اعلان‌های ایمیلی",
"smtpEnableDesc": "فعال‌سازی اعلان‌های ایمیلی از طریق SMTP",
"smtpEncryption": "رمزنگاری",
"smtpEncryptionDesc": "روش رمزنگاری اتصال SMTP",
"smtpEncryptionNone": "هیچ‌کدام (متن ساده)",
"smtpEncryptionStartTLS": "STARTTLS",
"smtpEncryptionTLS": "TLS (ضمنی)",
"smtpEventBusNotify": "اعلان‌های رویداد ایمیلی",
"smtpEventBusNotifyDesc": "انتخاب کنید کدام رویدادها اعلان ایمیلی را فعال می‌کنند",
"smtpHost": "میزبان SMTP",
"smtpHostDesc": "نام میزبان سرور SMTP (مثلاً smtp.gmail.com)",
"smtpHostNotConfigured": "میزبان SMTP پیکربندی نشده است",
"smtpNoRecipients": "هیچ گیرنده‌ای پیکربندی نشده است",
"smtpNotInitialized": "SMTP مقداردهی اولیه نشده است",
"smtpPassword": "رمز عبور SMTP",
"smtpPasswordDesc": "رمز عبور احراز هویت SMTP",
"smtpPort": "پورت SMTP",
"smtpPortDesc": "پورت سرور SMTP (پیش‌فرض: ۵۸۷)",
"smtpSettings": "تنظیمات SMTP",
"smtpStageAuth": "احراز هویت",
"smtpStageConnect": "اتصال",
"smtpStageAuth": "احراز هویت",
"smtpStageSend": "ارسال",
"smtpTestSuccess": "ایمیل آزمایشی با موفقیت ارسال شد",
"smtpTo": "گیرندگان",
"smtpToDesc": "آدرس‌های ایمیل گیرندگان، جداشده با کاما",
"smtpUsername": "نام‌کاربری SMTP",
"smtpUsernameDesc": "نام‌کاربری احراز هویت SMTP",
"smtpHostNotConfigured": "میزبان SMTP پیکربندی نشده است",
"smtpNoRecipients": "هیچ گیرنده‌ای پیکربندی نشده است",
"eventLoginAttempt": "تلاش برای ورود",
"telegramTokenConfigured": "پیکربندی شده؛ برای حفظ توکن فعلی خالی بگذارید.",
"telegramTokenPlaceholder": "پیکربندی شده - برای جایگزینی، توکن جدید وارد کنید",
"testSmtp": "ارسال ایمیل آزمایشی",
"testTgBot": "ارسال پیام آزمایشی",
"smtpPasswordConfigured": "پیکربندی شده؛ برای حفظ رمز عبور فعلی خالی بگذارید.",
"smtpPasswordPlaceholder": "پیکربندی شده - برای جایگزینی، رمز عبور جدید وارد کنید",
"smtpNotInitialized": "SMTP مقداردهی اولیه نشده است",
"tgBotNotEnabled": "ربات تلگرام فعال نیست",
"tgBotNotRunning": "ربات تلگرام در حال اجرا نیست",
"tgEventBusNotify": "اعلان‌های رویداد تلگرام",
"tgEventBusNotifyDesc": "انتخاب کنید کدام رویدادها اعلان تلگرام را فعال می‌کنند",
"tgTestFailed": "آزمایش تلگرام ناموفق بود",
"tgTestSuccess": "پیام آزمایشی به تلگرام ارسال شد",
"tgBotNotRunning": "ربات تلگرام در حال اجرا نیست",
"smtpErrorAuth": "احراز هویت ناموفق بود — نام‌کاربری و رمز عبور را بررسی کنید",
"smtpErrorStarttls": "سرور به STARTTLS نیاز دارد — نوع رمزنگاری را تغییر دهید",
"smtpErrorTls": "سرور به TLS نیاز دارد — نوع رمزنگاری را تغییر دهید",
@@ -1266,12 +1290,7 @@
"smtpErrorTimeout": "مهلت اتصال به پایان رسید — میزبان در دسترس نیست",
"smtpErrorRelay": "سرور ارسال از این آدرس را رد می‌کند",
"smtpErrorEof": "اتصال توسط سرور بسته شد",
"smtpErrorUnknown": "خطای SMTP: {{ .Error }}",
"eventGroupNode": "نودها",
"eventNodeDown": "قطع",
"eventNodeUp": "وصل",
"smtpPasswordConfigured": "پیکربندی شده؛ برای حفظ رمز عبور فعلی خالی بگذارید.",
"smtpPasswordPlaceholder": "پیکربندی شده - برای جایگزینی، رمز عبور جدید وارد کنید"
"smtpErrorUnknown": "خطای SMTP: {{ .Error }}"
},
"xray": {
"title": "پیکربندی ایکس‌ری",
@@ -1319,6 +1338,8 @@
"Inbounds": "ورودی‌ها",
"InboundsDesc": "پذیرش کلاینت خاص",
"Outbounds": "خروجی‌ها",
"OutboundSubscriptions": "سابسکریپشن‌های خروجی",
"OutboundSubscriptionsDesc": "خروجی‌ها را از آدرس‌های سابسکریپشن راه‌دور (vmess/vless/trojan/ss/...) وارد کنید. تگ‌ها ثابت می‌مانند تا در بالانسرها و قوانین مسیریابی قابل استفاده باشند. به‌روزرسانی‌ها به‌صورت خودکار انجام می‌شوند.",
"Balancers": "بالانسرها",
"balancerTagRequired": "تگ الزامی است",
"balancerSelectorRequired": "حداقل یک خروجی انتخاب کنید",
@@ -1496,8 +1517,6 @@
"privateKey": "کلید خصوصی",
"load": "فشار سرور"
},
"OutboundSubscriptions": "سابسکریپشن‌های خروجی",
"OutboundSubscriptionsDesc": "خروجی‌ها را از آدرس‌های سابسکریپشن راه‌دور (vmess/vless/trojan/ss/...) وارد کنید. تگ‌ها ثابت می‌مانند تا در بالانسرها و قوانین مسیریابی قابل استفاده باشند. به‌روزرسانی‌ها به‌صورت خودکار انجام می‌شوند.",
"outboundSub": {
"manage": "سابسکریپشن‌ها",
"title": "سابسکریپشن‌های خروجی",
@@ -1775,17 +1794,17 @@
"SuccessResetTraffic": "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ✅ موفقیت‌آمیز",
"FailedResetTraffic": "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠️ خطا: [ {{ .ErrorMessage }} ]",
"FinishProcess": "🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید.",
"eventCPUHigh": "بالا بودن CPU",
"eventCPUHighDetail": "CPU: {{ .Detail }}",
"eventDelayDetail": "تأخیر: {{ .Delay }} میلی‌ثانیه",
"eventErrorDetail": "خطا: {{ .Error }}",
"eventLoginFallback": "ورود ناموفق از {{ .Source }}",
"eventOutboundDown": "خروجی {{ .Tag }} قطع است",
"eventOutboundUp": "خروجی {{ .Tag }} وصل است",
"eventErrorDetail": "خطا: {{ .Error }}",
"eventDelayDetail": "تأخیر: {{ .Delay }} میلی‌ثانیه",
"eventXrayCrash": "Xray کرش کرد",
"eventXrayCrashError": "خطا: {{ .Error }}",
"eventNodeDown": "نود {{ .Name }} قطع است",
"eventNodeUp": "نود {{ .Name }} وصل است"
"eventNodeUp": "نود {{ .Name }} وصل است",
"eventCPUHigh": "بالا بودن CPU",
"eventCPUHighDetail": "CPU: {{ .Detail }}",
"eventLoginFallback": "ورود ناموفق از {{ .Source }}"
},
"buttons": {
"closeKeyboard": "❌ بستن کیبورد",
@@ -1857,55 +1876,35 @@
}
},
"email": {
"labelDelay": "تأخیر",
"labelDetail": "جزئیات",
"labelError": "خطا",
"labelIP": "IP",
"labelOutbound": "خروجی",
"labelReason": "دلیل",
"labelSource": "مبدأ",
"labelStatus": "وضعیت",
"labelTime": "زمان",
"labelUsername": "نام‌کاربری",
"statusBanned": "BANNED",
"statusCrashed": "کرش کرد",
"statusDown": "قطع",
"statusFailed": "ناموفق",
"statusFull": "FULL",
"statusHigh": "بالا",
"statusOffline": "OFFLINE",
"statusOnline": "ONLINE",
"statusRunning": "در حال اجرا",
"statusSuccess": "موفق",
"statusUp": "وصل",
"statusXrayDown": "Xray DOWN",
"statusXrayUp": "Xray UP",
"subjectCPUHigh": "بالا بودن CPU",
"subjectDiskFull": "Disk full",
"subjectIPBanned": "IP banned: {{ .IP }}",
"subjectLoginFailed": "ورود ناموفق",
"subjectLoginSuccess": "ورود موفق",
"subjectNodeOffline": "Node {{ .Node }} is OFFLINE",
"subjectNodeOnline": "Node {{ .Node }} is ONLINE",
"subjectNodeXrayDown": "Node {{ .Node }} Xray is DOWN",
"subjectNodeXrayUp": "Node {{ .Node }} Xray is UP",
"subjectOutboundDown": "خروجی {{ .Tag }} قطع است",
"subjectOutboundUp": "خروجی {{ .Tag }} وصل است",
"subjectXrayCrash": "Xray کرش کرد",
"subjectXrayUp": "Xray is UP",
"titleCPUHigh": "بالا بودن CPU",
"titleDiskFull": "Disk full",
"titleIPBanned": "IP banned",
"titleLoginFailed": "ورود ناموفق",
"titleLoginSuccess": "ورود موفق",
"titleNodeOffline": "Node OFFLINE",
"titleNodeOnline": "Node ONLINE",
"titleNodeXrayDown": "Node Xray DOWN",
"titleNodeXrayUp": "Node Xray UP",
"subjectCPUHigh": "بالا بودن CPU",
"subjectLoginSuccess": "ورود موفق",
"subjectLoginFailed": "ورود ناموفق",
"titleOutboundDown": "خروجی قطع شد",
"titleOutboundUp": "خروجی وصل شد",
"titleXrayCrash": "Xray کرش کرد",
"titleXrayUp": "Xray UP",
"labelNode": "نود"
"titleCPUHigh": "بالا بودن CPU",
"titleLoginSuccess": "ورود موفق",
"titleLoginFailed": "ورود ناموفق",
"labelStatus": "وضعیت",
"labelOutbound": "خروجی",
"labelNode": "نود",
"labelError": "خطا",
"labelDelay": "تأخیر",
"labelDetail": "جزئیات",
"labelUsername": "نام‌کاربری",
"labelIP": "IP",
"labelReason": "دلیل",
"labelSource": "مبدأ",
"labelTime": "زمان",
"statusCrashed": "کرش کرد",
"statusRunning": "در حال اجرا",
"statusHigh": "بالا",
"statusSuccess": "موفق",
"statusFailed": "ناموفق",
"statusDown": "قطع",
"statusUp": "وصل"
}
}