mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-13 14:50:59 +00:00
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:
@@ -4,12 +4,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service/integration"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service/outbound"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -46,6 +48,9 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||
g.POST("/testOutbound", a.testOutbound)
|
||||
g.POST("/balancerStatus", a.balancerStatus)
|
||||
g.POST("/balancerOverride", a.balancerOverride)
|
||||
g.POST("/routeTest", a.routeTest)
|
||||
|
||||
// Outbound subscription (remote outbound lists)
|
||||
g.GET("/outbound-subs", a.listOutboundSubs)
|
||||
@@ -120,7 +125,9 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
jsonObj(c, string(result), nil)
|
||||
}
|
||||
|
||||
// updateSetting updates the Xray configuration settings.
|
||||
// updateSetting updates the Xray configuration settings and applies them to
|
||||
// the running core right away — through the gRPC API when only inbounds,
|
||||
// outbounds or routing rules changed, with a process restart otherwise.
|
||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
xraySetting := c.PostForm("xraySetting")
|
||||
if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {
|
||||
@@ -135,6 +142,13 @@ func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
// Only reconcile a running core; a manually stopped xray stays stopped.
|
||||
if a.XrayService.IsXrayRunning() {
|
||||
if err := a.XrayService.RestartXray(false); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
|
||||
}
|
||||
|
||||
@@ -272,6 +286,77 @@ func (a *XraySettingController) testOutbound(c *gin.Context) {
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
// balancerStatus reports the live state (override + strategy picks) of the
|
||||
// balancer tags given as a comma-separated "tags" form field.
|
||||
func (a *XraySettingController) balancerStatus(c *gin.Context) {
|
||||
raw := c.PostForm("tags")
|
||||
var tags []string
|
||||
for _, tag := range strings.Split(raw, ",") {
|
||||
if tag = strings.TrimSpace(tag); tag != "" {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
statuses, err := a.XrayService.GetBalancersStatus(tags)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
byTag := make(map[string]service.BalancerStatus, len(statuses))
|
||||
for _, status := range statuses {
|
||||
byTag[status.Tag] = status
|
||||
}
|
||||
jsonObj(c, byTag, nil)
|
||||
}
|
||||
|
||||
// balancerOverride forces a balancer to a specific outbound tag; an empty
|
||||
// "target" clears the override.
|
||||
func (a *XraySettingController) balancerOverride(c *gin.Context) {
|
||||
tag := c.PostForm("tag")
|
||||
if tag == "" {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("tag is required"))
|
||||
return
|
||||
}
|
||||
target := c.PostForm("target")
|
||||
if err := a.XrayService.OverrideBalancer(tag, target); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, "", nil)
|
||||
}
|
||||
|
||||
// routeTest asks the running core which outbound it would route a synthetic
|
||||
// connection to.
|
||||
func (a *XraySettingController) routeTest(c *gin.Context) {
|
||||
port := 0
|
||||
if portStr := c.PostForm("port"); portStr != "" {
|
||||
parsed, err := strconv.Atoi(portStr)
|
||||
if err != nil || parsed < 0 || parsed > 65535 {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("invalid port"))
|
||||
return
|
||||
}
|
||||
port = parsed
|
||||
}
|
||||
req := xray.RouteTestRequest{
|
||||
InboundTag: c.PostForm("inboundTag"),
|
||||
Domain: c.PostForm("domain"),
|
||||
IP: c.PostForm("ip"),
|
||||
Port: port,
|
||||
Network: c.PostForm("network"),
|
||||
Protocol: c.PostForm("protocol"),
|
||||
Email: c.PostForm("email"),
|
||||
}
|
||||
if req.Domain == "" && req.IP == "" {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("domain or ip is required"))
|
||||
return
|
||||
}
|
||||
result, err := a.XrayService.TestRoute(req)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
// --- Outbound Subscription handlers ---
|
||||
|
||||
func (a *XraySettingController) listOutboundSubs(c *gin.Context) {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
"StatsService",
|
||||
"RoutingService"
|
||||
],
|
||||
"tag": "api"
|
||||
},
|
||||
|
||||
@@ -115,6 +115,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
xrayConfig.LogConfig = resolveXrayLogPaths(xrayConfig.LogConfig)
|
||||
xrayConfig.API = ensureAPIServices(xrayConfig.API)
|
||||
|
||||
_, _, _ = s.inboundService.AddTraffic(nil, nil)
|
||||
|
||||
@@ -306,6 +307,47 @@ func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) {
|
||||
cfg.OutboundConfigs = json_util.RawMessage(combined)
|
||||
}
|
||||
|
||||
// ensureAPIServices guarantees the gRPC services the panel depends on are
|
||||
// listed in the generated config's api block: HandlerService and StatsService
|
||||
// have always been required for inbound/user management and traffic polling,
|
||||
// and RoutingService enables hot routing reload on templates saved before it
|
||||
// was added to the default template. The stored template itself is not
|
||||
// modified — only the generated runtime config.
|
||||
func ensureAPIServices(api json_util.RawMessage) json_util.RawMessage {
|
||||
if len(api) == 0 {
|
||||
// No api block means the panel's API integration is deliberately
|
||||
// disabled; don't resurrect it behind the user's back.
|
||||
return api
|
||||
}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(api, &parsed); err != nil {
|
||||
return api
|
||||
}
|
||||
services, _ := parsed["services"].([]any)
|
||||
have := make(map[string]bool, len(services))
|
||||
for _, svc := range services {
|
||||
if name, ok := svc.(string); ok {
|
||||
have[name] = true
|
||||
}
|
||||
}
|
||||
added := false
|
||||
for _, name := range []string{"HandlerService", "StatsService", "RoutingService"} {
|
||||
if !have[name] {
|
||||
services = append(services, name)
|
||||
added = true
|
||||
}
|
||||
}
|
||||
if !added {
|
||||
return api
|
||||
}
|
||||
parsed["services"] = services
|
||||
out, err := json.Marshal(parsed)
|
||||
if err != nil {
|
||||
return api
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// resolveXrayLogPaths rewrites relative `log.access` / `log.error` values to
|
||||
// absolute paths under config.GetLogFolder(), so Xray writes those files
|
||||
// alongside the panel's other logs regardless of the working directory the
|
||||
@@ -378,7 +420,81 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
|
||||
return traffic, clientTraffic, nil
|
||||
}
|
||||
|
||||
// RestartXray restarts the Xray process, optionally forcing a restart even if config unchanged.
|
||||
// BalancerStatus is the live view of one balancer for the panel UI. Running
|
||||
// is false when the balancer isn't present in the running core (e.g. xray is
|
||||
// stopped or the balancer hasn't been saved/applied yet).
|
||||
type BalancerStatus struct {
|
||||
Tag string `json:"tag"`
|
||||
Running bool `json:"running"`
|
||||
Override string `json:"override"`
|
||||
Selected []string `json:"selected"`
|
||||
}
|
||||
|
||||
// GetBalancersStatus queries the running core for the live state of the
|
||||
// given balancer tags. Per-tag failures are reported as Running=false rather
|
||||
// than failing the whole call, so the UI can render saved-but-not-applied
|
||||
// balancers alongside live ones.
|
||||
func (s *XrayService) GetBalancersStatus(tags []string) ([]BalancerStatus, error) {
|
||||
statuses := make([]BalancerStatus, 0, len(tags))
|
||||
if !s.IsXrayRunning() {
|
||||
for _, tag := range tags {
|
||||
statuses = append(statuses, BalancerStatus{Tag: tag})
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.xrayAPI.Close()
|
||||
|
||||
for _, tag := range tags {
|
||||
info, err := s.xrayAPI.GetBalancerInfo(tag)
|
||||
if err != nil {
|
||||
logger.Debug("get balancer info [", tag, "] failed:", err)
|
||||
statuses = append(statuses, BalancerStatus{Tag: tag})
|
||||
continue
|
||||
}
|
||||
statuses = append(statuses, BalancerStatus{
|
||||
Tag: tag,
|
||||
Running: true,
|
||||
Override: info.Override,
|
||||
Selected: info.Selected,
|
||||
})
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
// OverrideBalancer forces a balancer in the running core to use the given
|
||||
// outbound tag; an empty target clears the override.
|
||||
func (s *XrayService) OverrideBalancer(tag, target string) error {
|
||||
if !s.IsXrayRunning() {
|
||||
return errors.New("xray is not running")
|
||||
}
|
||||
if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.xrayAPI.Close()
|
||||
return s.xrayAPI.SetBalancerTarget(tag, target)
|
||||
}
|
||||
|
||||
// TestRoute asks the running core which outbound its router picks for the
|
||||
// described connection.
|
||||
func (s *XrayService) TestRoute(req xray.RouteTestRequest) (*xray.RouteTestResult, error) {
|
||||
if !s.IsXrayRunning() {
|
||||
return nil, errors.New("xray is not running")
|
||||
}
|
||||
if err := s.xrayAPI.Init(p.GetAPIPort()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.xrayAPI.Close()
|
||||
return s.xrayAPI.TestRoute(req)
|
||||
}
|
||||
|
||||
// RestartXray reconciles the running Xray process with the current desired
|
||||
// config. When isForce is false it first tries to apply the changes through
|
||||
// the Xray gRPC API without restarting the process (inbounds, outbounds and
|
||||
// routing rules/balancers are hot-reloadable); only changes the core cannot
|
||||
// take at runtime — or a force request — stop and restart the process.
|
||||
func (s *XrayService) RestartXray(isForce bool) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
@@ -391,10 +507,15 @@ func (s *XrayService) RestartXray(isForce bool) error {
|
||||
}
|
||||
|
||||
if s.IsXrayRunning() {
|
||||
if !isForce && p.GetConfig().Equals(xrayConfig) && !isNeedXrayRestart.Load() {
|
||||
configUnchanged := p.GetConfig().Equals(xrayConfig)
|
||||
if !isForce && configUnchanged && !isNeedXrayRestart.Load() {
|
||||
logger.Debug("It does not need to restart Xray")
|
||||
return nil
|
||||
}
|
||||
if !isForce && !configUnchanged && s.tryHotApply(xrayConfig) {
|
||||
logger.Info("Xray config changes applied through the core API, no restart needed")
|
||||
return nil
|
||||
}
|
||||
p.Stop()
|
||||
}
|
||||
|
||||
@@ -409,6 +530,112 @@ func (s *XrayService) RestartXray(isForce bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryHotApply attempts to reconcile the running Xray instance with newCfg
|
||||
// through the core gRPC API (HandlerService for inbounds/outbounds,
|
||||
// RoutingService for rules/balancers). It returns true when the running
|
||||
// instance now matches newCfg; on any failure it returns false and the
|
||||
// caller falls back to a full process restart, which cleans up whatever was
|
||||
// partially applied. Callers must hold the package-level lock.
|
||||
func (s *XrayService) tryHotApply(newCfg *xray.Config) bool {
|
||||
oldCfg := p.GetConfig()
|
||||
diff, ok := xray.ComputeHotDiff(oldCfg, newCfg)
|
||||
if !ok {
|
||||
logger.Debug("hot apply: config change is not API-applicable, falling back to restart")
|
||||
return false
|
||||
}
|
||||
if diff.Empty() {
|
||||
p.SetConfig(newCfg)
|
||||
return true
|
||||
}
|
||||
|
||||
apiPort := p.GetAPIPort()
|
||||
if apiPort <= 0 {
|
||||
return false
|
||||
}
|
||||
// A dedicated client: s.xrayAPI may be in use by traffic polling on other
|
||||
// service instances and is reset around restarts.
|
||||
hotAPI := xray.XrayAPI{}
|
||||
if err := hotAPI.Init(apiPort); err != nil {
|
||||
logger.Debug("hot apply: failed to init xray api:", err)
|
||||
return false
|
||||
}
|
||||
defer hotAPI.Close()
|
||||
|
||||
// Removals first so changed handlers and port swaps never collide with
|
||||
// the additions that follow.
|
||||
for _, tag := range diff.RemovedInboundTags {
|
||||
if err := hotAPI.DelInbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
|
||||
logger.Info("hot apply: remove inbound [", tag, "] failed:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, tag := range diff.RemovedOutboundTags {
|
||||
if err := hotAPI.DelOutbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
|
||||
logger.Info("hot apply: remove outbound [", tag, "] failed:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, ob := range diff.AddedOutbounds {
|
||||
if err := addOutboundReconciling(&hotAPI, ob); err != nil {
|
||||
logger.Info("hot apply: add outbound failed:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, ib := range diff.AddedInbounds {
|
||||
if err := addInboundReconciling(&hotAPI, ib); err != nil {
|
||||
logger.Info("hot apply: add inbound failed:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if diff.RoutingConfig != nil {
|
||||
if err := hotAPI.ApplyRoutingConfig(diff.RoutingConfig); err != nil {
|
||||
logger.Info("hot apply: apply routing config failed:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
p.SetConfig(newCfg)
|
||||
return true
|
||||
}
|
||||
|
||||
// addInboundReconciling adds an inbound, and on a tag conflict (the handler
|
||||
// was already created through the runtime API while the stored snapshot was
|
||||
// stale) replaces the existing handler instead.
|
||||
func addInboundReconciling(api *xray.XrayAPI, inbound []byte) error {
|
||||
err := api.AddInbound(inbound)
|
||||
if err == nil || !xray.IsExistingTagErr(err) {
|
||||
return err
|
||||
}
|
||||
var meta struct {
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
if jsonErr := json.Unmarshal(inbound, &meta); jsonErr != nil || meta.Tag == "" {
|
||||
return err
|
||||
}
|
||||
if delErr := api.DelInbound(meta.Tag); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
|
||||
return delErr
|
||||
}
|
||||
return api.AddInbound(inbound)
|
||||
}
|
||||
|
||||
// addOutboundReconciling mirrors addInboundReconciling for outbounds.
|
||||
func addOutboundReconciling(api *xray.XrayAPI, outbound []byte) error {
|
||||
err := api.AddOutbound(outbound)
|
||||
if err == nil || !xray.IsExistingTagErr(err) {
|
||||
return err
|
||||
}
|
||||
var meta struct {
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
if jsonErr := json.Unmarshal(outbound, &meta); jsonErr != nil || meta.Tag == "" {
|
||||
return err
|
||||
}
|
||||
if delErr := api.DelOutbound(meta.Tag); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
|
||||
return delErr
|
||||
}
|
||||
return api.AddOutbound(outbound)
|
||||
}
|
||||
|
||||
// StopXray stops the running Xray process.
|
||||
func (s *XrayService) StopXray() error {
|
||||
lock.Lock()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||
)
|
||||
|
||||
func TestEnsureAPIServices(t *testing.T) {
|
||||
// legacy template without RoutingService gets it injected
|
||||
out := ensureAPIServices(json_util.RawMessage(`{"services":["HandlerService","LoggerService","StatsService"],"tag":"api"}`))
|
||||
var parsed struct {
|
||||
Services []string `json:"services"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &parsed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]bool{"HandlerService": true, "StatsService": true, "RoutingService": true, "LoggerService": true}
|
||||
if len(parsed.Services) != 4 {
|
||||
t.Fatalf("expected 4 services, got %v", parsed.Services)
|
||||
}
|
||||
for _, svc := range parsed.Services {
|
||||
if !want[svc] {
|
||||
t.Fatalf("unexpected service %q", svc)
|
||||
}
|
||||
}
|
||||
if parsed.Tag != "api" {
|
||||
t.Fatalf("tag must be preserved, got %q", parsed.Tag)
|
||||
}
|
||||
|
||||
// complete api block is returned unchanged (no marshal churn)
|
||||
full := json_util.RawMessage(`{"services":["HandlerService","StatsService","RoutingService"],"tag":"api"}`)
|
||||
if got := ensureAPIServices(full); string(got) != string(full) {
|
||||
t.Fatalf("complete api block must pass through untouched, got %s", got)
|
||||
}
|
||||
|
||||
// absent api block stays absent
|
||||
if got := ensureAPIServices(nil); got != nil {
|
||||
t.Fatalf("nil api block must stay nil, got %s", got)
|
||||
}
|
||||
}
|
||||
@@ -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": "أولوية كل قاعدة مهمة جداً!",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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": "اولویت هر قانون مهم است",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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": "各ルールの優先順位が重要です",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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": "Важен приоритет каждого правила!",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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": "Пріоритет кожного правила важливий!",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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": "每条规则的优先级都很重要",
|
||||
|
||||
@@ -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": "每條規則的優先順序都很重要",
|
||||
|
||||
Reference in New Issue
Block a user