Browse Source

Add proxy support for ICMP echo request

世界 8 months ago
parent
commit
f875c8bc2e

+ 8 - 0
adapter/outbound.go

@@ -3,9 +3,11 @@ package adapter
 import (
 	"context"
 	"net/netip"
+	"time"
 
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-tun"
 	N "github.com/sagernet/sing/common/network"
 )
 
@@ -20,10 +22,16 @@ type Outbound interface {
 }
 
 type OutboundWithPreferredRoutes interface {
+	Outbound
 	PreferredDomain(domain string) bool
 	PreferredAddress(address netip.Addr) bool
 }
 
+type DirectRouteOutbound interface {
+	Outbound
+	NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
+}
+
 type OutboundRegistry interface {
 	option.OutboundOptionsRegistry
 	CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)

+ 3 - 1
adapter/router.go

@@ -6,8 +6,10 @@ import (
 	"net"
 	"net/http"
 	"sync"
+	"time"
 
 	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-tun"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/ntp"
@@ -19,7 +21,7 @@ import (
 type Router interface {
 	Lifecycle
 	ConnectionRouter
-	PreMatch(metadata InboundContext) error
+	PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
 	ConnectionRouterEx
 	RuleSet(tag string) (RuleSet, bool)
 	NeedWIFIState() bool

+ 8 - 0
common/dialer/default.go

@@ -315,6 +315,14 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
 	}
 }
 
+func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
+	if !destination.Is6() {
+		return dialerFromTCPDialer(d.dialer6)
+	} else {
+		return dialerFromTCPDialer(d.dialer4)
+	}
+}
+
 func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
 	if strategy == nil {
 		strategy = d.networkStrategy

+ 1 - 0
constant/rule.go

@@ -40,4 +40,5 @@ const (
 const (
 	RuleActionRejectMethodDefault = "default"
 	RuleActionRejectMethodDrop    = "drop"
+	RuleActionRejectMethodReply   = "reply"
 )

+ 11 - 2
docs/configuration/route/rule.md

@@ -7,7 +7,8 @@ icon: material/new-box
     :material-plus: [interface_address](#interface_address)  
     :material-plus: [network_interface_address](#network_interface_address)  
     :material-plus: [default_interface_address](#default_interface_address)  
-    :material-plus: [preferred_by](#preferred_by)
+    :material-plus: [preferred_by](#preferred_by)  
+    :material-alert: [network](#network)
 
 !!! quote "Changes in sing-box 1.11.0"
 
@@ -226,7 +227,15 @@ Sniffed client type, see [Protocol Sniff](/configuration/route/sniff/) for detai
 
 #### network
 
-`tcp` or `udp`.
+!!! quote "Changes in sing-box 1.13.0"
+
+    Since sing-box 1.13.0, you can match ICMP echo (ping) requests via the new `icmp` network.
+    
+    Such traffic originates from `TUN`, `WireGuard`, and `Tailscale` inbounds and can be routed to `Direct`, `WireGuard`, and `Tailscale` outbounds.
+
+Match network type.
+
+`tcp`, `udp` or `icmp`.
 
 #### domain
 

+ 11 - 2
docs/configuration/route/rule.zh.md

@@ -7,7 +7,8 @@ icon: material/new-box
     :material-plus: [interface_address](#interface_address)  
     :material-plus: [network_interface_address](#network_interface_address)  
     :material-plus: [default_interface_address](#default_interface_address)  
-    :material-plus: [preferred_by](#preferred_by)
+    :material-plus: [preferred_by](#preferred_by)  
+    :material-alert: [network](#network)
 
 !!! quote "sing-box 1.11.0 中的更改"
 
@@ -223,7 +224,15 @@ icon: material/new-box
 
 #### network
 
-`tcp` 或 `udp`。
+!!! quote "sing-box 1.13.0 中的更改"
+
+    自 sing-box 1.13.0 起,您可以通过新的 `icmp` 网络匹配 ICMP 回显(ping)请求。
+
+    此类流量源自 `TUN`、`WireGuard` 和 `Tailscale` 入站,并可路由至 `Direct`、`WireGuard` 和 `Tailscale` 出站。
+
+匹配网络类型。
+
+`tcp`、`udp` 或 `icmp`。
 
 #### domain
 

+ 16 - 0
docs/configuration/route/rule_action.md

@@ -2,6 +2,10 @@
 icon: material/new-box
 ---
 
+!!! quote "Changes in sing-box 1.13.0"
+
+    :material-alert: [reject](#reject)
+
 !!! quote "Changes in sing-box 1.12.0"
 
     :material-plus: [tls_fragment](#tls_fragment)  
@@ -42,6 +46,10 @@ See `route-options` fields below.
 
 ### reject
 
+!!! quote "Changes in sing-box 1.13.0"
+
+    Since sing-box 1.13.0, you can reject (or directly reply to) ICMP echo (ping) requests using `reject` action.
+
 ```json
 {
   "action": "reject",
@@ -58,9 +66,17 @@ For non-tun connections and already established connections, will just be closed
 
 #### method
 
+For TCP and UDP connections:
+
 - `default`: Reply with TCP RST for TCP connections, and ICMP port unreachable for UDP packets.
 - `drop`: Drop packets.
 
+For ICMP echo requests:
+
+- `default`: Reply with ICMP host unreachable.
+- `drop`: Drop packets.
+- `reply`: Reply with ICMP echo reply.
+
 #### no_drop
 
 If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s.

+ 16 - 0
docs/configuration/route/rule_action.zh.md

@@ -2,6 +2,10 @@
 icon: material/new-box
 ---
 
+!!! quote "sing-box 1.13.0 中的更改"
+
+    :material-alert: [reject](#reject)
+
 !!! quote "sing-box 1.12.0 中的更改"
 
     :material-plus: [tls_fragment](#tls_fragment)  
@@ -38,6 +42,10 @@ icon: material/new-box
 
 ### reject
 
+!!! quote "sing-box 1.13.0 中的更改"
+
+    自 sing-box 1.13.0 起,您可以通过 `reject` 动作拒绝(或直接回复)ICMP 回显(ping)请求。
+
 ```json
 {
   "action": "reject",
@@ -54,9 +62,17 @@ icon: material/new-box
 
 #### method
 
+对于 TCP 和 UDP 连接:
+
 - `default`: 对于 TCP 连接回复 RST,对于 UDP 包回复 ICMP 端口不可达。
 - `drop`: 丢弃数据包。
 
+对于 ICMP 回显请求:
+
+- `default`: 回复 ICMP 主机不可达。
+- `drop`: 丢弃数据包。
+- `reply`: 回复以 ICMP 回显应答。
+
 #### no_drop
 
 如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。

+ 17 - 17
go.mod

@@ -1,6 +1,6 @@
 module github.com/sagernet/sing-box
 
-go 1.23.1
+go 1.24.0
 
 require (
 	github.com/anytls/sing-anytls v0.0.11
@@ -25,30 +25,30 @@ require (
 	github.com/sagernet/cors v1.2.1
 	github.com/sagernet/fswatch v0.1.1
 	github.com/sagernet/gomobile v0.1.8
-	github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
-	github.com/sagernet/quic-go v0.52.0-sing-box-mod.2
-	github.com/sagernet/sing v0.7.12
+	github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506
+	github.com/sagernet/quic-go v0.54.0-sing-box-mod.3
+	github.com/sagernet/sing v0.8.0-beta.5
 	github.com/sagernet/sing-mux v0.3.3
-	github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
+	github.com/sagernet/sing-quic v0.6.0-beta.3
 	github.com/sagernet/sing-shadowsocks v0.2.8
 	github.com/sagernet/sing-shadowsocks2 v0.2.1
 	github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
-	github.com/sagernet/sing-tun v0.7.2
+	github.com/sagernet/sing-tun v0.8.0-beta.8
 	github.com/sagernet/sing-vmess v0.2.7
 	github.com/sagernet/smux v1.5.34-mod.2
-	github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1
+	github.com/sagernet/tailscale v1.80.3-sing-box-1.13-mod.1
 	github.com/sagernet/wireguard-go v0.0.1-beta.7
 	github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
 	github.com/spf13/cobra v1.9.1
-	github.com/stretchr/testify v1.10.0
+	github.com/stretchr/testify v1.11.1
 	github.com/vishvananda/netns v0.0.5
 	go.uber.org/zap v1.27.0
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
-	golang.org/x/crypto v0.41.0
-	golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
-	golang.org/x/mod v0.27.0
-	golang.org/x/net v0.43.0
-	golang.org/x/sys v0.35.0
+	golang.org/x/crypto v0.42.0
+	golang.org/x/exp v0.0.0-20250911091902-df9299821621
+	golang.org/x/mod v0.28.0
+	golang.org/x/net v0.44.0
+	golang.org/x/sys v0.36.0
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
 	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
@@ -122,11 +122,11 @@ require (
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap/exp v0.3.0 // indirect
 	go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
-	golang.org/x/sync v0.16.0 // indirect
-	golang.org/x/term v0.34.0 // indirect
-	golang.org/x/text v0.28.0 // indirect
+	golang.org/x/sync v0.17.0 // indirect
+	golang.org/x/term v0.35.0 // indirect
+	golang.org/x/text v0.29.0 // indirect
 	golang.org/x/time v0.9.0 // indirect
-	golang.org/x/tools v0.36.0 // indirect
+	golang.org/x/tools v0.37.0 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect

+ 32 - 32
go.sum

@@ -158,35 +158,35 @@ github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQ
 github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
 github.com/sagernet/gomobile v0.1.8 h1:vXgoN0pjsMONAaYCTdsKBX2T1kxuS7sbT/mZ7PElGoo=
 github.com/sagernet/gomobile v0.1.8/go.mod h1:A8l3FlHi2D/+mfcd4HHvk5DGFPW/ShFb9jHP5VmSiDY=
-github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38=
-github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
+github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506 h1:x/t3XqWshOlWqRuumpvbUvjtEr/6mJuBXAVovPefbUg=
+github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
 github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
 github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
-github.com/sagernet/quic-go v0.52.0-sing-box-mod.2 h1:QTPr/ptUPsgregVfFXReBFrhv/8U83deZG8urQ7pWYI=
-github.com/sagernet/quic-go v0.52.0-sing-box-mod.2/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
+github.com/sagernet/quic-go v0.54.0-sing-box-mod.3 h1:12pJN/zdpRltLG8l8JA65QYy/a+Mz938yAN3ZQUinbo=
+github.com/sagernet/quic-go v0.54.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
 github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
-github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA=
-github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
+github.com/sagernet/sing v0.8.0-beta.5 h1:Cm4CnLQGNyG5Jl1U9pKWAjFUcbjchGGqn1xeXzfI5kw=
+github.com/sagernet/sing v0.8.0-beta.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
 github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
 github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
-github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=
-github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
+github.com/sagernet/sing-quic v0.6.0-beta.3 h1:Z2vt49f9vNtHc9BbF9foI859n4+NAOV3gBeB1LuzL1Q=
+github.com/sagernet/sing-quic v0.6.0-beta.3/go.mod h1:2/swrSS6wG6MyQA5Blq31VEWitHgBju+yZE8cPK1J5I=
 github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
 github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
 github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
 github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
-github.com/sagernet/sing-tun v0.7.2 h1:uJkAZM0KBqIYzrq077QGqdvj/+4i/pMOx6Pnx0jYqAs=
-github.com/sagernet/sing-tun v0.7.2/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
+github.com/sagernet/sing-tun v0.8.0-beta.8 h1:K/tPAiuW4Qf1usZvdqjAMVwaxAVE/fb64YqO9SCvcTg=
+github.com/sagernet/sing-tun v0.8.0-beta.8/go.mod h1:DCGwHe70ujuzmQ3bvUnf9u1FFRoRvBQ1dDpqZov1ZDA=
 github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
 github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
 github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
 github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
-github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1 h1:gMC0q+0VvZBotZMZ9G0R8ZMEIT/Q6KnXbw0/OgMjmdk=
-github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
+github.com/sagernet/tailscale v1.80.3-sing-box-1.13-mod.1 h1:cWM1iPwqIE1t06ft80wpvFB4xbhOpIFI+TFnTw2gnbs=
+github.com/sagernet/tailscale v1.80.3-sing-box-1.13-mod.1/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
 github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
 github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
@@ -204,8 +204,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
 github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
 github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
@@ -262,21 +262,21 @@ go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wus
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
-golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
-golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
-golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
+golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
+golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
+golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
+golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
 golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
 golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
-golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
-golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
+golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
+golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
-golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
+golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
-golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -285,20 +285,20 @@ golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
-golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
+golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
-golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
+golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
+golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
-golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
+golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
 golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
 golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
-golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
+golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
+golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

+ 1 - 0
option/rule_action.go

@@ -282,6 +282,7 @@ func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error {
 	case "", C.RuleActionRejectMethodDefault:
 		r.Method = C.RuleActionRejectMethodDefault
 	case C.RuleActionRejectMethodDrop:
+	case C.RuleActionRejectMethodReply:
 	default:
 		return E.New("unknown reject method: " + r.Method)
 	}

+ 17 - 1
protocol/direct/outbound.go

@@ -13,6 +13,9 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing-tun/ping"
+	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
@@ -28,10 +31,12 @@ var (
 	_ N.ParallelDialer             = (*Outbound)(nil)
 	_ dialer.ParallelNetworkDialer = (*Outbound)(nil)
 	_ dialer.DirectDialer          = (*Outbound)(nil)
+	_ adapter.DirectRouteOutbound  = (*Outbound)(nil)
 )
 
 type Outbound struct {
 	outbound.Adapter
+	ctx                 context.Context
 	logger              logger.ContextLogger
 	dialer              dialer.ParallelInterfaceDialer
 	domainStrategy      C.DomainStrategy
@@ -57,7 +62,8 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 		return nil, err
 	}
 	outbound := &Outbound{
-		Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
+		Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions),
+		ctx:     ctx,
 		logger:  logger,
 		//nolint:staticcheck
 		domainStrategy: C.DomainStrategy(options.DomainStrategy),
@@ -145,6 +151,16 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
 	return conn, nil
 }
 
+func (h *Outbound) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	ctx := log.ContextWithNewID(h.ctx)
+	destination, err := ping.ConnectDestination(ctx, h.logger, common.MustCast[*dialer.DefaultDialer](h.dialer).DialerForICMPDestination(metadata.Destination.Addr).Control, metadata.Destination.Addr, routeContext, timeout)
+	if err != nil {
+		return nil, err
+	}
+	h.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
+	return destination, nil
+}
+
 func (h *Outbound) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) {
 	ctx, metadata := adapter.ExtendContext(ctx)
 	metadata.Outbound = h.Tag()

+ 10 - 0
protocol/group/selector.go

@@ -3,6 +3,7 @@ package group
 import (
 	"context"
 	"net"
+	"time"
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter/outbound"
@@ -10,6 +11,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	tun "github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
@@ -174,6 +176,14 @@ func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
 	}
 }
 
+func (s *Selector) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	selected := s.selected.Load()
+	if !common.Contains(selected.Network(), metadata.Network) {
+		return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag())
+	}
+	return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
+}
+
 func RealTag(detour adapter.Outbound) string {
 	if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
 		return group.Now()

+ 16 - 0
protocol/group/urltest.go

@@ -14,6 +14,7 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/batch"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -170,6 +171,21 @@ func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
 	s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
 }
 
+func (s *URLTest) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	s.group.Touch()
+	selected := s.group.selectedOutboundTCP
+	if selected == nil {
+		selected, _ = s.group.Select(N.NetworkTCP)
+	}
+	if selected == nil {
+		return nil, E.New("missing supported outbound")
+	}
+	if !common.Contains(selected.Network(), metadata.Network) {
+		return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag())
+	}
+	return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
+}
+
 type URLTestGroup struct {
 	ctx                          context.Context
 	router                       adapter.Router

+ 68 - 9
protocol/tailscale/endpoint.go

@@ -21,6 +21,7 @@ import (
 	"github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet"
 	"github.com/sagernet/gvisor/pkg/tcpip/header"
 	"github.com/sagernet/gvisor/pkg/tcpip/stack"
+	"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
 	"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
 	"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
 	"github.com/sagernet/sing-box/adapter"
@@ -30,7 +31,9 @@ import (
 	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing-tun/ping"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	"github.com/sagernet/sing/common/control"
@@ -56,7 +59,10 @@ import (
 	"go4.org/netipx"
 )
 
-var _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
+var (
+	_ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
+	_ adapter.DirectRouteOutbound         = (*Endpoint)(nil)
+)
 
 func init() {
 	version.SetVersion("sing-box " + C.Version)
@@ -76,6 +82,7 @@ type Endpoint struct {
 	platformInterface platform.Interface
 	server            *tsnet.Server
 	stack             *stack.Stack
+	icmpForwarder     *tun.ICMPForwarder
 	filter            *atomic.Pointer[filter.Filter]
 	onReconfigHook    wgengine.ReconfigListener
 
@@ -175,7 +182,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
 		},
 	}
 	return &Endpoint{
-		Adapter:                endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
+		Adapter:                endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),
 		ctx:                    ctx,
 		router:                 router,
 		logger:                 logger,
@@ -240,9 +247,12 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
 		return gonet.TranslateNetstackError(gErr)
 	}
 	ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket)
-	udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout)
-	ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
+	ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout).HandlePacket)
+	icmpForwarder := tun.NewICMPForwarder(t.ctx, ipStack, t, t.udpTimeout)
+	ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
+	ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
 	t.stack = ipStack
+	t.icmpForwarder = icmpForwarder
 
 	localBackend := t.server.ExportLocalBackend()
 	perfs := &ipn.MaskedPrefs{
@@ -415,7 +425,7 @@ func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
 	return udpConn, nil
 }
 
-func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
+func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
 	tsFilter := t.filter.Load()
 	if tsFilter != nil {
 		var ipProto ipproto.Proto
@@ -424,22 +434,41 @@ func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destina
 			ipProto = ipproto.TCP
 		case N.NetworkUDP:
 			ipProto = ipproto.UDP
+		case N.NetworkICMP:
+			if !destination.IsIPv6() {
+				ipProto = ipproto.ICMPv4
+			} else {
+				ipProto = ipproto.ICMPv6
+			}
 		}
 		response := tsFilter.Check(source.Addr, destination.Addr, destination.Port, ipProto)
 		switch response {
 		case filter.Drop:
-			return syscall.ECONNRESET
+			return nil, syscall.ECONNREFUSED
 		case filter.DropSilently:
-			return tun.ErrDrop
+			return nil, tun.ErrDrop
 		}
 	}
-	return t.router.PreMatch(adapter.InboundContext{
+	var ipVersion uint8
+	if !destination.IsIPv6() {
+		ipVersion = 4
+	} else {
+		ipVersion = 6
+	}
+	routeDestination, err := t.router.PreMatch(adapter.InboundContext{
 		Inbound:     t.Tag(),
 		InboundType: t.Type(),
+		IPVersion:   ipVersion,
 		Network:     network,
 		Source:      source,
 		Destination: destination,
-	})
+	}, routeContext, timeout)
+	if err != nil {
+		if !rule.IsRejected(err) {
+			t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()))
+		}
+	}
+	return routeDestination, err
 }
 
 func (t *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
@@ -482,6 +511,27 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
 	t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
 }
 
+func (t *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	inet4Address, inet6Address := t.server.TailscaleIPs()
+	if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() {
+		return nil, E.New("Tailscale is not ready yet")
+	}
+	ctx := log.ContextWithNewID(t.ctx)
+	destination, err := ping.ConnectGVisor(
+		ctx, t.logger,
+		metadata.Source.Addr, metadata.Destination.Addr,
+		routeContext,
+		t.stack,
+		inet4Address, inet6Address,
+		timeout,
+	)
+	if err != nil {
+		return nil, err
+	}
+	t.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
+	return destination, nil
+}
+
 func (t *Endpoint) PreferredDomain(domain string) bool {
 	routeDomains := t.routeDomains.Load()
 	if routeDomains == nil {
@@ -509,6 +559,15 @@ func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCf
 	if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) {
 		return
 	}
+	var inet4Address, inet6Address netip.Addr
+	for _, address := range cfg.Addresses {
+		if address.Addr().Is4() {
+			inet4Address = address.Addr()
+		} else if address.Addr().Is6() {
+			inet6Address = address.Addr()
+		}
+	}
+	t.icmpForwarder.SetLocalAddresses(inet4Address, inet6Address)
 	t.cfg = cfg
 	t.dnsCfg = dnsCfg
 

+ 17 - 3
protocol/tun/inbound.go

@@ -18,6 +18,7 @@ import (
 	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -454,15 +455,28 @@ func (t *Inbound) Close() error {
 	)
 }
 
-func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
-	return t.router.PreMatch(adapter.InboundContext{
+func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	var ipVersion uint8
+	if !destination.IsIPv6() {
+		ipVersion = 4
+	} else {
+		ipVersion = 6
+	}
+	routeDestination, err := t.router.PreMatch(adapter.InboundContext{
 		Inbound:        t.tag,
 		InboundType:    C.TypeTun,
+		IPVersion:      ipVersion,
 		Network:        network,
 		Source:         source,
 		Destination:    destination,
 		InboundOptions: t.inboundOptions,
-	})
+	}, routeContext, timeout)
+	if err != nil {
+		if !rule.IsRejected(err) {
+			t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()))
+		}
+	}
+	return routeDestination, err
 }
 
 func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {

+ 23 - 4
protocol/wireguard/endpoint.go

@@ -12,7 +12,9 @@ import (
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing-box/transport/wireguard"
+	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/bufio"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -40,7 +42,7 @@ type Endpoint struct {
 
 func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardEndpointOptions) (adapter.Endpoint, error) {
 	ep := &Endpoint{
-		Adapter:        endpoint.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
+		Adapter:        endpoint.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions),
 		ctx:            ctx,
 		router:         router,
 		dnsRouter:      service.FromContext[adapter.DNSRouter](ctx),
@@ -124,14 +126,27 @@ func (w *Endpoint) Close() error {
 	return w.endpoint.Close()
 }
 
-func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
-	return w.router.PreMatch(adapter.InboundContext{
+func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	var ipVersion uint8
+	if !destination.IsIPv6() {
+		ipVersion = 4
+	} else {
+		ipVersion = 6
+	}
+	routeDestination, err := w.router.PreMatch(adapter.InboundContext{
 		Inbound:     w.Tag(),
 		InboundType: w.Type(),
+		IPVersion:   ipVersion,
 		Network:     network,
 		Source:      source,
 		Destination: destination,
-	})
+	}, routeContext, timeout)
+	if err != nil {
+		if !rule.IsRejected(err) {
+			w.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()))
+		}
+	}
+	return routeDestination, err
 }
 
 func (w *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
@@ -220,3 +235,7 @@ func (w *Endpoint) PreferredDomain(domain string) bool {
 func (w *Endpoint) PreferredAddress(address netip.Addr) bool {
 	return w.endpoint.Lookup(address) != nil
 }
+
+func (w *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	return w.endpoint.NewDirectRouteConnection(metadata, routeContext, timeout)
+}

+ 7 - 1
protocol/wireguard/outbound.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"net"
 	"net/netip"
+	"time"
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter/outbound"
@@ -13,6 +14,7 @@ import (
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/transport/wireguard"
+	tun "github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
@@ -42,7 +44,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
 		deprecated.Report(ctx, deprecated.OptionWireGuardGSO)
 	}
 	outbound := &Outbound{
-		Adapter:        outbound.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
+		Adapter:        outbound.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions),
 		ctx:            ctx,
 		dnsRouter:      service.FromContext[adapter.DNSRouter](ctx),
 		logger:         logger,
@@ -168,3 +170,7 @@ func (o *Outbound) PreferredDomain(domain string) bool {
 func (o *Outbound) PreferredAddress(address netip.Addr) bool {
 	return o.endpoint.Lookup(address) != nil
 }
+
+func (o *Outbound) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	return o.endpoint.NewDirectRouteConnection(metadata, routeContext, timeout)
+}

+ 45 - 11
route/route.go

@@ -16,6 +16,7 @@ import (
 	"github.com/sagernet/sing-box/option"
 	R "github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing-mux"
+	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing-vmess"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
@@ -113,6 +114,9 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
 			}
 		case *R.RuleActionReject:
 			buf.ReleaseMulti(buffers)
+			if action.Method == C.RuleActionRejectMethodReply {
+				return E.New("reject method `reply` is not supported for TCP connections")
+			}
 			return action.Error(ctx)
 		case *R.RuleActionHijackDNS:
 			for _, buffer := range buffers {
@@ -228,6 +232,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
 			}
 		case *R.RuleActionReject:
 			N.ReleaseMultiPacketBuffer(packetBuffers)
+			if action.Method == C.RuleActionRejectMethodReply {
+				return E.New("reject method `reply` is not supported for UDP connections")
+			}
 			return action.Error(ctx)
 		case *R.RuleActionHijackDNS:
 			return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
@@ -259,19 +266,47 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
 	return nil
 }
 
-func (r *Router) PreMatch(metadata adapter.InboundContext) error {
+func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
 	selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, nil, nil)
 	if err != nil {
-		return err
+		return nil, err
 	}
-	if selectedRule == nil {
-		return nil
+	if selectedRule != nil {
+		switch action := selectedRule.Action().(type) {
+		case *R.RuleActionReject:
+			switch metadata.Network {
+			case N.NetworkTCP:
+				if action.Method == C.RuleActionRejectMethodReply {
+					return nil, E.New("reject method `reply` is not supported for TCP connections")
+				}
+			case N.NetworkUDP:
+				if action.Method == C.RuleActionRejectMethodReply {
+					return nil, E.New("reject method `reply` is not supported for UDP connections")
+				}
+			}
+			return nil, action.Error(context.Background())
+		case *R.RuleActionRoute:
+			if routeContext == nil {
+				return nil, nil
+			}
+			outbound, loaded := r.outbound.Outbound(action.Outbound)
+			if !loaded {
+				return nil, E.New("outbound not found: ", action.Outbound)
+			}
+			if !common.Contains(outbound.Network(), metadata.Network) {
+				return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound)
+			}
+			return outbound.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
+		}
 	}
-	rejectAction, isReject := selectedRule.Action().(*R.RuleActionReject)
-	if !isReject {
-		return nil
+	if selectedRule != nil || metadata.Network != N.NetworkICMP {
+		return nil, nil
 	}
-	return rejectAction.Error(context.Background())
+	defaultOutbound := r.outbound.Default()
+	if !common.Contains(defaultOutbound.Network(), metadata.Network) {
+		return nil, E.New(metadata.Network, " is not supported by default outbound: ", defaultOutbound.Tag())
+	}
+	return defaultOutbound.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
 }
 
 func (r *Router) matchRule(
@@ -464,7 +499,7 @@ match:
 					fatalErr = newErr
 					return
 				}
-			} else {
+			} else if metadata.Network != N.NetworkICMP {
 				selectedRule = currentRule
 				selectedRuleIndex = currentRuleIndex
 				break match
@@ -478,8 +513,7 @@ match:
 		actionType := currentRule.Action().Type()
 		if actionType == C.RuleActionTypeRoute ||
 			actionType == C.RuleActionTypeReject ||
-			actionType == C.RuleActionTypeHijackDNS ||
-			(actionType == C.RuleActionTypeSniff && preMatch) {
+			actionType == C.RuleActionTypeHijackDNS {
 			selectedRule = currentRule
 			selectedRuleIndex = currentRuleIndex
 			break match

+ 3 - 2
route/rule/rule_action.go

@@ -6,7 +6,6 @@ import (
 	"net/netip"
 	"strings"
 	"sync"
-	"syscall"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -325,9 +324,11 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
 	var returnErr error
 	switch r.Method {
 	case C.RuleActionRejectMethodDefault:
-		returnErr = &RejectedError{syscall.ECONNREFUSED}
+		returnErr = &RejectedError{tun.ErrReset}
 	case C.RuleActionRejectMethodDrop:
 		return &RejectedError{tun.ErrDrop}
+	case C.RuleActionRejectMethodReply:
+		return nil
 	default:
 		panic(F.ToString("unknown reject method: ", r.Method))
 	}

+ 9 - 1
transport/wireguard/device.go

@@ -5,6 +5,7 @@ import (
 	"net/netip"
 	"time"
 
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common/logger"
 	N "github.com/sagernet/sing/common/network"
@@ -17,6 +18,8 @@ type Device interface {
 	N.Dialer
 	Start() error
 	SetDevice(device *device.Device)
+	Inet4Address() netip.Addr
+	Inet6Address() netip.Addr
 }
 
 type DeviceOptions struct {
@@ -35,9 +38,14 @@ type DeviceOptions struct {
 func NewDevice(options DeviceOptions) (Device, error) {
 	if !options.System {
 		return newStackDevice(options)
-	} else if options.Handler == nil {
+	} else if !tun.WithGVisor {
 		return newSystemDevice(options)
 	} else {
 		return newSystemStackDevice(options)
 	}
 }
+
+type NatDevice interface {
+	Device
+	CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
+}

+ 103 - 0
transport/wireguard/device_nat.go

@@ -0,0 +1,103 @@
+package wireguard
+
+import (
+	"context"
+	"sync/atomic"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing-tun/ping"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/logger"
+)
+
+var _ Device = (*natDeviceWrapper)(nil)
+
+type natDeviceWrapper struct {
+	Device
+	ctx            context.Context
+	logger         logger.ContextLogger
+	packetOutbound chan *buf.Buffer
+	rewriter       *ping.Rewriter
+	buffer         [][]byte
+}
+
+func NewNATDevice(ctx context.Context, logger logger.ContextLogger, upstream Device) NatDevice {
+	wrapper := &natDeviceWrapper{
+		Device:         upstream,
+		ctx:            ctx,
+		logger:         logger,
+		packetOutbound: make(chan *buf.Buffer, 256),
+		rewriter:       ping.NewRewriter(ctx, logger, upstream.Inet4Address(), upstream.Inet6Address()),
+	}
+	return wrapper
+}
+
+func (d *natDeviceWrapper) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
+	select {
+	case packet := <-d.packetOutbound:
+		defer packet.Release()
+		sizes[0] = copy(bufs[0][offset:], packet.Bytes())
+		return 1, nil
+	default:
+	}
+	return d.Device.Read(bufs, sizes, offset)
+}
+
+func (d *natDeviceWrapper) Write(bufs [][]byte, offset int) (int, error) {
+	for _, buffer := range bufs {
+		handled, err := d.rewriter.WriteBack(buffer[offset:])
+		if handled {
+			if err != nil {
+				return 0, err
+			}
+		} else {
+			d.buffer = append(d.buffer, buffer)
+		}
+	}
+	if len(d.buffer) > 0 {
+		_, err := d.Device.Write(d.buffer, offset)
+		if err != nil {
+			return 0, err
+		}
+		d.buffer = d.buffer[:0]
+	}
+	return 0, nil
+}
+
+func (d *natDeviceWrapper) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	ctx := log.ContextWithNewID(d.ctx)
+	session := tun.DirectRouteSession{
+		Source:      metadata.Source.Addr,
+		Destination: metadata.Destination.Addr,
+	}
+	d.rewriter.CreateSession(session, routeContext)
+	d.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
+	return &natDestination{device: d, session: session}, nil
+}
+
+var _ tun.DirectRouteDestination = (*natDestination)(nil)
+
+type natDestination struct {
+	device  *natDeviceWrapper
+	session tun.DirectRouteSession
+	closed  atomic.Bool
+}
+
+func (d *natDestination) WritePacket(buffer *buf.Buffer) error {
+	d.device.rewriter.RewritePacket(buffer.Bytes())
+	d.device.packetOutbound <- buffer
+	return nil
+}
+
+func (d *natDestination) Close() error {
+	d.closed.Store(true)
+	d.device.rewriter.DeleteSession(d.session)
+	return nil
+}
+
+func (d *natDestination) IsClosed() bool {
+	return d.closed.Load()
+}

+ 88 - 30
transport/wireguard/device_stack.go

@@ -5,7 +5,9 @@ package wireguard
 import (
 	"context"
 	"net"
+	"net/netip"
 	"os"
+	"time"
 
 	"github.com/sagernet/gvisor/pkg/buffer"
 	"github.com/sagernet/gvisor/pkg/tcpip"
@@ -14,9 +16,14 @@ import (
 	"github.com/sagernet/gvisor/pkg/tcpip/network/ipv4"
 	"github.com/sagernet/gvisor/pkg/tcpip/network/ipv6"
 	"github.com/sagernet/gvisor/pkg/tcpip/stack"
+	"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
 	"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
 	"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing-tun/ping"
+	"github.com/sagernet/sing/common/buf"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
@@ -24,30 +31,40 @@ import (
 	wgTun "github.com/sagernet/wireguard-go/tun"
 )
 
-var _ Device = (*stackDevice)(nil)
+var _ NatDevice = (*stackDevice)(nil)
 
 type stackDevice struct {
-	stack      *stack.Stack
-	mtu        uint32
-	events     chan wgTun.Event
-	outbound   chan *stack.PacketBuffer
-	done       chan struct{}
-	dispatcher stack.NetworkDispatcher
-	addr4      tcpip.Address
-	addr6      tcpip.Address
+	ctx            context.Context
+	logger         log.ContextLogger
+	stack          *stack.Stack
+	mtu            uint32
+	events         chan wgTun.Event
+	outbound       chan *stack.PacketBuffer
+	packetOutbound chan *buf.Buffer
+	done           chan struct{}
+	dispatcher     stack.NetworkDispatcher
+	inet4Address   netip.Addr
+	inet6Address   netip.Addr
 }
 
 func newStackDevice(options DeviceOptions) (*stackDevice, error) {
 	tunDevice := &stackDevice{
-		mtu:      options.MTU,
-		events:   make(chan wgTun.Event, 1),
-		outbound: make(chan *stack.PacketBuffer, 256),
-		done:     make(chan struct{}),
+		ctx:            options.Context,
+		logger:         options.Logger,
+		mtu:            options.MTU,
+		events:         make(chan wgTun.Event, 1),
+		outbound:       make(chan *stack.PacketBuffer, 256),
+		packetOutbound: make(chan *buf.Buffer, 256),
+		done:           make(chan struct{}),
 	}
-	ipStack, err := tun.NewGVisorStack((*wireEndpoint)(tunDevice))
+	ipStack, err := tun.NewGVisorStackWithOptions((*wireEndpoint)(tunDevice), stack.NICOptions{}, true)
 	if err != nil {
 		return nil, err
 	}
+	var (
+		inet4Address netip.Addr
+		inet6Address netip.Addr
+	)
 	for _, prefix := range options.Address {
 		addr := tun.AddressFromAddr(prefix.Addr())
 		protoAddr := tcpip.ProtocolAddress{
@@ -57,10 +74,12 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
 			},
 		}
 		if prefix.Addr().Is4() {
-			tunDevice.addr4 = addr
+			inet4Address = prefix.Addr()
+			tunDevice.inet4Address = inet4Address
 			protoAddr.Protocol = ipv4.ProtocolNumber
 		} else {
-			tunDevice.addr6 = addr
+			inet6Address = prefix.Addr()
+			tunDevice.inet6Address = inet6Address
 			protoAddr.Protocol = ipv6.ProtocolNumber
 		}
 		gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{})
@@ -72,6 +91,10 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
 	if options.Handler != nil {
 		ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
 		ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
+		icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout)
+		icmpForwarder.SetLocalAddresses(inet4Address, inet6Address)
+		ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
+		ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
 	}
 	return tunDevice, nil
 }
@@ -87,11 +110,17 @@ func (w *stackDevice) DialContext(ctx context.Context, network string, destinati
 	}
 	var networkProtocol tcpip.NetworkProtocolNumber
 	if destination.IsIPv4() {
+		if !w.inet4Address.IsValid() {
+			return nil, E.New("missing IPv4 local address")
+		}
 		networkProtocol = header.IPv4ProtocolNumber
-		bind.Addr = w.addr4
+		bind.Addr = tun.AddressFromAddr(w.inet4Address)
 	} else {
+		if !w.inet6Address.IsValid() {
+			return nil, E.New("missing IPv6 local address")
+		}
 		networkProtocol = header.IPv6ProtocolNumber
-		bind.Addr = w.addr6
+		bind.Addr = tun.AddressFromAddr(w.inet6Address)
 	}
 	switch N.NetworkName(network) {
 	case N.NetworkTCP:
@@ -118,10 +147,10 @@ func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr)
 	var networkProtocol tcpip.NetworkProtocolNumber
 	if destination.IsIPv4() {
 		networkProtocol = header.IPv4ProtocolNumber
-		bind.Addr = w.addr4
+		bind.Addr = tun.AddressFromAddr(w.inet4Address)
 	} else {
 		networkProtocol = header.IPv6ProtocolNumber
-		bind.Addr = w.addr6
+		bind.Addr = tun.AddressFromAddr(w.inet4Address)
 	}
 	udpConn, err := gonet.DialUDP(w.stack, &bind, nil, networkProtocol)
 	if err != nil {
@@ -130,6 +159,14 @@ func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr)
 	return udpConn, nil
 }
 
+func (w *stackDevice) Inet4Address() netip.Addr {
+	return w.inet4Address
+}
+
+func (w *stackDevice) Inet6Address() netip.Addr {
+	return w.inet6Address
+}
+
 func (w *stackDevice) SetDevice(device *device.Device) {
 }
 
@@ -144,20 +181,24 @@ func (w *stackDevice) File() *os.File {
 
 func (w *stackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) {
 	select {
-	case packetBuffer, ok := <-w.outbound:
+	case packet, ok := <-w.outbound:
 		if !ok {
 			return 0, os.ErrClosed
 		}
-		defer packetBuffer.DecRef()
-		p := bufs[0]
-		p = p[offset:]
-		n := 0
-		for _, slice := range packetBuffer.AsSlices() {
-			n += copy(p[n:], slice)
+		defer packet.DecRef()
+		var copyN int
+		/*rangeIterate(packet.Data().AsRange(), func(view *buffer.View) {
+			copyN += copy(bufs[0][offset+copyN:], view.AsSlice())
+		})*/
+		for _, view := range packet.AsSlices() {
+			copyN += copy(bufs[0][offset+copyN:], view)
 		}
-		sizes[0] = n
-		count = 1
-		return
+		sizes[0] = copyN
+		return 1, nil
+	case packet := <-w.packetOutbound:
+		defer packet.Release()
+		sizes[0] = copy(bufs[0][offset:], packet.Bytes())
+		return 1, nil
 	case <-w.done:
 		return 0, os.ErrClosed
 	}
@@ -217,6 +258,23 @@ func (w *stackDevice) BatchSize() int {
 	return 1
 }
 
+func (w *stackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	ctx := log.ContextWithNewID(w.ctx)
+	destination, err := ping.ConnectGVisor(
+		ctx, w.logger,
+		metadata.Source.Addr, metadata.Destination.Addr,
+		routeContext,
+		w.stack,
+		w.inet4Address, w.inet6Address,
+		timeout,
+	)
+	if err != nil {
+		return nil, err
+	}
+	w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
+	return destination, nil
+}
+
 var _ stack.LinkEndpoint = (*wireEndpoint)(nil)
 
 type wireEndpoint stackDevice

+ 37 - 9
transport/wireguard/device_system.go

@@ -22,22 +22,42 @@ import (
 var _ Device = (*systemDevice)(nil)
 
 type systemDevice struct {
-	options     DeviceOptions
-	dialer      N.Dialer
-	device      tun.Tun
-	batchDevice tun.LinuxTUN
-	events      chan wgTun.Event
-	closeOnce   sync.Once
+	options      DeviceOptions
+	dialer       N.Dialer
+	device       tun.Tun
+	batchDevice  tun.LinuxTUN
+	events       chan wgTun.Event
+	closeOnce    sync.Once
+	inet4Address netip.Addr
+	inet6Address netip.Addr
 }
 
 func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
 	if options.Name == "" {
 		options.Name = tun.CalculateInterfaceName("wg")
 	}
+	var inet4Address netip.Addr
+	var inet6Address netip.Addr
+	if len(options.Address) > 0 {
+		if prefix := common.Find(options.Address, func(it netip.Prefix) bool {
+			return it.Addr().Is4()
+		}); prefix.IsValid() {
+			inet4Address = prefix.Addr()
+		}
+	}
+	if len(options.Address) > 0 {
+		if prefix := common.Find(options.Address, func(it netip.Prefix) bool {
+			return it.Addr().Is6()
+		}); prefix.IsValid() {
+			inet6Address = prefix.Addr()
+		}
+	}
 	return &systemDevice{
-		options: options,
-		dialer:  options.CreateDialer(options.Name),
-		events:  make(chan wgTun.Event, 1),
+		options:      options,
+		dialer:       options.CreateDialer(options.Name),
+		events:       make(chan wgTun.Event, 1),
+		inet4Address: inet4Address,
+		inet6Address: inet6Address,
 	}, nil
 }
 
@@ -49,6 +69,14 @@ func (w *systemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr
 	return w.dialer.ListenPacket(ctx, destination)
 }
 
+func (w *systemDevice) Inet4Address() netip.Addr {
+	return w.inet4Address
+}
+
+func (w *systemDevice) Inet6Address() netip.Addr {
+	return w.inet6Address
+}
+
 func (w *systemDevice) SetDevice(device *device.Device) {
 }
 

+ 64 - 3
transport/wireguard/device_system_stack.go

@@ -3,16 +3,26 @@
 package wireguard
 
 import (
+	"context"
 	"net/netip"
+	"time"
 
 	"github.com/sagernet/gvisor/pkg/buffer"
 	"github.com/sagernet/gvisor/pkg/tcpip"
 	"github.com/sagernet/gvisor/pkg/tcpip/header"
+	"github.com/sagernet/gvisor/pkg/tcpip/network/ipv4"
+	"github.com/sagernet/gvisor/pkg/tcpip/network/ipv6"
 	"github.com/sagernet/gvisor/pkg/tcpip/stack"
+	"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
 	"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
 	"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing-tun/ping"
 	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/logger"
 	"github.com/sagernet/wireguard-go/device"
 )
 
@@ -20,6 +30,8 @@ var _ Device = (*systemStackDevice)(nil)
 
 type systemStackDevice struct {
 	*systemDevice
+	ctx       context.Context
+	logger    logger.ContextLogger
 	stack     *stack.Stack
 	endpoint  *deviceEndpoint
 	writeBufs [][]byte
@@ -34,13 +46,45 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
 		mtu:  options.MTU,
 		done: make(chan struct{}),
 	}
-	ipStack, err := tun.NewGVisorStack(endpoint)
+	ipStack, err := tun.NewGVisorStackWithOptions(endpoint, stack.NICOptions{}, true)
 	if err != nil {
 		return nil, err
 	}
-	ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
-	ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
+	var (
+		inet4Address netip.Addr
+		inet6Address netip.Addr
+	)
+	for _, prefix := range options.Address {
+		addr := tun.AddressFromAddr(prefix.Addr())
+		protoAddr := tcpip.ProtocolAddress{
+			AddressWithPrefix: tcpip.AddressWithPrefix{
+				Address:   addr,
+				PrefixLen: prefix.Bits(),
+			},
+		}
+		if prefix.Addr().Is4() {
+			inet4Address = prefix.Addr()
+			protoAddr.Protocol = ipv4.ProtocolNumber
+		} else {
+			inet6Address = prefix.Addr()
+			protoAddr.Protocol = ipv6.ProtocolNumber
+		}
+		gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{})
+		if gErr != nil {
+			return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String())
+		}
+	}
+	if options.Handler != nil {
+		ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
+		ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
+		icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout)
+		icmpForwarder.SetLocalAddresses(inet4Address, inet6Address)
+		ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
+		ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
+	}
 	return &systemStackDevice{
+		ctx:          options.Context,
+		logger:       options.Logger,
 		systemDevice: system,
 		stack:        ipStack,
 		endpoint:     endpoint,
@@ -116,6 +160,23 @@ func (w *systemStackDevice) writeStack(packet []byte) bool {
 	return true
 }
 
+func (w *systemStackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	ctx := log.ContextWithNewID(w.ctx)
+	destination, err := ping.ConnectGVisor(
+		ctx, w.logger,
+		metadata.Source.Addr, metadata.Destination.Addr,
+		routeContext,
+		w.stack,
+		w.inet4Address, w.inet6Address,
+		timeout,
+	)
+	if err != nil {
+		return nil, err
+	}
+	w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
+	return destination, nil
+}
+
 type deviceEndpoint struct {
 	mtu        uint32
 	done       chan struct{}

+ 23 - 1
transport/wireguard/endpoint.go

@@ -10,8 +10,11 @@ import (
 	"os"
 	"reflect"
 	"strings"
+	"time"
 	"unsafe"
 
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
@@ -31,6 +34,7 @@ type Endpoint struct {
 	ipcConf        string
 	allowedAddress []netip.Prefix
 	tunDevice      Device
+	natDevice      NatDevice
 	device         *device.Device
 	allowedIPs     *device.AllowedIPs
 	pause          pause.Manager
@@ -114,12 +118,17 @@ func NewEndpoint(options EndpointOptions) (*Endpoint, error) {
 	if err != nil {
 		return nil, E.Cause(err, "create WireGuard device")
 	}
+	natDevice, isNatDevice := tunDevice.(NatDevice)
+	if !isNatDevice {
+		natDevice = NewNATDevice(options.Context, options.Logger, tunDevice)
+	}
 	return &Endpoint{
 		options:        options,
 		peers:          peers,
 		ipcConf:        ipcConf,
 		allowedAddress: allowedAddresses,
 		tunDevice:      tunDevice,
+		natDevice:      natDevice,
 	}, nil
 }
 
@@ -179,7 +188,13 @@ func (e *Endpoint) Start(resolve bool) error {
 			e.options.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...))
 		},
 	}
-	wgDevice := device.NewDevice(e.options.Context, e.tunDevice, bind, logger, e.options.Workers)
+	var deviceInput Device
+	if e.natDevice != nil {
+		deviceInput = e.natDevice
+	} else {
+		deviceInput = e.tunDevice
+	}
+	wgDevice := device.NewDevice(e.options.Context, deviceInput, bind, logger, e.options.Workers)
 	e.tunDevice.SetDevice(wgDevice)
 	ipcConf := e.ipcConf
 	for _, peer := range e.peers {
@@ -229,6 +244,13 @@ func (e *Endpoint) Lookup(address netip.Addr) *device.Peer {
 	return e.allowedIPs.Lookup(address.AsSlice())
 }
 
+func (e *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
+	if e.natDevice == nil {
+		return nil, os.ErrInvalid
+	}
+	return e.natDevice.CreateDestination(metadata, routeContext, timeout)
+}
+
 func (e *Endpoint) onPauseUpdated(event int) {
 	switch event {
 	case pause.EventDevicePaused, pause.EventNetworkPause: