浏览代码

Add override destination to route options

世界 11 月之前
父节点
当前提交
2dbb8c55c9

+ 4 - 3
adapter/inbound.go

@@ -61,9 +61,10 @@ type InboundContext struct {
 	// cache
 	// cache
 
 
 	// Deprecated: implement in rule action
 	// Deprecated: implement in rule action
-	InboundDetour     string
-	LastInbound       string
-	OriginDestination M.Socksaddr
+	InboundDetour            string
+	LastInbound              string
+	OriginDestination        M.Socksaddr
+	RouteOriginalDestination M.Socksaddr
 	// Deprecated
 	// Deprecated
 	InboundOptions            option.InboundOptions
 	InboundOptions            option.InboundOptions
 	UDPDisableDomainUnmapping bool
 	UDPDisableDomainUnmapping bool

+ 11 - 13
adapter/outbound/default.go

@@ -25,11 +25,7 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a
 	var outConn net.Conn
 	var outConn net.Conn
 	var err error
 	var err error
 	if len(metadata.DestinationAddresses) > 0 {
 	if len(metadata.DestinationAddresses) > 0 {
-		if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer {
-			outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
-		} else {
-			outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses)
-		}
+		outConn, err = dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
 	} else {
 	} else {
 		outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
 		outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
 	}
 	}
@@ -73,11 +69,7 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
 		}
 		}
 	} else {
 	} else {
 		if len(metadata.DestinationAddresses) > 0 {
 		if len(metadata.DestinationAddresses) > 0 {
-			if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer {
-				outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, parallelDialer, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
-			} else {
-				outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses)
-			}
+			outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, this, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
 		} else {
 		} else {
 			outPacketConn, err = this.ListenPacket(ctx, metadata.Destination)
 			outPacketConn, err = this.ListenPacket(ctx, metadata.Destination)
 		}
 		}
@@ -91,11 +83,17 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
 		return err
 		return err
 	}
 	}
 	if destinationAddress.IsValid() {
 	if destinationAddress.IsValid() {
-		if metadata.Destination.IsFqdn() {
+		var originDestination M.Socksaddr
+		if metadata.RouteOriginalDestination.IsValid() {
+			originDestination = metadata.RouteOriginalDestination
+		} else {
+			originDestination = metadata.Destination
+		}
+		if metadata.Destination != M.SocksaddrFrom(destinationAddress, metadata.Destination.Port) {
 			if metadata.UDPDisableDomainUnmapping {
 			if metadata.UDPDisableDomainUnmapping {
-				outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
+				outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination)
 			} else {
 			} else {
-				outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
+				outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination)
 			}
 			}
 		}
 		}
 		if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {
 		if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {

+ 32 - 12
common/dialer/default_parallel_network.go

@@ -13,17 +13,27 @@ import (
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 )
 )
 
 
-func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
+func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
 	if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
 	if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
 		return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 		return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	}
 	}
 	var errors []error
 	var errors []error
-	for _, address := range destinationAddresses {
-		conn, err := dialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
-		if err == nil {
-			return conn, nil
+	if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
+		for _, address := range destinationAddresses {
+			conn, err := parallelDialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
+			if err == nil {
+				return conn, nil
+			}
+			errors = append(errors, err)
+		}
+	} else {
+		for _, address := range destinationAddresses {
+			conn, err := dialer.DialContext(ctx, network, M.SocksaddrFrom(address, destination.Port))
+			if err == nil {
+				return conn, nil
+			}
+			errors = append(errors, err)
 		}
 		}
-		errors = append(errors, err)
 	}
 	}
 	return nil, E.Errors(errors...)
 	return nil, E.Errors(errors...)
 }
 }
@@ -106,17 +116,27 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne
 	}
 	}
 }
 }
 
 
-func ListenSerialNetworkPacket(ctx context.Context, dialer ParallelInterfaceDialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
+func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
 	if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
 	if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
 		return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 		return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	}
 	}
 	var errors []error
 	var errors []error
-	for _, address := range destinationAddresses {
-		conn, err := dialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
-		if err == nil {
-			return conn, address, nil
+	if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
+		for _, address := range destinationAddresses {
+			conn, err := parallelDialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
+			if err == nil {
+				return conn, address, nil
+			}
+			errors = append(errors, err)
+		}
+	} else {
+		for _, address := range destinationAddresses {
+			conn, err := dialer.ListenPacket(ctx, M.SocksaddrFrom(address, destination.Port))
+			if err == nil {
+				return conn, address, nil
+			}
+			errors = append(errors, err)
 		}
 		}
-		errors = append(errors, err)
 	}
 	}
 	return nil, netip.Addr{}, E.Errors(errors...)
 	return nil, netip.Addr{}, E.Errors(errors...)
 }
 }

+ 2 - 7
docs/configuration/dns/index.md

@@ -1,16 +1,11 @@
 ---
 ---
-icon: material/new
+icon: material/new-box
 ---
 ---
 
 
-
 !!! quote "Changes in sing-box 1.11.0"
 !!! quote "Changes in sing-box 1.11.0"
 
 
     :material-plus: [cache_capacity](#cache_capacity)
     :material-plus: [cache_capacity](#cache_capacity)
 
 
-!!! quote "Changes in sing-box 1.9.0"
-
-    :material-plus: [client_subnet](#client_subnet)
-
 # DNS
 # DNS
 
 
 ### Structure
 ### Structure
@@ -70,7 +65,7 @@ Make each DNS server's cache independent for special purposes. If enabled, will
 
 
 #### cache_capacity
 #### cache_capacity
 
 
-!!! quote "Since sing-box 1.11.0"
+!!! question "Since sing-box 1.11.0"
 
 
 LRU cache capacity.
 LRU cache capacity.
 
 

+ 3 - 7
docs/configuration/dns/index.zh.md

@@ -1,15 +1,11 @@
 ---
 ---
-icon: material/new
+icon: material/new-box
 ---
 ---
 
 
-!!! quote "自 sing-box 1.11.0 起"
+!!! quote "sing-box 1.11.0 中的更改"
 
 
     :material-plus: [cache_capacity](#cache_capacity)
     :material-plus: [cache_capacity](#cache_capacity)
 
 
-!!! quote "自 sing-box 1.9.0 起"
-
-    :material-plus: [client_subnet](#client_subnet)
-
 # DNS
 # DNS
 
 
 ### 结构
 ### 结构
@@ -68,7 +64,7 @@ icon: material/new
 
 
 #### cache_capacity
 #### cache_capacity
 
 
-!!! quote "自 sing-box 1.11.0 起"
+!!! question "自 sing-box 1.11.0 起"
 
 
 LRU 缓存容量。
 LRU 缓存容量。
 
 

+ 16 - 4
docs/configuration/outbound/direct.md

@@ -1,3 +1,12 @@
+---
+icon: material/alert-decagram
+---
+
+!!! quote "Changes in sing-box 1.11.0"
+
+    :material-alert-decagram: [override_address](#override_address)  
+    :material-alert-decagram: [override_port](#override_port)
+
 `direct` outbound send requests directly.
 `direct` outbound send requests directly.
 
 
 ### Structure
 ### Structure
@@ -9,7 +18,6 @@
   
   
   "override_address": "1.0.0.1",
   "override_address": "1.0.0.1",
   "override_port": 53,
   "override_port": 53,
-  "proxy_protocol": 0,
   
   
   ... // Dial Fields
   ... // Dial Fields
 }
 }
@@ -19,15 +27,19 @@
 
 
 #### override_address
 #### override_address
 
 
+!!! failure "Deprecated in sing-box 1.11.0"
+
+    Destination override fields are deprecated in sing-box 1.11.0 and will be removed in sing-box 1.13.0, see [Migration](/migration/#migrate-destination-override-fields-to-route-options).
+
 Override the connection destination address.
 Override the connection destination address.
 
 
 #### override_port
 #### override_port
 
 
-Override the connection destination port.
+!!! failure "Deprecated in sing-box 1.11.0"
 
 
-#### proxy_protocol
+    Destination override fields are deprecated in sing-box 1.11.0 and will be removed in sing-box 1.13.0, see [Migration](/migration/#migrate-destination-override-fields-to-route-options).
 
 
-Write [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) in the connection header.
+Override the connection destination port.
 
 
 Protocol value can be `1` or `2`.
 Protocol value can be `1` or `2`.
 
 

+ 16 - 6
docs/configuration/outbound/direct.zh.md

@@ -1,3 +1,12 @@
+---
+icon: material/alert-decagram
+---
+
+!!! quote "sing-box 1.11.0 中的更改"
+
+    :material-alert-decagram: [override_address](#override_address)  
+    :material-alert-decagram: [override_port](#override_port)
+
 `direct` 出站直接发送请求。
 `direct` 出站直接发送请求。
 
 
 ### 结构
 ### 结构
@@ -9,7 +18,6 @@
   
   
   "override_address": "1.0.0.1",
   "override_address": "1.0.0.1",
   "override_port": 53,
   "override_port": 53,
-  "proxy_protocol": 0,
 
 
   ... // 拨号字段
   ... // 拨号字段
 }
 }
@@ -19,17 +27,19 @@
 
 
 #### override_address
 #### override_address
 
 
+!!! failure "已在 sing-box 1.11.0 废弃"
+
+    目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
+
 覆盖连接目标地址。
 覆盖连接目标地址。
 
 
 #### override_port
 #### override_port
 
 
-覆盖连接目标端口。
-
-#### proxy_protocol
+!!! failure "已在 sing-box 1.11.0 废弃"
 
 
-写出 [代理协议](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) 到连接头
+    目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
 
 
-可用协议版本值:`1` 或 `2`
+覆盖连接目标端口
 
 
 ### 拨号字段
 ### 拨号字段
 
 

+ 30 - 20
docs/configuration/route/rule_action.md

@@ -10,12 +10,8 @@ icon: material/new-box
 {
 {
   "action": "route", // default
   "action": "route", // default
   "outbound": "",
   "outbound": "",
-  "network_strategy": "",
-  "network_type": [],
-  "fallback_network_type": [],
-  "fallback_delay": "",
-  "udp_disable_domain_unmapping": false,
-  "udp_connect": false
+ 
+  ... // route-options Fields
 }
 }
 ```
 ```
 
 
@@ -31,6 +27,34 @@ icon: material/new-box
 
 
 Tag of target outbound.
 Tag of target outbound.
 
 
+#### route-options Fields
+
+See `route-options` fields below.
+
+### route-options
+
+```json
+{
+  "action": "route-options",
+  "override_address": "",
+  "override_port": 0,
+  "network_strategy": "",
+  "fallback_delay": "",
+  "udp_disable_domain_unmapping": false,
+  "udp_connect": false
+}
+```
+
+`route-options` set options for routing.
+
+#### override_address
+
+Override the connection destination address.
+
+#### override_port
+
+Override the connection destination port.
+
 #### network_strategy
 #### network_strategy
 
 
 See [Dial Fields](/configuration/shared/dial/#network_strategy) for details.
 See [Dial Fields](/configuration/shared/dial/#network_strategy) for details.
@@ -62,20 +86,6 @@ do not support receiving UDP packets with domain addresses, such as Surge.
 
 
 If enabled, attempts to connect UDP connection to the destination instead of listen.
 If enabled, attempts to connect UDP connection to the destination instead of listen.
 
 
-### route-options
-
-```json
-{
-  "action": "route-options",
-  "network_strategy": "",
-  "fallback_delay": "",
-  "udp_disable_domain_unmapping": false,
-  "udp_connect": false
-}
-```
-
-`route-options` set options for routing.
-
 ### reject
 ### reject
 
 
 ```json
 ```json

+ 34 - 24
docs/configuration/route/rule_action.zh.md

@@ -10,12 +10,8 @@ icon: material/new-box
 {
 {
   "action": "route", // 默认
   "action": "route", // 默认
   "outbound": "",
   "outbound": "",
-  "network_strategy": "",
-  "fallback_delay": "",
-  "network_type": [],
-  "fallback_network_type": [],
-  "udp_disable_domain_unmapping": false,
-  "udp_connect": false
+  
+  ... // route-options 字段
 }
 }
 ```
 ```
 
 
@@ -27,6 +23,38 @@ icon: material/new-box
 
 
 目标出站的标签。
 目标出站的标签。
 
 
+#### route-options 字段
+
+参阅下方的 `route-options` 字段。
+
+### route-options
+
+```json
+{
+  "action": "route-options",
+  "override_address": "",
+  "override_port": 0,
+  "network_strategy": "",
+  "fallback_delay": "",
+  "udp_disable_domain_unmapping": false,
+  "udp_connect": false
+}
+```
+
+!!! note ""
+
+    当内容只有一项时,可以忽略 JSON 数组 [] 标签
+
+`route-options` 为路由设置选项。
+
+#### override_address
+
+覆盖目标地址。
+
+#### override_port
+
+覆盖目标端口。
+
 #### network_strategy
 #### network_strategy
 
 
 详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
 详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
@@ -56,24 +84,6 @@ icon: material/new-box
 
 
 如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。
 如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。
 
 
-### route-options
-
-```json
-{
-  "action": "route-options",
-  "network_strategy": "",
-  "fallback_delay": "",
-  "udp_disable_domain_unmapping": false,
-  "udp_connect": false
-}
-```
-
-!!! note ""
-
-    当内容只有一项时,可以忽略 JSON 数组 [] 标签
-
-`route-options` 为路由设置选项。
-
 ### reject
 ### reject
 
 
 ```json
 ```json

+ 6 - 0
docs/deprecated.md

@@ -22,6 +22,12 @@ check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions).
 
 
 Old fields will be removed in sing-box 1.13.0.
 Old fields will be removed in sing-box 1.13.0.
 
 
+#### Destination override fields in direct outbound
+
+Destination override fields (`override_address` / `override_port`) in direct outbound are deprecated
+and can be replaced by rule actions,
+check [Migration](../migration/#migrate-destination-override-fields-to-route-options).
+
 ## 1.10.0
 ## 1.10.0
 
 
 #### TUN address fields are merged
 #### TUN address fields are merged

+ 7 - 0
docs/deprecated.zh.md

@@ -20,6 +20,13 @@ icon: material/delete-alert
 
 
 旧字段将在 sing-box 1.13.0 中被移除。
 旧字段将在 sing-box 1.13.0 中被移除。
 
 
+####  direct 出站中的目标地址覆盖字段
+
+direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代,
+参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
+
+旧字段将在 sing-box 1.13.0 中被移除。
+
 ## 1.10.0
 ## 1.10.0
 
 
 #### Match source 规则项已重命名
 #### Match source 规则项已重命名

+ 38 - 0
docs/migration.md

@@ -156,6 +156,44 @@ Inbound fields are deprecated and can be replaced by rule actions.
     }
     }
     ```
     ```
 
 
+### Migrate destination override fields to route options
+
+Destination override fields in direct outbound are deprecated and can be replaced by route options.
+
+!!! info "References"
+
+    [Rule Action](/configuration/route/rule_action/) /
+    [Direct](/configuration/outbound/direct/)
+
+=== ":material-card-remove: Deprecated"
+
+    ```json
+    {
+      "outbounds": [
+        {
+          "type": "direct",
+          "override_address": "1.1.1.1",
+          "override_port": 443
+        }
+      ]
+    }
+    ```
+
+=== ":material-card-multiple: New"
+
+    ```json
+    {
+      "route": {
+        "rules": [
+          {
+            "action": "route-options", // or route
+            "override_address": "1.1.1.1",
+            "override_port": 443
+          }
+        ]
+      }
+    ```
+
 ## 1.10.0
 ## 1.10.0
 
 
 ### TUN address fields are merged
 ### TUN address fields are merged

+ 40 - 2
docs/migration.zh.md

@@ -104,6 +104,7 @@ icon: material/arrange-bring-forward
 
 
 ### 迁移旧的入站字段到规则动作
 ### 迁移旧的入站字段到规则动作
 
 
+
 入站选项已被弃用,且可以被规则动作替代。
 入站选项已被弃用,且可以被规则动作替代。
 
 
 !!! info "参考"
 !!! info "参考"
@@ -156,6 +157,45 @@ icon: material/arrange-bring-forward
     }
     }
     ```
     ```
 
 
+### 迁移 direct 出站中的目标地址覆盖字段到路由字段
+
+direct 出站中的目标地址覆盖字段已废弃,且可以被路由字段替代。
+
+!!! info "参考"
+
+    [Rule Action](/zh/configuration/route/rule_action/) /
+    [Direct](/zh/configuration/outbound/direct/)
+
+=== ":material-card-remove: 弃用的"
+
+    ```json
+    {
+      "outbounds": [
+        {
+          "type": "direct",
+          "override_address": "1.1.1.1",
+          "override_port": 443
+        }
+      ]
+    }
+    ```
+
+=== ":material-card-multiple: 新的"
+
+    ```json
+    {
+      "route": {
+        "rules": [
+          {
+            "action": "route-options", // 或 route
+            "override_address": "1.1.1.1",
+            "override_port": 443
+          }
+        ]
+      }
+    }
+    ```
+
 ## 1.10.0
 ## 1.10.0
 
 
 ### TUN 地址字段已合并
 ### TUN 地址字段已合并
@@ -164,8 +204,6 @@ icon: material/arrange-bring-forward
 `inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`,
 `inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`,
 `inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。
 `inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。
 
 
-旧字段已废弃,且将在 sing-box 1.11.0 中移除。
-
 !!! info "参考"
 !!! info "参考"
 
 
     [TUN](/zh/configuration/inbound/tun/)
     [TUN](/zh/configuration/inbound/tun/)

+ 10 - 0
experimental/deprecated/constants.go

@@ -104,6 +104,15 @@ var OptionInboundOptions = Note{
 	MigrationLink:     "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions",
 	MigrationLink:     "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions",
 }
 }
 
 
+var OptionDestinationOverrideFields = Note{
+	Name:              "destination-override-fields",
+	Description:       "destination override fields in direct outbound",
+	DeprecatedVersion: "1.11.0",
+	ScheduledVersion:  "1.13.0",
+	EnvName:           "DESTINATION_OVERRIDE_FIELDS",
+	MigrationLink:     "https://sing-box.sagernet.org/migration/#migrate-destination-override-fields-to-route-options",
+}
+
 var Options = []Note{
 var Options = []Note{
 	OptionBadMatchSource,
 	OptionBadMatchSource,
 	OptionGEOIP,
 	OptionGEOIP,
@@ -111,4 +120,5 @@ var Options = []Note{
 	OptionTUNAddressX,
 	OptionTUNAddressX,
 	OptionSpecialOutbounds,
 	OptionSpecialOutbounds,
 	OptionInboundOptions,
 	OptionInboundOptions,
+	OptionDestinationOverrideFields,
 }
 }

+ 26 - 3
option/direct.go

@@ -1,5 +1,12 @@
 package option
 package option
 
 
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/experimental/deprecated"
+	"github.com/sagernet/sing/common/json"
+)
+
 type DirectInboundOptions struct {
 type DirectInboundOptions struct {
 	ListenOptions
 	ListenOptions
 	Network         NetworkList `json:"network,omitempty"`
 	Network         NetworkList `json:"network,omitempty"`
@@ -7,9 +14,25 @@ type DirectInboundOptions struct {
 	OverridePort    uint16      `json:"override_port,omitempty"`
 	OverridePort    uint16      `json:"override_port,omitempty"`
 }
 }
 
 
-type DirectOutboundOptions struct {
+type _DirectOutboundOptions struct {
 	DialerOptions
 	DialerOptions
+	// Deprecated: Use Route Action instead
 	OverrideAddress string `json:"override_address,omitempty"`
 	OverrideAddress string `json:"override_address,omitempty"`
-	OverridePort    uint16 `json:"override_port,omitempty"`
-	ProxyProtocol   uint8  `json:"proxy_protocol,omitempty"`
+	// Deprecated: Use Route Action instead
+	OverridePort uint16 `json:"override_port,omitempty"`
+	// Deprecated: removed
+	ProxyProtocol uint8 `json:"proxy_protocol,omitempty"`
+}
+
+type DirectOutboundOptions _DirectOutboundOptions
+
+func (d *DirectOutboundOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
+	err := json.UnmarshalDisallowUnknownFields(content, (*_DirectOutboundOptions)(d))
+	if err != nil {
+		return err
+	}
+	if d.OverrideAddress != "" || d.OverridePort != 0 {
+		deprecated.Report(ctx, deprecated.OptionDestinationOverrideFields)
+	}
+	return nil
 }
 }

+ 13 - 12
option/rule_action.go

@@ -137,24 +137,25 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e
 }
 }
 
 
 type RouteActionOptions struct {
 type RouteActionOptions struct {
-	Outbound                  string          `json:"outbound,omitempty"`
-	NetworkStrategy           NetworkStrategy `json:"network_strategy,omitempty"`
-	FallbackDelay             uint32          `json:"fallback_delay,omitempty"`
-	UDPDisableDomainUnmapping bool            `json:"udp_disable_domain_unmapping,omitempty"`
-	UDPConnect                bool            `json:"udp_connect,omitempty"`
+	Outbound string `json:"outbound,omitempty"`
+	RawRouteOptionsActionOptions
 }
 }
 
 
-type _RouteOptionsActionOptions struct {
-	NetworkStrategy           NetworkStrategy `json:"network_strategy,omitempty"`
-	FallbackDelay             uint32          `json:"fallback_delay,omitempty"`
-	UDPDisableDomainUnmapping bool            `json:"udp_disable_domain_unmapping,omitempty"`
-	UDPConnect                bool            `json:"udp_connect,omitempty"`
+type RawRouteOptionsActionOptions struct {
+	OverrideAddress string `json:"override_address,omitempty"`
+	OverridePort    uint16 `json:"override_port,omitempty"`
+
+	NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"`
+	FallbackDelay   uint32          `json:"fallback_delay,omitempty"`
+
+	UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"`
+	UDPConnect                bool `json:"udp_connect,omitempty"`
 }
 }
 
 
-type RouteOptionsActionOptions _RouteOptionsActionOptions
+type RouteOptionsActionOptions RawRouteOptionsActionOptions
 
 
 func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
 func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
-	err := json.Unmarshal(data, (*_RouteOptionsActionOptions)(r))
+	err := json.Unmarshal(data, (*RawRouteOptionsActionOptions)(r))
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 29 - 8
route/route.go

@@ -422,17 +422,38 @@ match:
 				}
 				}
 			}
 			}
 		}
 		}
+		var routeOptions *rule.RuleActionRouteOptions
 		switch action := currentRule.Action().(type) {
 		switch action := currentRule.Action().(type) {
 		case *rule.RuleActionRoute:
 		case *rule.RuleActionRoute:
-			metadata.NetworkStrategy = action.NetworkStrategy
-			metadata.FallbackDelay = action.FallbackDelay
-			metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping
-			metadata.UDPConnect = action.UDPConnect
+			routeOptions = &action.RuleActionRouteOptions
 		case *rule.RuleActionRouteOptions:
 		case *rule.RuleActionRouteOptions:
-			metadata.NetworkStrategy = action.NetworkStrategy
-			metadata.FallbackDelay = action.FallbackDelay
-			metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping
-			metadata.UDPConnect = action.UDPConnect
+			routeOptions = action
+		}
+		if routeOptions != nil {
+			// TODO: add nat
+			if (routeOptions.OverrideAddress.IsValid() || routeOptions.OverridePort > 0) && !metadata.RouteOriginalDestination.IsValid() {
+				metadata.RouteOriginalDestination = metadata.Destination
+			}
+			if routeOptions.OverrideAddress.IsValid() {
+				metadata.Destination = M.Socksaddr{
+					Addr: routeOptions.OverrideAddress.Addr,
+					Port: metadata.Destination.Port,
+					Fqdn: routeOptions.OverrideAddress.Fqdn,
+				}
+			}
+			if routeOptions.OverridePort > 0 {
+				metadata.Destination = M.Socksaddr{
+					Addr: metadata.Destination.Addr,
+					Port: routeOptions.OverridePort,
+					Fqdn: metadata.Destination.Fqdn,
+				}
+			}
+			metadata.NetworkStrategy = routeOptions.NetworkStrategy
+			metadata.FallbackDelay = routeOptions.FallbackDelay
+			metadata.UDPDisableDomainUnmapping = routeOptions.UDPDisableDomainUnmapping
+			metadata.UDPConnect = routeOptions.UDPConnect
+		}
+		switch action := currentRule.Action().(type) {
 		case *rule.RuleActionSniff:
 		case *rule.RuleActionSniff:
 			if !preMatch {
 			if !preMatch {
 				newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn)
 				newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn)

+ 7 - 0
route/rule/rule_action.go

@@ -19,6 +19,7 @@ import (
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/logger"
 	"github.com/sagernet/sing/common/logger"
+	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 )
 )
 
 
@@ -30,6 +31,8 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
 		return &RuleActionRoute{
 		return &RuleActionRoute{
 			Outbound: action.RouteOptions.Outbound,
 			Outbound: action.RouteOptions.Outbound,
 			RuleActionRouteOptions: RuleActionRouteOptions{
 			RuleActionRouteOptions: RuleActionRouteOptions{
+				OverrideAddress:           M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0),
+				OverridePort:              action.RouteOptions.OverridePort,
 				NetworkStrategy:           C.NetworkStrategy(action.RouteOptions.NetworkStrategy),
 				NetworkStrategy:           C.NetworkStrategy(action.RouteOptions.NetworkStrategy),
 				FallbackDelay:             time.Duration(action.RouteOptions.FallbackDelay),
 				FallbackDelay:             time.Duration(action.RouteOptions.FallbackDelay),
 				UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping,
 				UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping,
@@ -38,6 +41,8 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
 		}, nil
 		}, nil
 	case C.RuleActionTypeRouteOptions:
 	case C.RuleActionTypeRouteOptions:
 		return &RuleActionRouteOptions{
 		return &RuleActionRouteOptions{
+			OverrideAddress:           M.ParseSocksaddrHostPort(action.RouteOptionsOptions.OverrideAddress, 0),
+			OverridePort:              action.RouteOptionsOptions.OverridePort,
 			NetworkStrategy:           C.NetworkStrategy(action.RouteOptionsOptions.NetworkStrategy),
 			NetworkStrategy:           C.NetworkStrategy(action.RouteOptionsOptions.NetworkStrategy),
 			FallbackDelay:             time.Duration(action.RouteOptionsOptions.FallbackDelay),
 			FallbackDelay:             time.Duration(action.RouteOptionsOptions.FallbackDelay),
 			UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping,
 			UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping,
@@ -139,6 +144,8 @@ func (r *RuleActionRoute) String() string {
 }
 }
 
 
 type RuleActionRouteOptions struct {
 type RuleActionRouteOptions struct {
+	OverrideAddress           M.Socksaddr
+	OverridePort              uint16
 	NetworkStrategy           C.NetworkStrategy
 	NetworkStrategy           C.NetworkStrategy
 	NetworkType               []C.InterfaceType
 	NetworkType               []C.InterfaceType
 	FallbackNetworkType       []C.InterfaceType
 	FallbackNetworkType       []C.InterfaceType