fix(inbounds): reject missing TLS certificates before saving (#6429)

An inbound could be saved with security "tls" and a certificate row carrying
neither a file path nor inline content. Nothing rejected it, so the row reached
xray-core, whose readFileOrString fails with "both file and bytes are empty"
and takes the whole config build down with it — every other inbound included.

Validate the credentials on both sides of the wire. validateInboundTLSCertificates
follows xray's file-over-inline precedence, requires a private key for every
non-verify certificate and insists on at least one server certificate, so a
verify-only CA list no longer passes as a server config. The inbound form's Zod
schema enforces the same rules per field and serializes only the editor mode the
operator actually used, and a failed save jumps to the Security tab naming the
certificate row that broke.

On update the guard is scoped to a real TLS edit. A row already stored
incomplete is grandfathered: it stays editable, and only a save that breaks a
previously valid block is refused.

A sub-node stores whatever the master pushes, and Remote.UpdateInbound falls
back to AddInbound when the node does not yet hold the tag, so a grandfathered
row could otherwise never be deployed or re-seeded — the rejection is swallowed
to a logger.Debug line and the node stays on a stale config while the panel
shows the client as cut off. The controller now marks a node-sync request (mTLS
or a node-sync token) on a per-request copy of InboundService, and the guard
steps aside for it on both add and update: the row was judged where the
operator acted, and a node that refuses it only falls out of sync. Operator and
admin-token saves are held to the guard as before.

The security union is parameterised on its tlsSettings branch instead of copied,
and tlsCertUsesFiles is the one file-vs-inline inference shared by the form
schema and the adapter, so the mode the editor opens in and the pair of fields
the save serializes cannot drift apart.
This commit is contained in:
DuQi
2026-09-08 17:47:12 +02:00
committed by Sanaei
parent 9f76a66dcf
commit 47d2303334
28 changed files with 697 additions and 23 deletions
+4
View File
@@ -453,6 +453,7 @@
"scanRealityTargetPrivate": "Цель работает, но находится в приватной (локальной) сети.",
"invalidClientField": "Клиент {client}: поле {field} — {reason}",
"invalidField": "{field} — {reason}",
"invalidCertificate": "Сертификат TLS {index}: {reason}",
"moreIssues": "{message} (+{count} ещё)"
},
"form": {
@@ -631,6 +632,9 @@
"shortIds": "Short IDs",
"realityTargetHint": "Обязательно. Должно содержать порт (например, example.com:443). Без порта Xray-core не запускается.",
"realityTargetRequired": "Цель REALITY обязательна",
"tlsCertificateRequired": "Перед сохранением импортируйте сертификат TLS или укажите путь к его файлу",
"tlsPrivateKeyRequired": "Перед сохранением импортируйте закрытый ключ TLS или укажите путь к его файлу",
"tlsServerCertificateRequired": "Для TLS требуется хотя бы один сертификат сервера с закрытым ключом (encipherment или issue)",
"realityTargetNeedsPort": "Цель REALITY должна содержать порт (например, example.com:443)",
"realityTargetInvalidPort": "У цели REALITY указан недопустимый порт",
"scan": "Сканировать",