mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-04 17:37:19 +00:00
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:
@@ -446,6 +446,7 @@
|
||||
"inboundClientAddSuccess": "Đã thêm client inbound",
|
||||
"inboundClientDeleteSuccess": "Đã xóa client inbound",
|
||||
"inboundClientUpdateSuccess": "Đã cập nhật client inbound",
|
||||
"savedNodeOfflineWillSync": "Đã lưu cục bộ. Một nút hỗ trợ đang ngoại tuyến hoặc bị tắt — thay đổi sẽ được đồng bộ khi kết nối lại.",
|
||||
"delDepletedClientsSuccess": "Đã xóa tất cả client hết hạn",
|
||||
"resetAllClientTrafficSuccess": "Đã đặt lại toàn bộ lưu lượng client",
|
||||
"resetAllTrafficSuccess": "Đã đặt lại toàn bộ lưu lượng",
|
||||
@@ -912,6 +913,8 @@
|
||||
"status": "Trạng thái",
|
||||
"cpu": "CPU",
|
||||
"mem": "Bộ nhớ",
|
||||
"netUp": "Mạng lên (KB/s)",
|
||||
"netDown": "Mạng xuống (KB/s)",
|
||||
"uptime": "Thời gian hoạt động",
|
||||
"latency": "Độ trễ",
|
||||
"lastHeartbeat": "Heartbeat gần nhất",
|
||||
@@ -953,13 +956,29 @@
|
||||
"probeFailed": "Kiểm tra thất bại",
|
||||
"updateStarted": "Đã bắt đầu cập nhật bảng điều khiển",
|
||||
"updateResult": "Đã kích hoạt cập nhật trên {ok} node, {failed} thất bại",
|
||||
"updateNoneEligible": "Chọn ít nhất một node trực tuyến và đang bật"
|
||||
"updateNoneEligible": "Chọn ít nhất một node trực tuyến và đang bật",
|
||||
"saveMtls": "Lưu mTLS nút"
|
||||
},
|
||||
"tlsVerifyMode": "Xác minh TLS",
|
||||
"tlsVerifyModeHint": "Cách panel xác thực chứng chỉ HTTPS của node. Ghim hoặc Bỏ qua dành cho chứng chỉ tự ký (chỉ node https).",
|
||||
"tlsVerify": "Xác minh (CA mặc định)",
|
||||
"tlsPin": "Ghim chứng chỉ (SHA-256)",
|
||||
"tlsSkip": "Bỏ qua xác minh",
|
||||
"tlsMtls": "TLS song phương (chứng chỉ máy khách)",
|
||||
"mtlsFormHint": "Nút này xác thực bảng điều khiển bằng chứng chỉ máy khách. Sao chép CA của bảng điều khiển này từ mục mTLS nút sang nút, đặt CA tin cậy của nó, rồi khởi động lại.",
|
||||
"mtls": {
|
||||
"title": "mTLS nút",
|
||||
"intro": "TLS song phương bổ sung yếu tố chứng chỉ máy khách bên cạnh token API cho các lệnh gọi giữa các nút. Đây là tùy chọn: để trống để chỉ dùng xác thực bằng token.",
|
||||
"copyCa": "Sao chép CA của bảng điều khiển này",
|
||||
"copyCaHint": "Cấp CA này cho các nút mà bảng điều khiển này quản lý, sau đó đặt chế độ xác minh TLS của chúng thành TLS song phương.",
|
||||
"caCopied": "Đã sao chép chứng chỉ CA vào bộ nhớ tạm",
|
||||
"caFailed": "Không lấy được chứng chỉ CA",
|
||||
"trustLabel": "CA tin cậy (bảng điều khiển cha)",
|
||||
"trustHint": "Khi bảng điều khiển này bản thân là một nút, hãy dán CA của bảng điều khiển quản lý vào đây để yêu cầu chứng chỉ máy khách của nó. Khởi động lại bảng điều khiển để áp dụng.",
|
||||
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
|
||||
"save": "Lưu CA tin cậy",
|
||||
"saved": "Đã lưu CA tin cậy — khởi động lại bảng điều khiển để áp dụng"
|
||||
},
|
||||
"tlsSkipWarning": "Bỏ qua xác minh sẽ loại bỏ bảo vệ trước tấn công xen giữa — token API có thể bị chặn bắt. Nên ghim chứng chỉ thay vì vậy.",
|
||||
"pinnedCert": "SHA-256 của chứng chỉ đã ghim",
|
||||
"pinnedCertHint": "SHA-256 của chứng chỉ node ở dạng base64 hoặc hex. Dùng Lấy để đọc trực tiếp từ node.",
|
||||
@@ -1210,55 +1229,60 @@
|
||||
"getOutboundTrafficError": "Lỗi khi lấy lưu lượng truy cập đi",
|
||||
"resetOutboundTrafficError": "Lỗi khi đặt lại lưu lượng truy cập đi"
|
||||
},
|
||||
"emailNotifications": "Thông báo",
|
||||
"smtpSettings": "Cài đặt SMTP",
|
||||
"smtpEnable": "Bật thông báo qua email",
|
||||
"smtpEnableDesc": "Bật thông báo qua email bằng SMTP",
|
||||
"smtpHost": "Máy chủ SMTP",
|
||||
"smtpHostDesc": "Tên máy chủ SMTP (ví dụ: smtp.gmail.com)",
|
||||
"smtpPort": "Cổng SMTP",
|
||||
"smtpPortDesc": "Cổng máy chủ SMTP (mặc định: 587)",
|
||||
"smtpUsername": "Tên đăng nhập SMTP",
|
||||
"smtpUsernameDesc": "Tên đăng nhập xác thực SMTP",
|
||||
"smtpPassword": "Mật khẩu SMTP",
|
||||
"smtpPasswordDesc": "Mật khẩu xác thực SMTP",
|
||||
"smtpTo": "Người nhận",
|
||||
"smtpToDesc": "Các địa chỉ email người nhận, phân cách bằng dấu phẩy",
|
||||
"emailSettings": "Email",
|
||||
"eventCPUHigh": "CPU cao (%)",
|
||||
"emailNotifications": "Thông báo",
|
||||
"smtpEventBusNotify": "Thông báo sự kiện qua email",
|
||||
"smtpEventBusNotifyDesc": "Chọn những sự kiện nào sẽ kích hoạt thông báo qua email",
|
||||
"tgEventBusNotify": "Thông báo sự kiện qua Telegram",
|
||||
"tgEventBusNotifyDesc": "Chọn những sự kiện nào sẽ kích hoạt thông báo qua Telegram",
|
||||
"testSmtp": "Gửi email thử nghiệm",
|
||||
"testTgBot": "Gửi tin nhắn thử nghiệm",
|
||||
"eventGroupOutbound": "Outbound",
|
||||
"eventGroupSecurity": "Bảo mật",
|
||||
"eventGroupSystem": "Hệ thống",
|
||||
"eventGroupXray": "Xray Core",
|
||||
"eventLoginAttempt": "Lần thử đăng nhập",
|
||||
"eventGroupSystem": "Hệ thống",
|
||||
"eventGroupSecurity": "Bảo mật",
|
||||
"eventGroupNode": "Node",
|
||||
"eventOutboundDown": "Ngừng hoạt động",
|
||||
"eventOutboundUp": "Hoạt động",
|
||||
"eventXrayCrash": "Sự cố",
|
||||
"eventNodeDown": "Ngừng hoạt động",
|
||||
"eventNodeUp": "Hoạt động",
|
||||
"eventCPUHigh": "CPU cao (%)",
|
||||
"requestFailed": "Yêu cầu thất bại",
|
||||
"smtpEnable": "Bật thông báo qua email",
|
||||
"smtpEnableDesc": "Bật thông báo qua email bằng SMTP",
|
||||
"smtpEncryption": "Mã hóa",
|
||||
"smtpEncryptionDesc": "Phương thức mã hóa kết nối SMTP",
|
||||
"smtpEncryptionNone": "Không (văn bản thuần)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (ngầm định)",
|
||||
"smtpEventBusNotify": "Thông báo sự kiện qua email",
|
||||
"smtpEventBusNotifyDesc": "Chọn những sự kiện nào sẽ kích hoạt thông báo qua email",
|
||||
"smtpHost": "Máy chủ SMTP",
|
||||
"smtpHostDesc": "Tên máy chủ SMTP (ví dụ: smtp.gmail.com)",
|
||||
"smtpHostNotConfigured": "Chưa cấu hình máy chủ SMTP",
|
||||
"smtpNoRecipients": "Chưa cấu hình người nhận",
|
||||
"smtpNotInitialized": "SMTP chưa được khởi tạo",
|
||||
"smtpPassword": "Mật khẩu SMTP",
|
||||
"smtpPasswordDesc": "Mật khẩu xác thực SMTP",
|
||||
"smtpPort": "Cổng SMTP",
|
||||
"smtpPortDesc": "Cổng máy chủ SMTP (mặc định: 587)",
|
||||
"smtpSettings": "Cài đặt SMTP",
|
||||
"smtpStageAuth": "Xác thực",
|
||||
"smtpStageConnect": "Kết nối",
|
||||
"smtpStageAuth": "Xác thực",
|
||||
"smtpStageSend": "Gửi",
|
||||
"smtpTestSuccess": "Đã gửi email thử nghiệm thành công",
|
||||
"smtpTo": "Người nhận",
|
||||
"smtpToDesc": "Các địa chỉ email người nhận, phân cách bằng dấu phẩy",
|
||||
"smtpUsername": "Tên đăng nhập SMTP",
|
||||
"smtpUsernameDesc": "Tên đăng nhập xác thực SMTP",
|
||||
"smtpHostNotConfigured": "Chưa cấu hình máy chủ SMTP",
|
||||
"smtpNoRecipients": "Chưa cấu hình người nhận",
|
||||
"eventLoginAttempt": "Lần thử đăng nhập",
|
||||
"telegramTokenConfigured": "Đã cấu hình; để trống để giữ token hiện tại.",
|
||||
"telegramTokenPlaceholder": "Đã cấu hình - nhập token mới để thay thế",
|
||||
"testSmtp": "Gửi email thử nghiệm",
|
||||
"testTgBot": "Gửi tin nhắn thử nghiệm",
|
||||
"smtpPasswordConfigured": "Đã cấu hình; để trống để giữ mật khẩu hiện tại.",
|
||||
"smtpPasswordPlaceholder": "Đã cấu hình - nhập mật khẩu mới để thay thế",
|
||||
"smtpNotInitialized": "SMTP chưa được khởi tạo",
|
||||
"tgBotNotEnabled": "Bot Telegram chưa được bật",
|
||||
"tgBotNotRunning": "Bot Telegram không hoạt động",
|
||||
"tgEventBusNotify": "Thông báo sự kiện qua Telegram",
|
||||
"tgEventBusNotifyDesc": "Chọn những sự kiện nào sẽ kích hoạt thông báo qua Telegram",
|
||||
"tgTestFailed": "Thử nghiệm Telegram thất bại",
|
||||
"tgTestSuccess": "Đã gửi tin nhắn thử nghiệm tới Telegram",
|
||||
"tgBotNotRunning": "Bot Telegram không hoạt động",
|
||||
"smtpErrorAuth": "Xác thực thất bại — kiểm tra tên đăng nhập và mật khẩu",
|
||||
"smtpErrorStarttls": "Máy chủ yêu cầu STARTTLS — thay đổi kiểu mã hóa",
|
||||
"smtpErrorTls": "Máy chủ yêu cầu TLS — thay đổi kiểu mã hóa",
|
||||
@@ -1266,12 +1290,7 @@
|
||||
"smtpErrorTimeout": "Hết thời gian kết nối — không thể truy cập máy chủ",
|
||||
"smtpErrorRelay": "Máy chủ từ chối gửi từ địa chỉ này",
|
||||
"smtpErrorEof": "Kết nối đã bị máy chủ đóng",
|
||||
"smtpErrorUnknown": "Lỗi SMTP: {{ .Error }}",
|
||||
"eventGroupNode": "Node",
|
||||
"eventNodeDown": "Ngừng hoạt động",
|
||||
"eventNodeUp": "Hoạt động",
|
||||
"smtpPasswordConfigured": "Đã cấu hình; để trống để giữ mật khẩu hiện tại.",
|
||||
"smtpPasswordPlaceholder": "Đã cấu hình - nhập mật khẩu mới để thay thế"
|
||||
"smtpErrorUnknown": "Lỗi SMTP: {{ .Error }}"
|
||||
},
|
||||
"xray": {
|
||||
"title": "Cài đặt Xray",
|
||||
@@ -1319,6 +1338,8 @@
|
||||
"Inbounds": "Inbound",
|
||||
"InboundsDesc": "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể.",
|
||||
"Outbounds": "Outbound",
|
||||
"OutboundSubscriptions": "Đăng ký Outbound",
|
||||
"OutboundSubscriptionsDesc": "Nhập các outbound từ URL đăng ký từ xa (vmess/vless/trojan/ss/...). Tag được giữ ổn định để dùng trong bộ cân bằng tải và quy tắc định tuyến. Cập nhật diễn ra tự động.",
|
||||
"Balancers": "Cân bằng",
|
||||
"balancerTagRequired": "Tag là bắt buộc",
|
||||
"balancerSelectorRequired": "Chọn ít nhất một outbound",
|
||||
@@ -1496,8 +1517,6 @@
|
||||
"privateKey": "Khóa riêng",
|
||||
"load": "Tải"
|
||||
},
|
||||
"OutboundSubscriptions": "Đăng ký Outbound",
|
||||
"OutboundSubscriptionsDesc": "Nhập các outbound từ URL đăng ký từ xa (vmess/vless/trojan/ss/...). Tag được giữ ổn định để dùng trong bộ cân bằng tải và quy tắc định tuyến. Cập nhật diễn ra tự động.",
|
||||
"outboundSub": {
|
||||
"manage": "Đăng ký",
|
||||
"title": "Đăng ký Outbound",
|
||||
@@ -1775,17 +1794,17 @@
|
||||
"SuccessResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ✅ Thành công",
|
||||
"FailedResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠️ Lỗi: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng.",
|
||||
"eventCPUHigh": "CPU cao",
|
||||
"eventCPUHighDetail": "CPU: {{ .Detail }}",
|
||||
"eventDelayDetail": "Độ trễ: {{ .Delay }}ms",
|
||||
"eventErrorDetail": "Lỗi: {{ .Error }}",
|
||||
"eventLoginFallback": "Đăng nhập thất bại từ {{ .Source }}",
|
||||
"eventOutboundDown": "Outbound {{ .Tag }} đã NGỪNG HOẠT ĐỘNG",
|
||||
"eventOutboundUp": "Outbound {{ .Tag }} đã HOẠT ĐỘNG",
|
||||
"eventErrorDetail": "Lỗi: {{ .Error }}",
|
||||
"eventDelayDetail": "Độ trễ: {{ .Delay }}ms",
|
||||
"eventXrayCrash": "Xray GẶP SỰ CỐ",
|
||||
"eventXrayCrashError": "Lỗi: {{ .Error }}",
|
||||
"eventNodeDown": "Node {{ .Name }} đã NGỪNG HOẠT ĐỘNG",
|
||||
"eventNodeUp": "Node {{ .Name }} đã HOẠT ĐỘNG"
|
||||
"eventNodeUp": "Node {{ .Name }} đã HOẠT ĐỘNG",
|
||||
"eventCPUHigh": "CPU cao",
|
||||
"eventCPUHighDetail": "CPU: {{ .Detail }}",
|
||||
"eventLoginFallback": "Đăng nhập thất bại từ {{ .Source }}"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Đóng Bàn Phím",
|
||||
@@ -1857,55 +1876,35 @@
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"labelDelay": "Độ trễ",
|
||||
"labelDetail": "Chi tiết",
|
||||
"labelError": "Lỗi",
|
||||
"labelIP": "IP",
|
||||
"labelOutbound": "Outbound",
|
||||
"labelReason": "Lý do",
|
||||
"labelSource": "Nguồn",
|
||||
"labelStatus": "Trạng thái",
|
||||
"labelTime": "Thời gian",
|
||||
"labelUsername": "Tên đăng nhập",
|
||||
"statusBanned": "BANNED",
|
||||
"statusCrashed": "GẶP SỰ CỐ",
|
||||
"statusDown": "NGỪNG HOẠT ĐỘNG",
|
||||
"statusFailed": "THẤT BẠI",
|
||||
"statusFull": "FULL",
|
||||
"statusHigh": "CAO",
|
||||
"statusOffline": "OFFLINE",
|
||||
"statusOnline": "ONLINE",
|
||||
"statusRunning": "Đang chạy",
|
||||
"statusSuccess": "THÀNH CÔNG",
|
||||
"statusUp": "HOẠT ĐỘNG",
|
||||
"statusXrayDown": "Xray DOWN",
|
||||
"statusXrayUp": "Xray UP",
|
||||
"subjectCPUHigh": "CPU cao",
|
||||
"subjectDiskFull": "Disk full",
|
||||
"subjectIPBanned": "IP banned: {{ .IP }}",
|
||||
"subjectLoginFailed": "Đăng nhập thất bại",
|
||||
"subjectLoginSuccess": "Đăng nhập thành công",
|
||||
"subjectNodeOffline": "Node {{ .Node }} is OFFLINE",
|
||||
"subjectNodeOnline": "Node {{ .Node }} is ONLINE",
|
||||
"subjectNodeXrayDown": "Node {{ .Node }} Xray is DOWN",
|
||||
"subjectNodeXrayUp": "Node {{ .Node }} Xray is UP",
|
||||
"subjectOutboundDown": "Outbound {{ .Tag }} đã NGỪNG HOẠT ĐỘNG",
|
||||
"subjectOutboundUp": "Outbound {{ .Tag }} đã HOẠT ĐỘNG",
|
||||
"subjectXrayCrash": "Xray GẶP SỰ CỐ",
|
||||
"subjectXrayUp": "Xray is UP",
|
||||
"titleCPUHigh": "CPU cao",
|
||||
"titleDiskFull": "Disk full",
|
||||
"titleIPBanned": "IP banned",
|
||||
"titleLoginFailed": "Đăng nhập thất bại",
|
||||
"titleLoginSuccess": "Đăng nhập thành công",
|
||||
"titleNodeOffline": "Node OFFLINE",
|
||||
"titleNodeOnline": "Node ONLINE",
|
||||
"titleNodeXrayDown": "Node Xray DOWN",
|
||||
"titleNodeXrayUp": "Node Xray UP",
|
||||
"subjectCPUHigh": "CPU cao",
|
||||
"subjectLoginSuccess": "Đăng nhập thành công",
|
||||
"subjectLoginFailed": "Đăng nhập thất bại",
|
||||
"titleOutboundDown": "Outbound NGỪNG HOẠT ĐỘNG",
|
||||
"titleOutboundUp": "Outbound HOẠT ĐỘNG",
|
||||
"titleXrayCrash": "Xray GẶP SỰ CỐ",
|
||||
"titleXrayUp": "Xray UP",
|
||||
"labelNode": "Node"
|
||||
"titleCPUHigh": "CPU cao",
|
||||
"titleLoginSuccess": "Đăng nhập thành công",
|
||||
"titleLoginFailed": "Đăng nhập thất bại",
|
||||
"labelStatus": "Trạng thái",
|
||||
"labelOutbound": "Outbound",
|
||||
"labelNode": "Node",
|
||||
"labelError": "Lỗi",
|
||||
"labelDelay": "Độ trễ",
|
||||
"labelDetail": "Chi tiết",
|
||||
"labelUsername": "Tên đăng nhập",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Lý do",
|
||||
"labelSource": "Nguồn",
|
||||
"labelTime": "Thời gian",
|
||||
"statusCrashed": "GẶP SỰ CỐ",
|
||||
"statusRunning": "Đang chạy",
|
||||
"statusHigh": "CAO",
|
||||
"statusSuccess": "THÀNH CÔNG",
|
||||
"statusFailed": "THẤT BẠI",
|
||||
"statusDown": "NGỪNG HOẠT ĐỘNG",
|
||||
"statusUp": "HOẠT ĐỘNG"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user