mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-21 14:42:19 +07: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": "已添加入站客户端",
|
||||
"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 证书。固定或跳过用于自签名证书(仅 https 节点)。",
|
||||
"tlsVerify": "校验(默认 CA)",
|
||||
"tlsPin": "固定证书(SHA-256)",
|
||||
"tlsSkip": "跳过校验",
|
||||
"tlsMtls": "双向 TLS(客户端证书)",
|
||||
"mtlsFormHint": "此节点使用客户端证书对面板进行认证。请从“节点 mTLS”区域复制本面板的 CA 到该节点,设置其受信任的 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 令牌可能被截获。建议改用固定证书。",
|
||||
"pinnedCert": "固定证书的 SHA-256",
|
||||
"pinnedCertHint": "节点证书的 SHA-256(base64 或 hex)。点击“获取”可立即从节点读取。",
|
||||
@@ -1210,55 +1229,60 @@
|
||||
"getOutboundTrafficError": "获取出站流量错误",
|
||||
"resetOutboundTrafficError": "重置出站流量错误"
|
||||
},
|
||||
"emailNotifications": "通知",
|
||||
"smtpSettings": "SMTP 设置",
|
||||
"smtpEnable": "启用邮件通知",
|
||||
"smtpEnableDesc": "通过 SMTP 启用邮件通知",
|
||||
"smtpHost": "SMTP 主机",
|
||||
"smtpHostDesc": "SMTP 服务器主机名(例如 smtp.gmail.com)",
|
||||
"smtpPort": "SMTP 端口",
|
||||
"smtpPortDesc": "SMTP 服务器端口(默认:587)",
|
||||
"smtpUsername": "SMTP 用户名",
|
||||
"smtpUsernameDesc": "SMTP 认证用户名",
|
||||
"smtpPassword": "SMTP 密码",
|
||||
"smtpPasswordDesc": "SMTP 认证密码",
|
||||
"smtpTo": "收件人",
|
||||
"smtpToDesc": "以逗号分隔的收件人邮箱地址",
|
||||
"emailSettings": "邮件",
|
||||
"eventCPUHigh": "CPU 占用过高(%)",
|
||||
"emailNotifications": "通知",
|
||||
"smtpEventBusNotify": "邮件事件通知",
|
||||
"smtpEventBusNotifyDesc": "选择触发邮件通知的事件",
|
||||
"tgEventBusNotify": "Telegram 事件通知",
|
||||
"tgEventBusNotifyDesc": "选择触发 Telegram 通知的事件",
|
||||
"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 服务器端口(默认:587)",
|
||||
"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": "Telegram 机器人未启用",
|
||||
"tgBotNotRunning": "Telegram 机器人未运行",
|
||||
"tgEventBusNotify": "Telegram 事件通知",
|
||||
"tgEventBusNotifyDesc": "选择触发 Telegram 通知的事件",
|
||||
"tgTestFailed": "Telegram 测试失败",
|
||||
"tgTestSuccess": "测试消息已发送至 Telegram",
|
||||
"tgBotNotRunning": "Telegram 机器人未运行",
|
||||
"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": "Xray 配置",
|
||||
@@ -1319,6 +1338,8 @@
|
||||
"Inbounds": "入站",
|
||||
"InboundsDesc": "接受来自特定客户端的流量",
|
||||
"Outbounds": "出站",
|
||||
"OutboundSubscriptions": "出站订阅",
|
||||
"OutboundSubscriptionsDesc": "从远程订阅 URL(vmess/vless/trojan/ss/…)导入出站。标签保持稳定,可用于负载均衡器和路由规则。更新会自动进行。",
|
||||
"Balancers": "负载均衡",
|
||||
"balancerTagRequired": "标签为必填项",
|
||||
"balancerSelectorRequired": "至少选择一个出站",
|
||||
@@ -1496,8 +1517,6 @@
|
||||
"privateKey": "私钥",
|
||||
"load": "负载"
|
||||
},
|
||||
"OutboundSubscriptions": "出站订阅",
|
||||
"OutboundSubscriptionsDesc": "从远程订阅 URL(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": "恢复"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user