Explorar o código

feat(fallbacks): add per-rule dest override

Operators can now type an explicit dest (e.g. "8443", "127.0.0.1:8443",
"/dev/shm/x.sock") on each fallback row to override the auto-resolved
child listen+port. Empty keeps the existing auto behavior.

Adds the column to inbound_fallbacks (GORM AutoMigrate), threads it
through the panel form, API docs, and translations.
MHSanaei hai 1 día
pai
achega
798e18b6ee

+ 11 - 15
database/model/model.go

@@ -41,17 +41,17 @@ type User struct {
 
 
 // Inbound represents an Xray inbound configuration with traffic statistics and settings.
 // Inbound represents an Xray inbound configuration with traffic statistics and settings.
 type Inbound struct {
 type Inbound struct {
-	Id                   int                  `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`                                                    // Unique identifier
-	UserId               int                  `json:"-"`                                                                                               // Associated user ID
-	Up                   int64                `json:"up" form:"up"`                                                                                    // Upload traffic in bytes
-	Down                 int64                `json:"down" form:"down"`                                                                                // Download traffic in bytes
-	Total                int64                `json:"total" form:"total"`                                                                              // Total traffic limit in bytes
-	Remark               string               `json:"remark" form:"remark"`                                                                            // Human-readable remark
-	Enable               bool                 `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"`                           // Whether the inbound is enabled
-	ExpiryTime           int64                `json:"expiryTime" form:"expiryTime"`                                                                    // Expiration timestamp
+	Id                   int                  `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`                                                                                                                 // Unique identifier
+	UserId               int                  `json:"-"`                                                                                                                                                            // Associated user ID
+	Up                   int64                `json:"up" form:"up"`                                                                                                                                                 // Upload traffic in bytes
+	Down                 int64                `json:"down" form:"down"`                                                                                                                                             // Download traffic in bytes
+	Total                int64                `json:"total" form:"total"`                                                                                                                                           // Total traffic limit in bytes
+	Remark               string               `json:"remark" form:"remark"`                                                                                                                                         // Human-readable remark
+	Enable               bool                 `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"`                                                                                        // Whether the inbound is enabled
+	ExpiryTime           int64                `json:"expiryTime" form:"expiryTime"`                                                                                                                                 // Expiration timestamp
 	TrafficReset         string               `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule
 	TrafficReset         string               `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule
-	LastTrafficResetTime int64                `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"`                               // Last traffic reset timestamp
-	ClientStats          []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`                        // Client traffic statistics
+	LastTrafficResetTime int64                `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"`                                                                                            // Last traffic reset timestamp
+	ClientStats          []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`                                                                                     // Client traffic statistics
 
 
 	// Xray configuration fields
 	// Xray configuration fields
 	Listen         string   `json:"listen" form:"listen"`
 	Listen         string   `json:"listen" form:"listen"`
@@ -513,11 +513,6 @@ type ClientInbound struct {
 
 
 func (ClientInbound) TableName() string { return "client_inbounds" }
 func (ClientInbound) TableName() string { return "client_inbounds" }
 
 
-// InboundFallback is one routing rule on a master inbound's
-// settings.fallbacks array. The master is always a VLESS or Trojan
-// inbound on TCP transport with TLS or Reality. The child is any other
-// inbound — its listen+port becomes the fallback dest, with optional
-// SNI/ALPN/path match criteria pulled from the same row.
 type InboundFallback struct {
 type InboundFallback struct {
 	Id        int    `json:"id" gorm:"primaryKey;autoIncrement"`
 	Id        int    `json:"id" gorm:"primaryKey;autoIncrement"`
 	MasterId  int    `json:"masterId" gorm:"index;not null;column:master_id"`
 	MasterId  int    `json:"masterId" gorm:"index;not null;column:master_id"`
@@ -525,6 +520,7 @@ type InboundFallback struct {
 	Name      string `json:"name"`
 	Name      string `json:"name"`
 	Alpn      string `json:"alpn"`
 	Alpn      string `json:"alpn"`
 	Path      string `json:"path"`
 	Path      string `json:"path"`
+	Dest      string `json:"dest"`
 	Xver      int    `json:"xver"`
 	Xver      int    `json:"xver"`
 	SortOrder int    `json:"sortOrder" gorm:"default:0;column:sort_order"`
 	SortOrder int    `json:"sortOrder" gorm:"default:0;column:sort_order"`
 }
 }

+ 0 - 1
database/model/model_test.go

@@ -188,4 +188,3 @@ func TestInboundClientIpsUnmarshalJSONAcceptsBothShapes(t *testing.T) {
 		})
 		})
 	}
 	}
 }
 }
-

+ 4 - 2
frontend/public/openapi.json

@@ -863,7 +863,7 @@
         "tags": [
         "tags": [
           "Inbounds"
           "Inbounds"
         ],
         ],
-        "summary": "List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/xver match criteria.",
+        "summary": "List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/dest/xver match criteria. When dest is empty the child inbound's listen+port is used.",
         "operationId": "get_panel_api_inbounds_id_fallbacks",
         "operationId": "get_panel_api_inbounds_id_fallbacks",
         "parameters": [
         "parameters": [
           {
           {
@@ -903,6 +903,7 @@
                       "name": "",
                       "name": "",
                       "alpn": "",
                       "alpn": "",
                       "path": "/vlws",
                       "path": "/vlws",
+                      "dest": "",
                       "xver": 2,
                       "xver": 2,
                       "sortOrder": 0
                       "sortOrder": 0
                     }
                     }
@@ -946,7 +947,8 @@
                   },
                   },
                   {
                   {
                     "childId": 12,
                     "childId": 12,
-                    "alpn": "h2"
+                    "alpn": "h2",
+                    "dest": "8443"
                   }
                   }
                 ]
                 ]
               }
               }

+ 4 - 4
frontend/src/pages/api-docs/endpoints.ts

@@ -199,12 +199,12 @@ export const sections: readonly Section[] = [
       {
       {
         method: 'GET',
         method: 'GET',
         path: '/panel/api/inbounds/:id/fallbacks',
         path: '/panel/api/inbounds/:id/fallbacks',
-        summary: 'List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/xver match criteria.',
+        summary: 'List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/dest/xver match criteria. When dest is empty the child inbound\'s listen+port is used.',
         params: [
         params: [
           { name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' },
           { name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' },
         ],
         ],
         response:
         response:
-          '{\n  "success": true,\n  "obj": [\n    {\n      "id": 1,\n      "masterId": 10,\n      "childId": 11,\n      "name": "",\n      "alpn": "",\n      "path": "/vlws",\n      "xver": 2,\n      "sortOrder": 0\n    }\n  ]\n}',
+          '{\n  "success": true,\n  "obj": [\n    {\n      "id": 1,\n      "masterId": 10,\n      "childId": 11,\n      "name": "",\n      "alpn": "",\n      "path": "/vlws",\n      "dest": "",\n      "xver": 2,\n      "sortOrder": 0\n    }\n  ]\n}',
       },
       },
       {
       {
         method: 'POST',
         method: 'POST',
@@ -212,9 +212,9 @@ export const sections: readonly Section[] = [
         summary: 'Replace the entire fallback list for a master inbound. Body is JSON. Triggers an Xray restart.',
         summary: 'Replace the entire fallback list for a master inbound. Body is JSON. Triggers an Xray restart.',
         params: [
         params: [
           { name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' },
           { name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' },
-          { name: 'fallbacks', in: 'body (json)', type: 'object[]', desc: 'Array of {childId, name, alpn, path, xver, sortOrder} entries.' },
+          { name: 'fallbacks', in: 'body (json)', type: 'object[]', desc: 'Array of {childId, name, alpn, path, dest, xver, sortOrder} entries. Leave dest empty to auto-resolve from the child inbound\'s listen+port; set it (e.g. "8443", "127.0.0.1:8443", "/dev/shm/x.sock") to override.' },
         ],
         ],
-        body: '{\n  "fallbacks": [\n    { "childId": 11, "path": "/vlws", "xver": 2 },\n    { "childId": 12, "alpn": "h2" }\n  ]\n}',
+        body: '{\n  "fallbacks": [\n    { "childId": 11, "path": "/vlws", "xver": 2 },\n    { "childId": 12, "alpn": "h2", "dest": "8443" }\n  ]\n}',
         response: '{\n  "success": true,\n  "msg": "Inbound updated"\n}',
         response: '{\n  "success": true,\n  "msg": "Inbound updated"\n}',
       },
       },
     ],
     ],

+ 21 - 4
frontend/src/pages/inbounds/InboundFormModal.tsx

@@ -365,13 +365,21 @@ export default function InboundFormModal({
       return;
       return;
     }
     }
     setFallbacks(
     setFallbacks(
-      (msg.obj as { childId: number; name?: string; alpn?: string; path?: string; xver?: number }[])
+      (msg.obj as {
+        childId: number;
+        name?: string;
+        alpn?: string;
+        path?: string;
+        dest?: string;
+        xver?: number;
+      }[])
         .map((r) => ({
         .map((r) => ({
           rowKey: `fb-${++fallbackKeyRef.current}`,
           rowKey: `fb-${++fallbackKeyRef.current}`,
           childId: r.childId,
           childId: r.childId,
           name: r.name || '',
           name: r.name || '',
           alpn: r.alpn || '',
           alpn: r.alpn || '',
           path: r.path || '',
           path: r.path || '',
+          dest: r.dest || '',
           xver: r.xver || 0,
           xver: r.xver || 0,
         })),
         })),
     );
     );
@@ -385,6 +393,7 @@ export default function InboundFormModal({
         name: c.name,
         name: c.name,
         alpn: c.alpn,
         alpn: c.alpn,
         path: c.path,
         path: c.path,
+        dest: c.dest,
         xver: Number(c.xver) || 0,
         xver: Number(c.xver) || 0,
         sortOrder: i,
         sortOrder: i,
       })),
       })),
@@ -437,6 +446,7 @@ export default function InboundFormModal({
       name: '',
       name: '',
       alpn: '',
       alpn: '',
       path: '',
       path: '',
+      dest: '',
       xver: 0,
       xver: 0,
     }]);
     }]);
   };
   };
@@ -445,11 +455,11 @@ export default function InboundFormModal({
     setFallbacks((prev) => prev.map((r) => {
     setFallbacks((prev) => prev.map((r) => {
       if (r.rowKey !== rowKey) return r;
       if (r.rowKey !== rowKey) return r;
       // When the picker selects a new child inbound and the row hasn't
       // When the picker selects a new child inbound and the row hasn't
-      // been hand-edited yet (sni/alpn/path all blank, xver = 0), pull
-      // the SNI/ALPN/Path defaults off that child. Operators who
+      // been hand-edited yet (sni/alpn/path/dest all blank, xver = 0),
+      // pull the SNI/ALPN/Path defaults off that child. Operators who
       // intentionally typed values keep them — we only fill the empties.
       // intentionally typed values keep them — we only fill the empties.
       if (typeof patch.childId === 'number' && patch.childId !== r.childId) {
       if (typeof patch.childId === 'number' && patch.childId !== r.childId) {
-        const isPristine = !r.name && !r.alpn && !r.path && r.xver === 0;
+        const isPristine = !r.name && !r.alpn && !r.path && !r.dest && r.xver === 0;
         if (isPristine) return { ...r, ...patch, ...deriveFallbackDefaults(patch.childId) };
         if (isPristine) return { ...r, ...patch, ...deriveFallbackDefaults(patch.childId) };
       }
       }
       return { ...r, ...patch };
       return { ...r, ...patch };
@@ -490,6 +500,7 @@ export default function InboundFormModal({
             name: derived.name ?? '',
             name: derived.name ?? '',
             alpn: derived.alpn ?? '',
             alpn: derived.alpn ?? '',
             path: derived.path ?? '',
             path: derived.path ?? '',
+            dest: '',
             xver: derived.xver ?? 0,
             xver: derived.xver ?? 0,
           };
           };
         });
         });
@@ -1079,6 +1090,12 @@ export default function InboundFormModal({
               value={record.path}
               value={record.path}
               onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
               onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
             />
             />
+            <InputAddon>Dest</InputAddon>
+            <Input
+              placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
+              value={record.dest}
+              onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
+            />
             <InputAddon>xver</InputAddon>
             <InputAddon>xver</InputAddon>
             <InputNumber
             <InputNumber
               min={0}
               min={0}

+ 1 - 0
frontend/src/schemas/forms/inbound-form.ts

@@ -78,6 +78,7 @@ export const FallbackRowSchema = z.object({
   name: z.string().default(''),
   name: z.string().default(''),
   alpn: z.string().default(''),
   alpn: z.string().default(''),
   path: z.string().default(''),
   path: z.string().default(''),
+  dest: z.string().default(''),
   xver: z.number().int().min(0).max(2).default(0),
   xver: z.number().int().min(0).max(2).default(0),
 });
 });
 export type FallbackRow = z.infer<typeof FallbackRowSchema>;
 export type FallbackRow = z.infer<typeof FallbackRowSchema>;

+ 24 - 24
web/entity/entity.go

@@ -21,34 +21,34 @@ type Msg struct {
 // AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.
 // AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.
 type AllSetting struct {
 type AllSetting struct {
 	// Web server settings
 	// Web server settings
-	WebListen         string `json:"webListen" form:"webListen"`                                       // Web server listen IP address
-	WebDomain         string `json:"webDomain" form:"webDomain"`                                       // Web server domain for domain validation
-	WebPort           int    `json:"webPort" form:"webPort" validate:"gte=1,lte=65535"`                // Web server port number
-	WebCertFile       string `json:"webCertFile" form:"webCertFile"`                                   // Path to SSL certificate file for web server
-	WebKeyFile        string `json:"webKeyFile" form:"webKeyFile"`                                     // Path to SSL private key file for web server
-	WebBasePath       string `json:"webBasePath" form:"webBasePath"`                                   // Base path for web panel URLs
-	SessionMaxAge     int    `json:"sessionMaxAge" form:"sessionMaxAge" validate:"gte=0,lte=525600"`   // Session maximum age in minutes (cap at one year)
-	TrustedProxyCIDRs string `json:"trustedProxyCIDRs" form:"trustedProxyCIDRs"`                       // Trusted reverse proxy IPs/CIDRs for forwarded headers
-	PanelProxy        string `json:"panelProxy" form:"panelProxy"`                                     // Proxy URL for the panel's own outbound requests (GitHub/Telegram)
+	WebListen         string `json:"webListen" form:"webListen"`                                     // Web server listen IP address
+	WebDomain         string `json:"webDomain" form:"webDomain"`                                     // Web server domain for domain validation
+	WebPort           int    `json:"webPort" form:"webPort" validate:"gte=1,lte=65535"`              // Web server port number
+	WebCertFile       string `json:"webCertFile" form:"webCertFile"`                                 // Path to SSL certificate file for web server
+	WebKeyFile        string `json:"webKeyFile" form:"webKeyFile"`                                   // Path to SSL private key file for web server
+	WebBasePath       string `json:"webBasePath" form:"webBasePath"`                                 // Base path for web panel URLs
+	SessionMaxAge     int    `json:"sessionMaxAge" form:"sessionMaxAge" validate:"gte=0,lte=525600"` // Session maximum age in minutes (cap at one year)
+	TrustedProxyCIDRs string `json:"trustedProxyCIDRs" form:"trustedProxyCIDRs"`                     // Trusted reverse proxy IPs/CIDRs for forwarded headers
+	PanelProxy        string `json:"panelProxy" form:"panelProxy"`                                   // Proxy URL for the panel's own outbound requests (GitHub/Telegram)
 
 
 	// UI settings
 	// UI settings
-	PageSize    int    `json:"pageSize" form:"pageSize" validate:"gte=1,lte=1000"`     // Number of items per page in lists
-	ExpireDiff  int    `json:"expireDiff" form:"expireDiff" validate:"gte=0"`          // Expiration warning threshold in days
-	TrafficDiff int    `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"`// Traffic warning threshold percentage
-	RemarkModel string `json:"remarkModel" form:"remarkModel"`                         // Remark model pattern for inbounds
-	Datepicker  string `json:"datepicker" form:"datepicker"`                           // Date picker format
+	PageSize    int    `json:"pageSize" form:"pageSize" validate:"gte=1,lte=1000"`      // Number of items per page in lists
+	ExpireDiff  int    `json:"expireDiff" form:"expireDiff" validate:"gte=0"`           // Expiration warning threshold in days
+	TrafficDiff int    `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"` // Traffic warning threshold percentage
+	RemarkModel string `json:"remarkModel" form:"remarkModel"`                          // Remark model pattern for inbounds
+	Datepicker  string `json:"datepicker" form:"datepicker"`                            // Date picker format
 
 
 	// Telegram bot settings
 	// Telegram bot settings
-	TgBotEnable      bool   `json:"tgBotEnable" form:"tgBotEnable"`           // Enable Telegram bot notifications
-	TgBotToken       string `json:"tgBotToken" form:"tgBotToken"`             // Telegram bot token
-	TgBotProxy       string `json:"tgBotProxy" form:"tgBotProxy"`             // Proxy URL for Telegram bot
-	TgBotAPIServer   string `json:"tgBotAPIServer" form:"tgBotAPIServer"`     // Custom API server for Telegram bot
-	TgBotChatId      string `json:"tgBotChatId" form:"tgBotChatId"`           // Telegram chat ID for notifications
-	TgRunTime        string `json:"tgRunTime" form:"tgRunTime"`               // Cron schedule for Telegram notifications
-	TgBotBackup      bool   `json:"tgBotBackup" form:"tgBotBackup"`           // Enable database backup via Telegram
-	TgBotLoginNotify bool   `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`             // Send login notifications
-	TgCpu            int    `json:"tgCpu" form:"tgCpu" validate:"gte=0,lte=100"`          // CPU usage threshold for alerts (percent)
-	TgLang           string `json:"tgLang" form:"tgLang"`                                 // Telegram bot language
+	TgBotEnable      bool   `json:"tgBotEnable" form:"tgBotEnable"`              // Enable Telegram bot notifications
+	TgBotToken       string `json:"tgBotToken" form:"tgBotToken"`                // Telegram bot token
+	TgBotProxy       string `json:"tgBotProxy" form:"tgBotProxy"`                // Proxy URL for Telegram bot
+	TgBotAPIServer   string `json:"tgBotAPIServer" form:"tgBotAPIServer"`        // Custom API server for Telegram bot
+	TgBotChatId      string `json:"tgBotChatId" form:"tgBotChatId"`              // Telegram chat ID for notifications
+	TgRunTime        string `json:"tgRunTime" form:"tgRunTime"`                  // Cron schedule for Telegram notifications
+	TgBotBackup      bool   `json:"tgBotBackup" form:"tgBotBackup"`              // Enable database backup via Telegram
+	TgBotLoginNotify bool   `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`    // Send login notifications
+	TgCpu            int    `json:"tgCpu" form:"tgCpu" validate:"gte=0,lte=100"` // CPU usage threshold for alerts (percent)
+	TgLang           string `json:"tgLang" form:"tgLang"`                        // Telegram bot language
 
 
 	// Security settings
 	// Security settings
 	TimeLocation    string `json:"timeLocation" form:"timeLocation"`       // Time zone location
 	TimeLocation    string `json:"timeLocation" form:"timeLocation"`       // Time zone location

+ 10 - 7
web/service/fallback.go

@@ -18,6 +18,7 @@ type FallbackInput struct {
 	Name      string `json:"name"`
 	Name      string `json:"name"`
 	Alpn      string `json:"alpn"`
 	Alpn      string `json:"alpn"`
 	Path      string `json:"path"`
 	Path      string `json:"path"`
+	Dest      string `json:"dest"`
 	Xver      int    `json:"xver"`
 	Xver      int    `json:"xver"`
 	SortOrder int    `json:"sortOrder"`
 	SortOrder int    `json:"sortOrder"`
 }
 }
@@ -71,6 +72,7 @@ func (s *FallbackService) SetByMaster(masterId int, items []FallbackInput) error
 				Name:      c.Name,
 				Name:      c.Name,
 				Alpn:      c.Alpn,
 				Alpn:      c.Alpn,
 				Path:      c.Path,
 				Path:      c.Path,
+				Dest:      c.Dest,
 				Xver:      c.Xver,
 				Xver:      c.Xver,
 				SortOrder: c.SortOrder,
 				SortOrder: c.SortOrder,
 			}
 			}
@@ -85,9 +87,6 @@ func (s *FallbackService) SetByMaster(masterId int, items []FallbackInput) error
 	})
 	})
 }
 }
 
 
-// BuildFallbacksJSON resolves the master's fallback rows into Xray's
-// expected settings.fallbacks shape, looking up each child's listen+port
-// to fill the dest field. Returns nil when the master has no rules.
 func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[string]any, error) {
 func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[string]any, error) {
 	if tx == nil {
 	if tx == nil {
 		tx = database.GetDB()
 		tx = database.GetDB()
@@ -122,12 +121,16 @@ func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[s
 		if !ok {
 		if !ok {
 			continue
 			continue
 		}
 		}
-		listen := strings.TrimSpace(child.Listen)
-		if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" {
-			listen = "127.0.0.1"
+		dest := r.Dest
+		if dest == "" {
+			listen := strings.TrimSpace(child.Listen)
+			if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" {
+				listen = "127.0.0.1"
+			}
+			dest = fmt.Sprintf("%s:%d", listen, child.Port)
 		}
 		}
 		entry := map[string]any{
 		entry := map[string]any{
-			"dest": fmt.Sprintf("%s:%d", listen, child.Port),
+			"dest": dest,
 		}
 		}
 		if r.Name != "" {
 		if r.Name != "" {
 			entry["name"] = r.Name
 			entry["name"] = r.Name

+ 0 - 1
web/service/xray.go

@@ -304,7 +304,6 @@ func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
 	return out
 	return out
 }
 }
 
 
-
 // GetXrayTraffic fetches the current traffic statistics from the running Xray process.
 // GetXrayTraffic fetches the current traffic statistics from the running Xray process.
 func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
 func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
 	if !s.IsXrayRunning() {
 	if !s.IsXrayRunning() {

+ 1 - 0
web/translation/ar-EG.json

@@ -266,6 +266,7 @@
         "add": "إضافة fallback",
         "add": "إضافة fallback",
         "pickInbound": "اختر inbound",
         "pickInbound": "اختر inbound",
         "matchAny": "أي",
         "matchAny": "أي",
+        "destPlaceholder": "تلقائي (listen:port للفرع)",
         "rederive": "إعادة الملء من الفرع",
         "rederive": "إعادة الملء من الفرع",
         "rederived": "تم إعادة الملء من الفرع",
         "rederived": "تم إعادة الملء من الفرع",
         "editAdvanced": "تحرير حقول التوجيه",
         "editAdvanced": "تحرير حقول التوجيه",

+ 1 - 0
web/translation/en-US.json

@@ -266,6 +266,7 @@
         "add": "Add fallback",
         "add": "Add fallback",
         "pickInbound": "Pick an inbound",
         "pickInbound": "Pick an inbound",
         "matchAny": "any",
         "matchAny": "any",
+        "destPlaceholder": "auto (child listen:port)",
         "rederive": "Re-fill from child",
         "rederive": "Re-fill from child",
         "rederived": "Re-filled from child",
         "rederived": "Re-filled from child",
         "editAdvanced": "Edit routing fields",
         "editAdvanced": "Edit routing fields",

+ 1 - 0
web/translation/es-ES.json

@@ -266,6 +266,7 @@
         "add": "Añadir fallback",
         "add": "Añadir fallback",
         "pickInbound": "Selecciona un inbound",
         "pickInbound": "Selecciona un inbound",
         "matchAny": "cualquiera",
         "matchAny": "cualquiera",
+        "destPlaceholder": "automático (listen:puerto del hijo)",
         "rederive": "Rellenar desde el hijo",
         "rederive": "Rellenar desde el hijo",
         "rederived": "Rellenado desde el hijo",
         "rederived": "Rellenado desde el hijo",
         "editAdvanced": "Editar campos de enrutamiento",
         "editAdvanced": "Editar campos de enrutamiento",

+ 1 - 0
web/translation/fa-IR.json

@@ -266,6 +266,7 @@
         "add": "افزودن فال‌بک",
         "add": "افزودن فال‌بک",
         "pickInbound": "یک اینباند انتخاب کنید",
         "pickInbound": "یک اینباند انتخاب کنید",
         "matchAny": "همه",
         "matchAny": "همه",
+        "destPlaceholder": "خودکار (listen:port فرزند)",
         "rederive": "پر کردن مجدد از فرزند",
         "rederive": "پر کردن مجدد از فرزند",
         "rederived": "از فرزند پر شد",
         "rederived": "از فرزند پر شد",
         "editAdvanced": "ویرایش فیلدهای مسیریابی",
         "editAdvanced": "ویرایش فیلدهای مسیریابی",

+ 1 - 0
web/translation/id-ID.json

@@ -266,6 +266,7 @@
         "add": "Tambah fallback",
         "add": "Tambah fallback",
         "pickInbound": "Pilih inbound",
         "pickInbound": "Pilih inbound",
         "matchAny": "apa pun",
         "matchAny": "apa pun",
+        "destPlaceholder": "otomatis (listen:port child)",
         "rederive": "Isi ulang dari child",
         "rederive": "Isi ulang dari child",
         "rederived": "Diisi ulang dari child",
         "rederived": "Diisi ulang dari child",
         "editAdvanced": "Edit field routing",
         "editAdvanced": "Edit field routing",

+ 1 - 0
web/translation/ja-JP.json

@@ -266,6 +266,7 @@
         "add": "フォールバックを追加",
         "add": "フォールバックを追加",
         "pickInbound": "インバウンドを選択",
         "pickInbound": "インバウンドを選択",
         "matchAny": "任意",
         "matchAny": "任意",
+        "destPlaceholder": "自動(子の listen:port)",
         "rederive": "子から再取得",
         "rederive": "子から再取得",
         "rederived": "子から再取得しました",
         "rederived": "子から再取得しました",
         "editAdvanced": "ルーティング項目を編集",
         "editAdvanced": "ルーティング項目を編集",

+ 1 - 0
web/translation/pt-BR.json

@@ -266,6 +266,7 @@
         "add": "Adicionar fallback",
         "add": "Adicionar fallback",
         "pickInbound": "Escolha um inbound",
         "pickInbound": "Escolha um inbound",
         "matchAny": "qualquer",
         "matchAny": "qualquer",
+        "destPlaceholder": "automático (listen:porta do filho)",
         "rederive": "Preencher a partir do filho",
         "rederive": "Preencher a partir do filho",
         "rederived": "Preenchido a partir do filho",
         "rederived": "Preenchido a partir do filho",
         "editAdvanced": "Editar campos de roteamento",
         "editAdvanced": "Editar campos de roteamento",

+ 1 - 0
web/translation/ru-RU.json

@@ -266,6 +266,7 @@
         "add": "Добавить фолбэк",
         "add": "Добавить фолбэк",
         "pickInbound": "Выберите инбаунд",
         "pickInbound": "Выберите инбаунд",
         "matchAny": "любой",
         "matchAny": "любой",
+        "destPlaceholder": "авто (listen:порт дочернего)",
         "rederive": "Заполнить из дочернего",
         "rederive": "Заполнить из дочернего",
         "rederived": "Заполнено из дочернего",
         "rederived": "Заполнено из дочернего",
         "editAdvanced": "Изменить поля маршрутизации",
         "editAdvanced": "Изменить поля маршрутизации",

+ 1 - 0
web/translation/tr-TR.json

@@ -266,6 +266,7 @@
         "add": "Fallback ekle",
         "add": "Fallback ekle",
         "pickInbound": "Bir inbound seç",
         "pickInbound": "Bir inbound seç",
         "matchAny": "herhangi",
         "matchAny": "herhangi",
+        "destPlaceholder": "otomatik (child listen:port)",
         "rederive": "Child'dan yeniden doldur",
         "rederive": "Child'dan yeniden doldur",
         "rederived": "Child'dan yeniden dolduruldu",
         "rederived": "Child'dan yeniden dolduruldu",
         "editAdvanced": "Yönlendirme alanlarını düzenle",
         "editAdvanced": "Yönlendirme alanlarını düzenle",

+ 1 - 0
web/translation/uk-UA.json

@@ -266,6 +266,7 @@
         "add": "Додати фолбек",
         "add": "Додати фолбек",
         "pickInbound": "Оберіть інбаунд",
         "pickInbound": "Оберіть інбаунд",
         "matchAny": "будь-який",
         "matchAny": "будь-який",
+        "destPlaceholder": "авто (listen:порт дочірнього)",
         "rederive": "Заповнити з дочірнього",
         "rederive": "Заповнити з дочірнього",
         "rederived": "Заповнено з дочірнього",
         "rederived": "Заповнено з дочірнього",
         "editAdvanced": "Редагувати поля маршрутизації",
         "editAdvanced": "Редагувати поля маршрутизації",

+ 1 - 0
web/translation/vi-VN.json

@@ -266,6 +266,7 @@
         "add": "Thêm fallback",
         "add": "Thêm fallback",
         "pickInbound": "Chọn một inbound",
         "pickInbound": "Chọn một inbound",
         "matchAny": "bất kỳ",
         "matchAny": "bất kỳ",
+        "destPlaceholder": "tự động (listen:port của child)",
         "rederive": "Điền lại từ child",
         "rederive": "Điền lại từ child",
         "rederived": "Đã điền lại từ child",
         "rederived": "Đã điền lại từ child",
         "editAdvanced": "Sửa trường định tuyến",
         "editAdvanced": "Sửa trường định tuyến",

+ 1 - 0
web/translation/zh-CN.json

@@ -266,6 +266,7 @@
         "add": "添加回落",
         "add": "添加回落",
         "pickInbound": "选择一个入站",
         "pickInbound": "选择一个入站",
         "matchAny": "任意",
         "matchAny": "任意",
+        "destPlaceholder": "自动(子入站 listen:port)",
         "rederive": "从子入站重新填充",
         "rederive": "从子入站重新填充",
         "rederived": "已从子入站重新填充",
         "rederived": "已从子入站重新填充",
         "editAdvanced": "编辑路由字段",
         "editAdvanced": "编辑路由字段",

+ 1 - 0
web/translation/zh-TW.json

@@ -266,6 +266,7 @@
         "add": "新增回落",
         "add": "新增回落",
         "pickInbound": "選擇一個入站",
         "pickInbound": "選擇一個入站",
         "matchAny": "任何",
         "matchAny": "任何",
+        "destPlaceholder": "自動(子入站 listen:port)",
         "rederive": "從子入站重新填入",
         "rederive": "從子入站重新填入",
         "rederived": "已從子入站重新填入",
         "rederived": "已從子入站重新填入",
         "editAdvanced": "編輯路由欄位",
         "editAdvanced": "編輯路由欄位",