Преглед изворни кода

Add override destination to route options

世界 пре 11 месеци
родитељ
комит
2dbb8c55c9

+ 4 - 3
adapter/inbound.go

@@ -61,9 +61,10 @@ type InboundContext struct {
 	// cache
 
 	// Deprecated: implement in rule action
-	InboundDetour     string
-	LastInbound       string
-	OriginDestination M.Socksaddr
+	InboundDetour            string
+	LastInbound              string
+	OriginDestination        M.Socksaddr
+	RouteOriginalDestination M.Socksaddr
 	// Deprecated
 	InboundOptions            option.InboundOptions
 	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 err error
 	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 {
 		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 {
 		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 {
 			outPacketConn, err = this.ListenPacket(ctx, metadata.Destination)
 		}
@@ -91,11 +83,17 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
 		return err
 	}
 	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 {
-				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 {
-				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 {

+ 32 - 12
common/dialer/default_parallel_network.go

@@ -13,17 +13,27 @@ import (
 	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 {
 		return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	}
 	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...)
 }
@@ -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 {
 		return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
 	}
 	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...)
 }

+ 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"
 
     :material-plus: [cache_capacity](#cache_capacity)
 
-!!! quote "Changes in sing-box 1.9.0"
-
-    :material-plus: [client_subnet](#client_subnet)
-
 # DNS
 
 ### Structure
@@ -70,7 +65,7 @@ Make each DNS server's cache independent for special purposes. If enabled, will
 
 #### cache_capacity
 
-!!! quote "Since sing-box 1.11.0"
+!!! question "Since sing-box 1.11.0"
 
 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)
 
-!!! quote "自 sing-box 1.9.0 起"
-
-    :material-plus: [client_subnet](#client_subnet)
-
 # DNS
 
 ### 结构
@@ -68,7 +64,7 @@ icon: material/new
 
 #### cache_capacity
 
-!!! quote "自 sing-box 1.11.0 起"
+!!! question "自 sing-box 1.11.0 起"
 
 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.
 
 ### Structure
@@ -9,7 +18,6 @@
   
   "override_address": "1.0.0.1",
   "override_port": 53,
-  "proxy_protocol": 0,
   
   ... // Dial Fields
 }
@@ -19,15 +27,19 @@
 
 #### 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_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`.
 

+ 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` 出站直接发送请求。
 
 ### 结构
@@ -9,7 +18,6 @@
   
   "override_address": "1.0.0.1",
   "override_port": 53,
-  "proxy_protocol": 0,
 
   ... // 拨号字段
 }
@@ -19,17 +27,19 @@
 
 #### 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
 
-覆盖连接目标端口。
-
-#### 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
   "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.
 
+#### 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
 
 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.
 
-### 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
 
 ```json

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

@@ -10,12 +10,8 @@ icon: material/new-box
 {
   "action": "route", // 默认
   "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
 
 详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
@@ -56,24 +84,6 @@ icon: material/new-box
 
 如果启用,将尝试将 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
 
 ```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.
 
+#### 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
 
 #### TUN address fields are merged

+ 7 - 0
docs/deprecated.zh.md

@@ -20,6 +20,13 @@ icon: material/delete-alert
 
 旧字段将在 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
 
 #### 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
 
 ### TUN address fields are merged

+ 40 - 2
docs/migration.zh.md

@@ -104,6 +104,7 @@ icon: material/arrange-bring-forward
 
 ### 迁移旧的入站字段到规则动作
 
+
 入站选项已被弃用,且可以被规则动作替代。
 
 !!! 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
 
 ### TUN 地址字段已合并
@@ -164,8 +204,6 @@ icon: material/arrange-bring-forward
 `inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`,
 `inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。
 
-旧字段已废弃,且将在 sing-box 1.11.0 中移除。
-
 !!! info "参考"
 
     [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",
 }
 
+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{
 	OptionBadMatchSource,
 	OptionGEOIP,
@@ -111,4 +120,5 @@ var Options = []Note{
 	OptionTUNAddressX,
 	OptionSpecialOutbounds,
 	OptionInboundOptions,
+	OptionDestinationOverrideFields,
 }

+ 26 - 3
option/direct.go

@@ -1,5 +1,12 @@
 package option
 
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/experimental/deprecated"
+	"github.com/sagernet/sing/common/json"
+)
+
 type DirectInboundOptions struct {
 	ListenOptions
 	Network         NetworkList `json:"network,omitempty"`
@@ -7,9 +14,25 @@ type DirectInboundOptions struct {
 	OverridePort    uint16      `json:"override_port,omitempty"`
 }
 
-type DirectOutboundOptions struct {
+type _DirectOutboundOptions struct {
 	DialerOptions
+	// Deprecated: Use Route Action instead
 	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 {
-	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 {
-	err := json.Unmarshal(data, (*_RouteOptionsActionOptions)(r))
+	err := json.Unmarshal(data, (*RawRouteOptionsActionOptions)(r))
 	if err != nil {
 		return err
 	}

+ 29 - 8
route/route.go

@@ -422,17 +422,38 @@ match:
 				}
 			}
 		}
+		var routeOptions *rule.RuleActionRouteOptions
 		switch action := currentRule.Action().(type) {
 		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:
-			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:
 			if !preMatch {
 				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"
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/logger"
+	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -30,6 +31,8 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
 		return &RuleActionRoute{
 			Outbound: action.RouteOptions.Outbound,
 			RuleActionRouteOptions: RuleActionRouteOptions{
+				OverrideAddress:           M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0),
+				OverridePort:              action.RouteOptions.OverridePort,
 				NetworkStrategy:           C.NetworkStrategy(action.RouteOptions.NetworkStrategy),
 				FallbackDelay:             time.Duration(action.RouteOptions.FallbackDelay),
 				UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping,
@@ -38,6 +41,8 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
 		}, nil
 	case C.RuleActionTypeRouteOptions:
 		return &RuleActionRouteOptions{
+			OverrideAddress:           M.ParseSocksaddrHostPort(action.RouteOptionsOptions.OverrideAddress, 0),
+			OverridePort:              action.RouteOptionsOptions.OverridePort,
 			NetworkStrategy:           C.NetworkStrategy(action.RouteOptionsOptions.NetworkStrategy),
 			FallbackDelay:             time.Duration(action.RouteOptionsOptions.FallbackDelay),
 			UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping,
@@ -139,6 +144,8 @@ func (r *RuleActionRoute) String() string {
 }
 
 type RuleActionRouteOptions struct {
+	OverrideAddress           M.Socksaddr
+	OverridePort              uint16
 	NetworkStrategy           C.NetworkStrategy
 	NetworkType               []C.InterfaceType
 	FallbackNetworkType       []C.InterfaceType