fix(xray): clean stale routing references when a balancer or outbound is deleted (#5648)

* feat(xray): reference-cleanup helpers for entity deletion

When an outbound or balancer is deleted on the Xray page, routing rules and
balancers that reference it must be repaired in the same edit, or the saved
config breaks the core: a dangling balancerTag stops Router.Init (whole core
down), a dangling outboundTag black-holes matched traffic at the dispatcher.

Add pure plan*/apply* helpers that compute and apply the cleanup. A rule is
kept when a destination (outboundTag or balancerTag) remains and dropped when
none does. Deleting an outbound cascades: emptying a balancer selector removes
that balancer too, then repairs its rules in one pass against the full removed
set; fallbackTag and dialerProxy references are cleared and observatories
re-synced.

* fix(balancers): clean routing rules referencing a deleted balancer

Deleting a balancer left routing rules pointing at its balancerTag. xray-core's
Router.Init then fails ("balancer <tag> not found"), the core won't restart and
every inbound drops — the saved config passes CheckXrayConfig (JSON shape only),
so it breaks only on the next restart.

The delete confirm now lists the affected rules (modified vs removed) next to
the existing observatory warning and applies planBalancerDeletion's cleanup: a
rule keeps its outboundTag when present, otherwise the whole rule is dropped.
Adds the shared DeletionImpactList and refCleanup strings across all 13 locales.

* fix(outbounds): clean rules, balancer selectors and dialerProxy on outbound delete

Deleting an outbound left routing rules pointing at its outboundTag (matched
traffic black-holed at the dispatcher), plus stale references in balancer
selectors / fallbackTag and other outbounds' dialerProxy.

The delete confirm now shows planOutboundDeletion's impact and applies the
cascade: rules keep a remaining balancerTag (else are dropped), the tag is
pulled from balancer selectors and fallbacks, dialerProxy references are
cleared, and a balancer whose selector is emptied is removed along with its
own now-targetless rules.

* refactor(xray): share one rule classifier across preview and apply

Code review flagged that the keep/drop predicate was transcribed twice — in
ruleImpacts (the delete-modal preview) and in applyCleanup (the mutation) — kept
in sync only by a parity test. Extract a single classifyRule() that both call,
so the preview can never disagree with what apply actually does.

Also harden balancersEmptiedBy to skip tagless balancers: an empty/missing tag
would otherwise enter the removed set as "" and silently drop every other
tagless balancer (only reachable via a hand-edited config, but a silent data
loss). And remove observersRemovedByDeletingBalancer, orphaned once BalancersTab
switched to planBalancerDeletion.

* fix(xray): null-guard reference cleanup against unvalidated configs

The PR review noted that classifyRule and applyCleanup dereferenced rule /
balancer entries directly, while the sibling propagateOutboundTagRename uses
optional chaining — because fetchXrayConfig falls back to the unvalidated parsed
object when Zod validation fails, a stray null in rules / balancers can survive
into the editor and would throw during the delete preview/apply.

Match that defensive style: classifyRule and balancersEmptiedBy read through
optional chaining, the balancer loop skips nullish entries, and the dialerProxy
walk guards the outbound. A delete on a hand-edited config with null entries now
degrades gracefully instead of throwing.
This commit is contained in:
nima1024m
2026-06-29 12:52:18 +02:00
committed by GitHub
parent 71aca2018a
commit 7a5d6da28c
20 changed files with 634 additions and 75 deletions
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "هذا آخر موازن يستخدم Observatory، لذلك ستتم إزالته أيضًا.",
"deleteAlsoBurst": "هذا آخر موازن يستخدم Burst Observatory، لذلك ستتم إزالته أيضًا."
},
"refCleanup": {
"header": "حذف هذا سيُحدِّث التوجيه أيضًا:",
"ruleRemoved": "القاعدة {label} — أُزيلت (لا توجد وجهة متبقية)",
"ruleModified": "القاعدة {label} — مُحتفَظ بها (تستخدم الآن {keeps})",
"balancerRemoved": "الموازن {tag} — أُزيل (لا توجد أهداف متبقية)"
},
"balancer": {
"addBalancer": "أضف موازن تحميل",
"editBalancer": "عدل موازن التحميل",
+6
View File
@@ -1825,6 +1825,12 @@
"deleteAlsoObservatory": "This is the last balancer using the Observatory, so it will be removed too.",
"deleteAlsoBurst": "This is the last balancer using the Burst Observatory, so it will be removed too."
},
"refCleanup": {
"header": "Deleting this also updates your routing:",
"ruleRemoved": "Rule {label} — removed (no destination left)",
"ruleModified": "Rule {label} — kept (now uses {keeps})",
"balancerRemoved": "Balancer {tag} — removed (no targets left)"
},
"balancer": {
"addBalancer": "Add Balancer",
"editBalancer": "Edit Balancer",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "Este es el último balanceador que usa el Observatorio, por lo que también se eliminará.",
"deleteAlsoBurst": "Este es el último balanceador que usa el Observatorio Burst, por lo que también se eliminará."
},
"refCleanup": {
"header": "Al eliminar esto también se actualiza tu enrutamiento:",
"ruleRemoved": "Regla {label} — eliminada (sin destino restante)",
"ruleModified": "Regla {label} — conservada (ahora usa {keeps})",
"balancerRemoved": "Balanceador {tag} — eliminado (sin destinos restantes)"
},
"balancer": {
"addBalancer": "Agregar equilibrador",
"editBalancer": "Editar balanceador",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "این آخرین بالانسری است که از Observatory استفاده می‌کند، بنابراین آن هم حذف خواهد شد.",
"deleteAlsoBurst": "این آخرین بالانسری است که از Burst Observatory استفاده می‌کند، بنابراین آن هم حذف خواهد شد."
},
"refCleanup": {
"header": "حذف این مورد مسیریابی شما را هم به‌روز می‌کند:",
"ruleRemoved": "قاعده {label} — حذف شد (مقصدی باقی نماند)",
"ruleModified": "قاعده {label} — حفظ شد (اکنون از {keeps} استفاده می‌کند)",
"balancerRemoved": "بالانسر {tag} — حذف شد (هدفی باقی نماند)"
},
"balancer": {
"addBalancer": "افزودن بالانسر",
"editBalancer": "ویرایش بالانسر",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "Ini balancer terakhir yang memakai Observatory, jadi itu juga akan dihapus.",
"deleteAlsoBurst": "Ini balancer terakhir yang memakai Burst Observatory, jadi itu juga akan dihapus."
},
"refCleanup": {
"header": "Menghapus ini juga memperbarui perutean Anda:",
"ruleRemoved": "Aturan {label} — dihapus (tidak ada tujuan tersisa)",
"ruleModified": "Aturan {label} — dipertahankan (kini memakai {keeps})",
"balancerRemoved": "Balancer {tag} — dihapus (tidak ada target tersisa)"
},
"balancer": {
"addBalancer": "Tambahkan Penyeimbang",
"editBalancer": "Sunting Penyeimbang",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "これは Observatory を使用する最後のバランサーのため、こちらも削除されます。",
"deleteAlsoBurst": "これは Burst Observatory を使用する最後のバランサーのため、こちらも削除されます。"
},
"refCleanup": {
"header": "これを削除するとルーティングも更新されます:",
"ruleRemoved": "ルール {label} — 削除(送信先が残っていません)",
"ruleModified": "ルール {label} — 保持(現在は {keeps} を使用)",
"balancerRemoved": "バランサー {tag} — 削除(対象が残っていません)"
},
"balancer": {
"addBalancer": "負荷分散追加",
"editBalancer": "負荷分散編集",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "Este é o último balanceador que usa o Observatório, então ele também será removido.",
"deleteAlsoBurst": "Este é o último balanceador que usa o Observatório Burst, então ele também será removido."
},
"refCleanup": {
"header": "Excluir isto também atualiza o seu roteamento:",
"ruleRemoved": "Regra {label} — removida (sem destino restante)",
"ruleModified": "Regra {label} — mantida (agora usa {keeps})",
"balancerRemoved": "Balanceador {tag} — removido (sem destinos restantes)"
},
"balancer": {
"addBalancer": "Adicionar Balanceador",
"editBalancer": "Editar Balanceador",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "Это последний балансировщик, использующий Observatory, поэтому он тоже будет удалён.",
"deleteAlsoBurst": "Это последний балансировщик, использующий Burst Observatory, поэтому он тоже будет удалён."
},
"refCleanup": {
"header": "Удаление также обновит маршрутизацию:",
"ruleRemoved": "Правило {label} — удалено (не осталось назначения)",
"ruleModified": "Правило {label} — сохранено (теперь использует {keeps})",
"balancerRemoved": "Балансировщик {tag} — удалён (не осталось целей)"
},
"balancer": {
"addBalancer": "Создать балансировщик",
"editBalancer": "Редактировать балансировщик",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "Bu, Observatory kullanan son dengeleyici, bu yüzden o da kaldırılacak.",
"deleteAlsoBurst": "Bu, Burst Observatory kullanan son dengeleyici, bu yüzden o da kaldırılacak."
},
"refCleanup": {
"header": "Bunu silmek yönlendirmenizi de günceller:",
"ruleRemoved": "Kural {label} — kaldırıldı (hedef kalmadı)",
"ruleModified": "Kural {label} — korundu (artık {keeps} kullanıyor)",
"balancerRemoved": "Dengeleyici {tag} — kaldırıldı (hedef kalmadı)"
},
"balancer": {
"addBalancer": "Dengeleyici Ekle",
"editBalancer": "Dengeleyiciyi Düzenle",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "Це останній балансувальник, що використовує Observatory, тож його теж буде видалено.",
"deleteAlsoBurst": "Це останній балансувальник, що використовує Burst Observatory, тож його теж буде видалено."
},
"refCleanup": {
"header": "Видалення також оновить маршрутизацію:",
"ruleRemoved": "Правило {label} — видалено (не залишилося призначення)",
"ruleModified": "Правило {label} — збережено (тепер використовує {keeps})",
"balancerRemoved": "Балансувальник {tag} — видалено (не залишилося цілей)"
},
"balancer": {
"addBalancer": "Додати балансир",
"editBalancer": "Редагувати балансир",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "Đây là balancer cuối cùng dùng Observatory, nên nó cũng sẽ bị xóa.",
"deleteAlsoBurst": "Đây là balancer cuối cùng dùng Burst Observatory, nên nó cũng sẽ bị xóa."
},
"refCleanup": {
"header": "Xóa mục này cũng cập nhật định tuyến của bạn:",
"ruleRemoved": "Quy tắc {label} — đã xóa (không còn đích đến)",
"ruleModified": "Quy tắc {label} — giữ lại (giờ dùng {keeps})",
"balancerRemoved": "Balancer {tag} — đã xóa (không còn mục tiêu)"
},
"balancer": {
"addBalancer": "Thêm cân bằng",
"editBalancer": "Chỉnh sửa cân bằng",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "这是最后一个使用 Observatory 的负载均衡器,因此它也会被一并移除。",
"deleteAlsoBurst": "这是最后一个使用 Burst Observatory 的负载均衡器,因此它也会被一并移除。"
},
"refCleanup": {
"header": "删除此项还会更新你的路由:",
"ruleRemoved": "规则 {label} — 已移除(没有剩余出口)",
"ruleModified": "规则 {label} — 已保留(现使用 {keeps}",
"balancerRemoved": "负载均衡器 {tag} — 已移除(没有剩余目标)"
},
"balancer": {
"addBalancer": "添加负载均衡",
"editBalancer": "编辑负载均衡",
+6
View File
@@ -1709,6 +1709,12 @@
"deleteAlsoObservatory": "這是最後一個使用 Observatory 的負載平衡器,因此它也會一併被移除。",
"deleteAlsoBurst": "這是最後一個使用 Burst Observatory 的負載平衡器,因此它也會一併被移除。"
},
"refCleanup": {
"header": "刪除此項也會更新你的路由:",
"ruleRemoved": "規則 {label} — 已移除(沒有剩餘出口)",
"ruleModified": "規則 {label} — 已保留(現使用 {keeps}",
"balancerRemoved": "負載平衡器 {tag} — 已移除(沒有剩餘目標)"
},
"balancer": {
"addBalancer": "新增負載均衡",
"editBalancer": "編輯負載均衡",