feat: apply inbound/outbound/routing changes live via Xray gRPC API

Add a hot-apply layer that computes a diff between the old and new
generated config and applies only the changed parts through the Xray
gRPC HandlerService and RoutingService, avoiding a full process restart
whenever possible. A restart is still performed when sections that have
no reload API (log, dns, policy, observatory, ...) actually change.

Key additions:
- internal/xray/hot_diff.go: ComputeHotDiff with canonical-JSON
  comparison (sorted keys, null=absent, full number precision) so UI
  reformatting never triggers a spurious restart
- internal/xray/api.go: AddOutbound/DelOutbound, ApplyRoutingConfig,
  GetBalancerInfo, SetBalancerTarget, TestRoute gRPC wrappers
- internal/web/service/xray.go: tryHotApply, ensureAPIServices,
  GetBalancersStatus, OverrideBalancer, TestRoute service methods
- internal/web/controller/xray_setting.go: balancerStatus,
  balancerOverride, routeTest API endpoints
- frontend: BalancersTab live-status/override columns, RouteTester
  component, Restart button removed (Save now hot-applies)
- balancer-helpers.ts: syncObservatories never creates observatory
  sections for random/roundRobin balancers (no reload API → restart)
- i18n: balancerLive/Override/routeTester keys added to all 13 locales
This commit is contained in:
MHSanaei
2026-06-10 23:01:33 +02:00
parent 3092326d9e
commit 6b16d8c37a
32 changed files with 2209 additions and 109 deletions
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "موازنات التحميل",
"balancerTagRequired": "الوسم مطلوب",
"balancerSelectorRequired": "اختر صادراً واحداً على الأقل",
"balancerLive": "الهدف الحالي",
"balancerOverride": "تجاوز الاختيار",
"balancerOverridePh": "تلقائي (الاستراتيجية)",
"balancerLiveRefresh": "تحديث حالة موازن التحميل",
"balancerNotRunning": "موازن التحميل ده مش نشط في Xray الشغال — احفظ التغييرات أو ابدأ Xray الأول",
"routeTester": "اختبار المسار",
"routeTesterDesc": "اسأل Xray الشغال أي صادر هيتعامل مع الاتصال ده. مفيش ترافيك بيتبعت — القرار بييجي مباشرة من محرك التوجيه الحي.",
"routeTesterDest": "نطاق أو IP",
"routeTesterPort": "المنفذ",
"routeTesterInbound": "الوارد",
"routeTesterProtocol": "البروتوكول المكتشف",
"routeTesterTest": "اختبر المسار",
"routeTesterMatchedOutbound": "الصادر المطابق",
"routeTesterViaBalancer": "عبر موازن التحميل",
"routeTesterDefaultOutbound": "ما في قاعدة توجيه اتطابقت — الترافيك رايح للصادر الافتراضي (الأول).",
"OutboundsDesc": "حدد مسار الترافيك الصادر.",
"Routings": "قواعد التوجيه",
"RoutingsDesc": "أولوية كل قاعدة مهمة جداً!",
+15
View File
@@ -1186,6 +1186,21 @@
"Balancers": "Balancers",
"balancerTagRequired": "Tag is required",
"balancerSelectorRequired": "Pick at least one outbound",
"balancerLive": "Live Target",
"balancerOverride": "Override",
"balancerOverridePh": "Auto (strategy)",
"balancerLiveRefresh": "Refresh live balancer state",
"balancerNotRunning": "This balancer is not active in the running Xray — save your changes or start Xray first",
"routeTester": "Route Tester",
"routeTesterDesc": "Ask the running Xray which outbound would handle a connection. No traffic is sent — the decision comes straight from the live routing engine.",
"routeTesterDest": "Domain or IP",
"routeTesterPort": "Port",
"routeTesterInbound": "Inbound",
"routeTesterProtocol": "Sniffed protocol",
"routeTesterTest": "Test Route",
"routeTesterMatchedOutbound": "Matched outbound",
"routeTesterViaBalancer": "via balancer",
"routeTesterDefaultOutbound": "No routing rule matched — traffic goes to the default (first) outbound.",
"OutboundsDesc": "Set the outgoing traffic pathway.",
"Routings": "Routing Rules",
"RoutingsDesc": "The priority of each rule is important!",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "Equilibradores",
"balancerTagRequired": "La etiqueta es obligatoria",
"balancerSelectorRequired": "Elige al menos una salida",
"balancerLive": "Destino actual",
"balancerOverride": "Forzar destino",
"balancerOverridePh": "Automático (estrategia)",
"balancerLiveRefresh": "Actualizar estado del balanceador",
"balancerNotRunning": "Este balanceador no está activo en el Xray en ejecución — guarda los cambios o inicia Xray primero",
"routeTester": "Prueba de ruta",
"routeTesterDesc": "Pregunta al Xray en ejecución qué salida gestionaría una conexión. No se envía tráfico real — la decisión viene directamente del motor de enrutamiento en vivo.",
"routeTesterDest": "Dominio o IP",
"routeTesterPort": "Puerto",
"routeTesterInbound": "Entrante",
"routeTesterProtocol": "Protocolo detectado",
"routeTesterTest": "Probar ruta",
"routeTesterMatchedOutbound": "Salida coincidente",
"routeTesterViaBalancer": "vía balanceador",
"routeTesterDefaultOutbound": "Ninguna regla de enrutamiento coincidió — el tráfico va a la salida predeterminada (primera).",
"OutboundsDesc": "Cambia la plantilla de configuración para definir formas de salida para este servidor.",
"Routings": "Reglas de enrutamiento",
"RoutingsDesc": "¡La prioridad de cada regla es importante!",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "بالانسرها",
"balancerTagRequired": "تگ الزامی است",
"balancerSelectorRequired": "حداقل یک خروجی انتخاب کنید",
"balancerLive": "هدف زنده",
"balancerOverride": "اجبار مسیر",
"balancerOverridePh": "خودکار (استراتژی)",
"balancerLiveRefresh": "به‌روزرسانی وضعیت زنده بالانسر",
"balancerNotRunning": "این بالانسر در Xray در حال اجرا فعال نیست — ابتدا تغییرات را ذخیره کنید یا Xray را روشن کنید",
"routeTester": "آزمایش مسیر",
"routeTesterDesc": "از Xray در حال اجرا بپرسید یک اتصال از کدام خروجی عبور می‌کند. هیچ ترافیکی ارسال نمی‌شود — پاسخ مستقیماً از موتور مسیریابی زنده می‌آید.",
"routeTesterDest": "دامنه یا IP",
"routeTesterPort": "پورت",
"routeTesterInbound": "ورودی",
"routeTesterProtocol": "پروتکل تشخیصی",
"routeTesterTest": "آزمایش مسیر",
"routeTesterMatchedOutbound": "خروجی منطبق",
"routeTesterViaBalancer": "از طریق بالانسر",
"routeTesterDefaultOutbound": "هیچ قانونی منطبق نشد — ترافیک به خروجی پیش‌فرض (اولین خروجی) می‌رود.",
"OutboundsDesc": "مسیر ترافیک خروجی را تنظیم کنید",
"Routings": "قوانین مسیریابی",
"RoutingsDesc": "اولویت هر قانون مهم است",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "Penyeimbang",
"balancerTagRequired": "Tag wajib diisi",
"balancerSelectorRequired": "Pilih setidaknya satu outbound",
"balancerLive": "Target Saat Ini",
"balancerOverride": "Paksa Tujuan",
"balancerOverridePh": "Otomatis (strategi)",
"balancerLiveRefresh": "Perbarui status penyeimbang beban",
"balancerNotRunning": "Penyeimbang ini tidak aktif di Xray yang berjalan — simpan perubahan atau mulai Xray terlebih dahulu",
"routeTester": "Uji Rute",
"routeTesterDesc": "Tanyakan kepada Xray yang berjalan outbound mana yang akan menangani koneksi. Tidak ada lalu lintas yang dikirim — keputusan langsung datang dari mesin routing langsung.",
"routeTesterDest": "Domain atau IP",
"routeTesterPort": "Port",
"routeTesterInbound": "Inbound",
"routeTesterProtocol": "Protokol yang terdeteksi",
"routeTesterTest": "Uji Rute",
"routeTesterMatchedOutbound": "Outbound yang cocok",
"routeTesterViaBalancer": "melalui penyeimbang",
"routeTesterDefaultOutbound": "Tidak ada aturan routing yang cocok — lalu lintas menuju outbound default (pertama).",
"OutboundsDesc": "Atur jalur lalu lintas keluar.",
"Routings": "Aturan Pengalihan",
"RoutingsDesc": "Prioritas setiap aturan penting!",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "負荷分散",
"balancerTagRequired": "タグは必須です",
"balancerSelectorRequired": "アウトバウンドを少なくとも1つ選んでください",
"balancerLive": "現在のターゲット",
"balancerOverride": "ターゲット強制",
"balancerOverridePh": "自動(ストラテジー)",
"balancerLiveRefresh": "ロードバランサーのライブ状態を更新",
"balancerNotRunning": "このバランサーは実行中の Xray でアクティブではありません — 変更を保存するか、先に Xray を起動してください",
"routeTester": "ルートテスト",
"routeTesterDesc": "実行中の Xray にどのアウトバウンドが接続を処理するか問い合わせます。実際のトラフィックは送信されません — 判断はライブルーティングエンジンから直接取得されます。",
"routeTesterDest": "ドメインまたは IP",
"routeTesterPort": "ポート",
"routeTesterInbound": "インバウンド",
"routeTesterProtocol": "検出されたプロトコル",
"routeTesterTest": "ルートをテスト",
"routeTesterMatchedOutbound": "マッチしたアウトバウンド",
"routeTesterViaBalancer": "バランサー経由",
"routeTesterDefaultOutbound": "ルーティングルールに一致しませんでした — トラフィックはデフォルト(最初の)アウトバウンドに送られます。",
"OutboundsDesc": "アウトバウンドトラフィックの送信方法を設定する",
"Routings": "ルーティングルール",
"RoutingsDesc": "各ルールの優先順位が重要です",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "Balanceadores",
"balancerTagRequired": "A tag é obrigatória",
"balancerSelectorRequired": "Selecione pelo menos uma saída",
"balancerLive": "Destino atual",
"balancerOverride": "Forçar destino",
"balancerOverridePh": "Automático (estratégia)",
"balancerLiveRefresh": "Atualizar estado do balanceador",
"balancerNotRunning": "Este balanceador não está ativo no Xray em execução — salve as alterações ou inicie o Xray primeiro",
"routeTester": "Teste de rota",
"routeTesterDesc": "Pergunte ao Xray em execução qual saída trataria uma conexão. Nenhum tráfego é enviado — a decisão vem diretamente do motor de roteamento ao vivo.",
"routeTesterDest": "Domínio ou IP",
"routeTesterPort": "Porta",
"routeTesterInbound": "Entrada",
"routeTesterProtocol": "Protocolo detectado",
"routeTesterTest": "Testar rota",
"routeTesterMatchedOutbound": "Saída correspondente",
"routeTesterViaBalancer": "via balanceador",
"routeTesterDefaultOutbound": "Nenhuma regra de roteamento correspondeu — o tráfego vai para a saída padrão (primeira).",
"OutboundsDesc": "Definir o caminho de saída do tráfego.",
"Routings": "Regras de Roteamento",
"RoutingsDesc": "A prioridade de cada regra é importante!",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "Балансировщик",
"balancerTagRequired": "Тег обязателен",
"balancerSelectorRequired": "Выберите хотя бы одно исходящее",
"balancerLive": "Текущая цель",
"balancerOverride": "Переопределить",
"balancerOverridePh": "Авто (стратегия)",
"balancerLiveRefresh": "Обновить состояние балансировщика",
"balancerNotRunning": "Этот балансировщик неактивен в запущенном Xray — сохраните изменения или запустите Xray",
"routeTester": "Тест маршрута",
"routeTesterDesc": "Спросите запущенный Xray, через какой исходящий будет обработано соединение. Трафик не отправляется — решение поступает напрямую от живого движка маршрутизации.",
"routeTesterDest": "Домен или IP",
"routeTesterPort": "Порт",
"routeTesterInbound": "Входящий",
"routeTesterProtocol": "Обнаруженный протокол",
"routeTesterTest": "Тест маршрута",
"routeTesterMatchedOutbound": "Совпавший исходящий",
"routeTesterViaBalancer": "через балансировщик",
"routeTesterDefaultOutbound": "Ни одно правило маршрутизации не совпало — трафик направляется в исходящий по умолчанию (первый).",
"OutboundsDesc": "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера",
"Routings": "Маршрутизация",
"RoutingsDesc": "Важен приоритет каждого правила!",
+15
View File
@@ -1184,6 +1184,21 @@
"Balancers": "Dengeleyiciler",
"balancerTagRequired": "Etiket zorunludur",
"balancerSelectorRequired": "En az bir giden bağlantı seçin",
"balancerLive": "Anlık Hedef",
"balancerOverride": "Hedef Zorla",
"balancerOverridePh": "Otomatik (strateji)",
"balancerLiveRefresh": "Dengeleyici durumunu yenile",
"balancerNotRunning": "Bu dengeleyici çalışan Xray'de etkin değil — değişikliklerinizi kaydedin veya önce Xray'i başlatın",
"routeTester": "Rota Testi",
"routeTesterDesc": "Çalışan Xray'e hangi giden bağlantının bir isteği işleyeceğini sorun. Gerçek trafik gönderilmez — karar doğrudan canlı yönlendirme motorundan gelir.",
"routeTesterDest": "Alan adı veya IP",
"routeTesterPort": "Port",
"routeTesterInbound": "Gelen",
"routeTesterProtocol": "Algılanan protokol",
"routeTesterTest": "Rotayı Test Et",
"routeTesterMatchedOutbound": "Eşleşen giden",
"routeTesterViaBalancer": "dengeleyici aracılığıyla",
"routeTesterDefaultOutbound": "Hiçbir yönlendirme kuralı eşleşmedi — trafik varsayılan (ilk) giden bağlantıya yönlendirilir.",
"OutboundsDesc": "Giden trafiğin yolunu ayarlayın.",
"Routings": "Yönlendirme Kuralları",
"RoutingsDesc": "Her kuralın önceliği önemlidir!",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "Балансери",
"balancerTagRequired": "Тег обов'язковий",
"balancerSelectorRequired": "Виберіть принаймні один вихідний",
"balancerLive": "Поточна ціль",
"balancerOverride": "Примусова ціль",
"balancerOverridePh": "Авто (стратегія)",
"balancerLiveRefresh": "Оновити стан балансувальника",
"balancerNotRunning": "Цей балансувальник неактивний у запущеному Xray — збережіть зміни або спочатку запустіть Xray",
"routeTester": "Тест маршруту",
"routeTesterDesc": "Запитайте запущений Xray, через який вихідний буде оброблено з'єднання. Реальний трафік не надсилається — рішення надходить безпосередньо від живого рушія маршрутизації.",
"routeTesterDest": "Домен або IP",
"routeTesterPort": "Порт",
"routeTesterInbound": "Вхідний",
"routeTesterProtocol": "Виявлений протокол",
"routeTesterTest": "Тест маршруту",
"routeTesterMatchedOutbound": "Відповідний вихідний",
"routeTesterViaBalancer": "через балансувальник",
"routeTesterDefaultOutbound": "Жодне правило маршрутизації не збіглося — трафік надходить до вихідного за замовчуванням (першого).",
"OutboundsDesc": "Встановити шлях вихідного трафіку.",
"Routings": "Правила маршрутизації",
"RoutingsDesc": "Пріоритет кожного правила важливий!",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "Cân bằng",
"balancerTagRequired": "Tag là bắt buộc",
"balancerSelectorRequired": "Chọn ít nhất một outbound",
"balancerLive": "Mục tiêu hiện tại",
"balancerOverride": "Ghi đè đích",
"balancerOverridePh": "Tự động (chiến lược)",
"balancerLiveRefresh": "Làm mới trạng thái bộ cân bằng tải",
"balancerNotRunning": "Bộ cân bằng này không hoạt động trong Xray đang chạy — hãy lưu thay đổi hoặc khởi động Xray trước",
"routeTester": "Kiểm tra tuyến đường",
"routeTesterDesc": "Hỏi Xray đang chạy outbound nào sẽ xử lý kết nối. Không có lưu lượng nào được gửi — quyết định đến thẳng từ công cụ định tuyến trực tiếp.",
"routeTesterDest": "Tên miền hoặc IP",
"routeTesterPort": "Cổng",
"routeTesterInbound": "Inbound",
"routeTesterProtocol": "Giao thức nhận diện",
"routeTesterTest": "Kiểm tra tuyến",
"routeTesterMatchedOutbound": "Outbound phù hợp",
"routeTesterViaBalancer": "qua bộ cân bằng tải",
"routeTesterDefaultOutbound": "Không có quy tắc định tuyến nào khớp — lưu lượng đến outbound mặc định (đầu tiên).",
"OutboundsDesc": "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này.",
"Routings": "Quy tắc định tuyến",
"RoutingsDesc": "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "负载均衡",
"balancerTagRequired": "标签为必填项",
"balancerSelectorRequired": "至少选择一个出站",
"balancerLive": "当前目标",
"balancerOverride": "强制指定",
"balancerOverridePh": "自动(策略)",
"balancerLiveRefresh": "刷新负载均衡器实时状态",
"balancerNotRunning": "此负载均衡器在运行中的 Xray 未激活 — 请先保存更改或启动 Xray",
"routeTester": "路由测试",
"routeTesterDesc": "向运行中的 Xray 查询某个连接将使用哪个出站。不会发送真实流量 — 结果直接来自实时路由引擎。",
"routeTesterDest": "域名或 IP",
"routeTesterPort": "端口",
"routeTesterInbound": "入站",
"routeTesterProtocol": "嗅探协议",
"routeTesterTest": "测试路由",
"routeTesterMatchedOutbound": "匹配出站",
"routeTesterViaBalancer": "经由负载均衡器",
"routeTesterDefaultOutbound": "无路由规则匹配 — 流量将发往默认(第一个)出站。",
"OutboundsDesc": "设置出站流量传出方式",
"Routings": "路由规则",
"RoutingsDesc": "每条规则的优先级都很重要",
+15
View File
@@ -1183,6 +1183,21 @@
"Balancers": "負載均衡",
"balancerTagRequired": "標籤為必填",
"balancerSelectorRequired": "至少選擇一個出站",
"balancerLive": "目前目標",
"balancerOverride": "強制指定",
"balancerOverridePh": "自動(策略)",
"balancerLiveRefresh": "重新整理負載均衡器即時狀態",
"balancerNotRunning": "此負載均衡器在執行中的 Xray 未啟用 — 請先儲存變更或啟動 Xray",
"routeTester": "路由測試",
"routeTesterDesc": "向執行中的 Xray 查詢某個連線將使用哪個出站。不會傳送真實流量 — 結果直接來自即時路由引擎。",
"routeTesterDest": "網域或 IP",
"routeTesterPort": "埠號",
"routeTesterInbound": "入站",
"routeTesterProtocol": "嗅探協議",
"routeTesterTest": "測試路由",
"routeTesterMatchedOutbound": "匹配出站",
"routeTesterViaBalancer": "經由負載均衡器",
"routeTesterDefaultOutbound": "無路由規則匹配 — 流量將導向預設(第一個)出站。",
"OutboundsDesc": "設定出站流量傳出方式",
"Routings": "路由規則",
"RoutingsDesc": "每條規則的優先順序都很重要",