diff --git a/common/process/searcher_linux_shared.go b/common/process/searcher_linux_shared.go index 9e868f36a..faf2bbe94 100644 --- a/common/process/searcher_linux_shared.go +++ b/common/process/searcher_linux_shared.go @@ -38,7 +38,7 @@ type socketDiagConn struct { } type uidProcessPathCache struct { - cache freelru.Cache[uint32, *uidProcessPaths] + cache *freelru.Cache[uint32, *uidProcessPaths] } type uidProcessPaths struct { @@ -85,7 +85,7 @@ func socketDiagSettings(network string, source netip.AddrPort) (family, protocol } func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache { - cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32)) + cache := common.Must1(freelru.New[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32, true)) cache.SetLifetime(ttl) return &uidProcessPathCache{cache: cache} } diff --git a/dns/client.go b/dns/client.go index f01605109..03c2a0e45 100644 --- a/dns/client.go +++ b/dns/client.go @@ -42,7 +42,7 @@ type Client struct { dnsCache adapter.DNSCacheStore initDNSCacheFunc func() adapter.DNSCacheStore logger logger.ContextLogger - cache freelru.Cache[dnsCacheKey, *dns.Msg] + cache *freelru.Cache[dnsCacheKey, *dns.Msg] cacheLock compatible.Map[dnsCacheKey, chan struct{}] backgroundRefresh compatible.Map[dnsCacheKey, struct{}] } @@ -104,7 +104,7 @@ func (c *Client) initializeMemoryCache() { if c.disableCache || c.cache != nil { return } - c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32)) + c.cache = common.Must1(freelru.New[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32, true)) } func extractNegativeTTL(response *dns.Msg) (uint32, bool) { diff --git a/dns/router.go b/dns/router.go index 2143f20e4..b1217b646 100644 --- a/dns/router.go +++ b/dns/router.go @@ -42,7 +42,7 @@ type Router struct { rawRules []option.DNSRule rules []adapter.DNSRule defaultDomainStrategy C.DomainStrategy - dnsReverseMapping freelru.Cache[netip.Addr, string] + dnsReverseMapping *freelru.Cache[netip.Addr, string] platformInterface adapter.PlatformInterface legacyDNSMode bool rulesAccess sync.RWMutex @@ -110,7 +110,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp Logger: router.logger, }) if options.ReverseMapping { - router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32)) + router.dnsReverseMapping = common.Must1(freelru.New[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32, true)) } return router, nil } diff --git a/docs/configuration/endpoint/wireguard.md b/docs/configuration/endpoint/wireguard.md index dc3b82289..070be3977 100644 --- a/docs/configuration/endpoint/wireguard.md +++ b/docs/configuration/endpoint/wireguard.md @@ -1,3 +1,9 @@ +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [udp_mapping](/configuration/shared/udp-nat/#udp_mapping) + :material-plus: [udp_filtering](/configuration/shared/udp-nat/#udp_filtering) + :material-plus: [udp_nat_max](/configuration/shared/udp-nat/#udp_nat_max) + !!! question "Since sing-box 1.11.0" ### Structure @@ -24,7 +30,9 @@ "reserved": [0, 0, 0] } ], - "udp_timeout": "", + + ... // UDP NAT Fields + "workers": 0, ... // Dial Fields @@ -112,18 +120,16 @@ Disabled by default. WireGuard reserved field bytes. -#### udp_timeout - -UDP NAT expiration time. - -`5m` will be used by default. - #### workers WireGuard worker count. CPU count is used by default. +### UDP NAT Fields + +See [UDP NAT Fields](/configuration/shared/udp-nat/) for details. + ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/endpoint/wireguard.zh.md b/docs/configuration/endpoint/wireguard.zh.md index 1935135f8..354358d1c 100644 --- a/docs/configuration/endpoint/wireguard.zh.md +++ b/docs/configuration/endpoint/wireguard.zh.md @@ -1,3 +1,9 @@ +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [udp_mapping](/zh/configuration/shared/udp-nat/#udp_mapping) + :material-plus: [udp_filtering](/zh/configuration/shared/udp-nat/#udp_filtering) + :material-plus: [udp_nat_max](/zh/configuration/shared/udp-nat/#udp_nat_max) + !!! question "自 sing-box 1.11.0 起" ### 结构 @@ -24,7 +30,9 @@ "reserved": [0, 0, 0] } ], - "udp_timeout": "", + + ... // UDP NAT 字段 + "workers": 0, ... // 拨号字段 @@ -114,18 +122,16 @@ WireGuard 对等方的列表。 对等方的保留字段字节。 -#### udp_timeout - -UDP NAT 过期时间。 - -默认使用 `5m`。 - #### workers WireGuard worker 数量。 默认使用 CPU 数量。 +### UDP NAT 字段 + +参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。 + ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/inbound/tproxy.md b/docs/configuration/inbound/tproxy.md index 422885374..d0032a114 100644 --- a/docs/configuration/inbound/tproxy.md +++ b/docs/configuration/inbound/tproxy.md @@ -1,3 +1,9 @@ +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [udp_mapping](/configuration/shared/udp-nat/#udp_mapping) + :material-plus: [udp_filtering](/configuration/shared/udp-nat/#udp_filtering) + :material-plus: [udp_nat_max](/configuration/shared/udp-nat/#udp_nat_max) + !!! quote "" Only supported on Linux. @@ -11,7 +17,9 @@ ... // Listen Fields - "network": "udp" + "network": "udp", + + ... // UDP NAT Fields } ``` @@ -26,3 +34,7 @@ See [Listen Fields](/configuration/shared/listen/) for details. Listen network, one of `tcp` `udp`. Both if empty. + +### UDP NAT Fields + +See [UDP NAT Fields](/configuration/shared/udp-nat/) for details. diff --git a/docs/configuration/inbound/tproxy.zh.md b/docs/configuration/inbound/tproxy.zh.md index 6e35ad5e0..a368b963c 100644 --- a/docs/configuration/inbound/tproxy.zh.md +++ b/docs/configuration/inbound/tproxy.zh.md @@ -1,3 +1,9 @@ +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [udp_mapping](/zh/configuration/shared/udp-nat/#udp_mapping) + :material-plus: [udp_filtering](/zh/configuration/shared/udp-nat/#udp_filtering) + :material-plus: [udp_nat_max](/zh/configuration/shared/udp-nat/#udp_nat_max) + !!! quote "" 仅支持 Linux。 @@ -11,7 +17,9 @@ ... // 监听字段 - "network": "udp" + "network": "udp", + + ... // UDP NAT 字段 } ``` @@ -26,3 +34,7 @@ 监听的网络协议,`tcp` `udp` 之一。 默认所有。 + +### UDP NAT 字段 + +参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。 diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 145caece3..eef22028d 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -8,7 +8,10 @@ icon: material/new-box :material-plus: [exclude_mac_address](#exclude_mac_address) :material-plus: [dns_mode](#dns_mode) :material-plus: [dns_address](#dns_address) - :material-plus: [netns](#netns) + :material-plus: [netns](#netns) + :material-plus: [udp_mapping](/configuration/shared/udp-nat/#udp_mapping) + :material-plus: [udp_filtering](/configuration/shared/udp-nat/#udp_filtering) + :material-plus: [udp_nat_max](/configuration/shared/udp-nat/#udp_nat_max) !!! quote "Changes in sing-box 1.13.3" @@ -112,7 +115,9 @@ icon: material/new-box "geoip-cn" ], "endpoint_independent_nat": false, - "udp_timeout": "5m", + + ... // UDP NAT Fields + "stack": "system", "include_interface": [ "lan0" @@ -553,12 +558,6 @@ Enable endpoint-independent NAT. Performance may degrade slightly, so it is not recommended to enable on when it is not needed. -#### udp_timeout - -UDP NAT expiration time. - -`5m` will be used by default. - #### stack !!! quote "Changes in sing-box 1.8.0" @@ -700,6 +699,10 @@ Hostnames that bypass the HTTP proxy. Hostnames that use the HTTP proxy. +### UDP NAT Fields + +See [UDP NAT Fields](/configuration/shared/udp-nat/) for details. + ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 4c88b11f0..b1f226afa 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -8,7 +8,10 @@ icon: material/new-box :material-plus: [exclude_mac_address](#exclude_mac_address) :material-plus: [dns_mode](#dns_mode) :material-plus: [dns_address](#dns_address) - :material-plus: [netns](#netns) + :material-plus: [netns](#netns) + :material-plus: [udp_mapping](/zh/configuration/shared/udp-nat/#udp_mapping) + :material-plus: [udp_filtering](/zh/configuration/shared/udp-nat/#udp_filtering) + :material-plus: [udp_nat_max](/zh/configuration/shared/udp-nat/#udp_nat_max) !!! quote "sing-box 1.13.3 中的更改" @@ -113,7 +116,9 @@ icon: material/new-box "geoip-cn" ], "endpoint_independent_nat": false, - "udp_timeout": "5m", + + ... // UDP NAT 字段 + "stack": "system", "include_interface": [ "lan0" @@ -533,12 +538,6 @@ sing-box DNS 模块,等价于一条 性能可能会略有下降,所以不建议在不需要的时候开启。 -#### udp_timeout - -UDP NAT 过期时间。 - -默认使用 `5m`。 - #### stack !!! quote "sing-box 1.8.0 中的更改" @@ -680,6 +679,10 @@ TCP/IP 栈。 代理的主机名列表。 +### UDP NAT 字段 + +参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。 + ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/shared/udp-nat.md b/docs/configuration/shared/udp-nat.md new file mode 100644 index 000000000..3a3a15cab --- /dev/null +++ b/docs/configuration/shared/udp-nat.md @@ -0,0 +1,67 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [udp_mapping](#udp_mapping) + :material-plus: [udp_filtering](#udp_filtering) + :material-plus: [udp_nat_max](#udp_nat_max) + +### Structure + +```json +{ + "udp_timeout": "5m", + "udp_mapping": "endpoint_independent", + "udp_filtering": "endpoint_independent", + "udp_nat_max": 0 +} +``` + +### Fields + +#### udp_timeout + +UDP NAT expiration time. + +`5m` will be used by default. + +#### udp_mapping + +!!! question "Since sing-box 1.14.0" + +UDP NAT mapping behavior. + +| Value | Behavior | +|------------------------------|-------------------------------------------------------------------------------| +| `endpoint_independent` | Reuse the same mapping for the same source address and port for all destinations. | +| `address_dependent` | Use a separate mapping for each destination address. | +| `address_and_port_dependent` | Use a separate mapping for each destination address and port. | + +`endpoint_independent` is used by default. + +#### udp_filtering + +!!! question "Since sing-box 1.14.0" + +UDP NAT filtering behavior. + +| Value | Behavior | +|------------------------------|-----------------------------------------------------------------------------| +| `endpoint_independent` | Accept packets from any remote endpoint. | +| `address_dependent` | Accept packets only from remote addresses to which packets have been sent. | +| `address_and_port_dependent` | Accept packets only from remote addresses and ports to which packets have been sent. | + +`endpoint_independent` is used by default. + +#### udp_nat_max + +!!! question "Since sing-box 1.14.0" + +Maximum number of UDP NAT sessions. + +When the limit is reached, the least recently used session is closed. + +When unset or set to `0`, `4096` is used on iOS. On other platforms, a value from `4096` to `16384` is selected based on total memory; +`16384` is used if total memory cannot be detected. diff --git a/docs/configuration/shared/udp-nat.zh.md b/docs/configuration/shared/udp-nat.zh.md new file mode 100644 index 000000000..d6490241f --- /dev/null +++ b/docs/configuration/shared/udp-nat.zh.md @@ -0,0 +1,67 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [udp_mapping](#udp_mapping) + :material-plus: [udp_filtering](#udp_filtering) + :material-plus: [udp_nat_max](#udp_nat_max) + +### 结构 + +```json +{ + "udp_timeout": "5m", + "udp_mapping": "endpoint_independent", + "udp_filtering": "endpoint_independent", + "udp_nat_max": 0 +} +``` + +### 字段 + +#### udp_timeout + +UDP NAT 过期时间。 + +默认使用 `5m`。 + +#### udp_mapping + +!!! question "自 sing-box 1.14.0 起" + +UDP NAT 映射行为。 + +| 值 | 行为 | +|------------------------------|----------------------------------------------------------| +| `endpoint_independent` | 对相同的源地址和端口,所有目标复用同一映射。 | +| `address_dependent` | 每个目标地址使用单独的映射。 | +| `address_and_port_dependent` | 每个目标地址和端口使用单独的映射。 | + +默认使用 `endpoint_independent`。 + +#### udp_filtering + +!!! question "自 sing-box 1.14.0 起" + +UDP NAT 过滤行为。 + +| 值 | 行为 | +|------------------------------|----------------------------------------------------------| +| `endpoint_independent` | 接受来自任意远程端点的数据包。 | +| `address_dependent` | 仅接受来自已向其发送过数据包的远程地址的数据包。 | +| `address_and_port_dependent` | 仅接受来自已向其发送过数据包的远程地址和端口的数据包。 | + +默认使用 `endpoint_independent`。 + +#### udp_nat_max + +!!! question "自 sing-box 1.14.0 起" + +UDP NAT 会话的最大数量。 + +达到限制时,将关闭最近最少使用的会话。 + +未设置或设置为 `0` 时,iOS 使用 `4096`。其他平台根据总内存在 `4096` 到 `16384` 之间选择; +无法检测总内存时使用 `16384`。 diff --git a/go.mod b/go.mod index e1b03b778..9314fff5e 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-mod.4 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.12-0.20260715103206-ac5f044167e4 + github.com/sagernet/sing v0.8.12-0.20260716111929-074fc9988b34 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.5 github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc @@ -51,7 +51,7 @@ require ( github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1 github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb - github.com/sagernet/sing-tun v0.8.12-0.20260714150017-c54b06b8a670 + github.com/sagernet/sing-tun v0.8.12-0.20260716125804-79ea1ac88855 github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 diff --git a/go.sum b/go.sum index 8578921e0..aaedbba96 100644 --- a/go.sum +++ b/go.sum @@ -270,8 +270,8 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.12-0.20260715103206-ac5f044167e4 h1:/CRUuZdfYnrt/8+IkcgrlOkrkSW6JTh0FesoIe99IXg= -github.com/sagernet/sing v0.8.12-0.20260715103206-ac5f044167e4/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/sagernet/sing v0.8.12-0.20260716111929-074fc9988b34 h1:rgSs2ttiz8EaubsOt0SkzsqciY0m0PRp3w/fOisPoNo= +github.com/sagernet/sing v0.8.12-0.20260716111929-074fc9988b34/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= @@ -286,8 +286,8 @@ github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkG github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb h1:VvU2/PZqP5tbKTDq0BxkhRO8ZnKI4UJzziakgBiP2Qg= github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY= -github.com/sagernet/sing-tun v0.8.12-0.20260714150017-c54b06b8a670 h1:suDpFVVpMTyzdssMNP/8OSgaADlGyTtdbnPm4JwJsS0= -github.com/sagernet/sing-tun v0.8.12-0.20260714150017-c54b06b8a670/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-tun v0.8.12-0.20260716125804-79ea1ac88855 h1:zP2Bb6NLl3/zV24mmBjcpWIdvOKTiMo2SS0eJ8WpYFU= +github.com/sagernet/sing-tun v0.8.12-0.20260716125804-79ea1ac88855/go.mod h1:SF7OPVtqIYY14T4KmnoHND5CFiMSUCTYxzVOT3nZUuc= github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0= github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= diff --git a/mkdocs.yml b/mkdocs.yml index f6beec14c..4a5b061e0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -139,6 +139,7 @@ nav: - Multiplex: configuration/shared/multiplex.md - V2Ray Transport: configuration/shared/v2ray-transport.md - UDP over TCP: configuration/shared/udp-over-tcp.md + - UDP NAT Fields: configuration/shared/udp-nat.md - TCP Brutal: configuration/shared/tcp-brutal.md - Wi-Fi State: configuration/shared/wifi-state.md - Neighbor Resolution: configuration/shared/neighbor.md @@ -302,6 +303,7 @@ plugins: Multiplex: 多路复用 V2Ray Transport: V2Ray 传输层 Wi-Fi State: Wi-Fi 状态 + UDP NAT Fields: UDP NAT 字段 Network Namespace: 网络命名空间 Endpoint: 端点 diff --git a/option/inbound.go b/option/inbound.go index 21497a3f4..0fdf3e644 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -87,6 +87,48 @@ type ListenOptions struct { InboundOptions } +type UDPNATBehavior uint8 + +const ( + UDPNATBehaviorEndpointIndependent UDPNATBehavior = iota + UDPNATBehaviorAddressDependent + UDPNATBehaviorAddressAndPortDependent +) + +func (b UDPNATBehavior) MarshalJSON() ([]byte, error) { + var value string + switch b { + case UDPNATBehaviorEndpointIndependent: + value = "endpoint_independent" + case UDPNATBehaviorAddressDependent: + value = "address_dependent" + case UDPNATBehaviorAddressAndPortDependent: + value = "address_and_port_dependent" + default: + return nil, E.New("unknown UDP NAT behavior: ", uint8(b)) + } + return json.Marshal(value) +} + +func (b *UDPNATBehavior) UnmarshalJSON(data []byte) error { + var value string + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + switch value { + case "", "endpoint_independent": + *b = UDPNATBehaviorEndpointIndependent + case "address_dependent": + *b = UDPNATBehaviorAddressDependent + case "address_and_port_dependent": + *b = UDPNATBehaviorAddressAndPortDependent + default: + return E.New("unknown UDP NAT behavior: ", value) + } + return nil +} + type UDPTimeoutCompat badoption.Duration func (c UDPTimeoutCompat) MarshalJSON() ([]byte, error) { diff --git a/option/redir.go b/option/redir.go index 743a6e107..1fca7ab9f 100644 --- a/option/redir.go +++ b/option/redir.go @@ -6,5 +6,8 @@ type RedirectInboundOptions struct { type TProxyInboundOptions struct { ListenOptions - Network NetworkList `json:"network,omitempty"` + Network NetworkList `json:"network,omitempty"` + UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"` + UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"` + UDPNATMax uint32 `json:"udp_nat_max,omitempty"` } diff --git a/option/tun.go b/option/tun.go index 34e95b734..554af76c3 100644 --- a/option/tun.go +++ b/option/tun.go @@ -45,6 +45,9 @@ type TunInboundOptions struct { IncludeMACAddress badoption.Listable[string] `json:"include_mac_address,omitempty"` ExcludeMACAddress badoption.Listable[string] `json:"exclude_mac_address,omitempty"` UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"` + UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"` + UDPNATMax uint32 `json:"udp_nat_max,omitempty"` Stack string `json:"stack,omitempty"` Platform *TunPlatformOptions `json:"platform,omitempty"` InboundOptions diff --git a/option/wireguard.go b/option/wireguard.go index c86abd112..d25d0d207 100644 --- a/option/wireguard.go +++ b/option/wireguard.go @@ -7,15 +7,18 @@ import ( ) type WireGuardEndpointOptions struct { - System bool `json:"system,omitempty"` - Name string `json:"name,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - Address badoption.Listable[netip.Prefix] `json:"address"` - PrivateKey string `json:"private_key"` - ListenPort uint16 `json:"listen_port,omitempty"` - Peers []WireGuardPeer `json:"peers,omitempty"` - UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` - Workers int `json:"workers,omitempty"` + System bool `json:"system,omitempty"` + Name string `json:"name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + Address badoption.Listable[netip.Prefix] `json:"address"` + PrivateKey string `json:"private_key"` + ListenPort uint16 `json:"listen_port,omitempty"` + Peers []WireGuardPeer `json:"peers,omitempty"` + UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` + UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"` + UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"` + UDPNATMax uint32 `json:"udp_nat_max,omitempty"` + Workers int `json:"workers,omitempty"` DialerOptions } diff --git a/protocol/redirect/tproxy.go b/protocol/redirect/tproxy.go index 5f2416262..48fae0f23 100644 --- a/protocol/redirect/tproxy.go +++ b/protocol/redirect/tproxy.go @@ -13,12 +13,13 @@ 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/buf" "github.com/sagernet/sing/common/control" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/udpnat2" + "github.com/sagernet/sing/service" ) func RegisterTProxy(registry *inbound.Registry) { @@ -31,7 +32,7 @@ type TProxy struct { router adapter.Router logger log.ContextLogger listener *listener.Listener - udpNat *udpnat.Service + udpNat *tun.UDPNat } func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) (adapter.Inbound, error) { @@ -47,7 +48,16 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog } else { udpTimeout = C.UDPTimeout } - tproxy.udpNat = udpnat.New(tproxy, tproxy.preparePacketConnection, udpTimeout, false) + networkManager := service.FromContext[adapter.NetworkManager](ctx) + tproxy.udpNat = tun.NewUDPNat(tun.UDPNatOptions{ + Handler: tproxy, + Prepare: tproxy.preparePacketConnection, + Timeout: udpTimeout, + Mapping: tun.NATMapping(options.UDPMapping), + Filtering: tun.NATFiltering(options.UDPFiltering), + MaxSize: options.UDPNATMax, + InterfaceFinder: networkManager.InterfaceFinder(), + }) tproxy.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, @@ -64,10 +74,19 @@ func (t *TProxy) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } - return t.listener.Start() + err := t.udpNat.Start() + if err != nil { + return err + } + err = t.listener.Start() + if err != nil { + _ = t.udpNat.Close() + } + return err } func (t *TProxy) Close() error { + _ = t.udpNat.Close() return t.listener.Close() } diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 26b024b9e..284df6836 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -42,6 +42,9 @@ type Inbound struct { logger log.ContextLogger tunOptions tun.Options udpTimeout time.Duration + udpMapping tun.NATMapping + udpFiltering tun.NATFiltering + udpNATMax uint32 dnsHijackAddress []netip.Addr stack string tunIf tun.Tun @@ -230,6 +233,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo EXP_MultiPendingPackets: multiPendingPackets, }, udpTimeout: udpTimeout, + udpMapping: tun.NATMapping(options.UDPMapping), + udpFiltering: tun.NATFiltering(options.UDPFiltering), + udpNATMax: options.UDPNATMax, stack: options.Stack, platformInterface: platformInterface, platformOptions: common.PtrValueOrDefault(options.Platform), @@ -450,6 +456,9 @@ func (t *Inbound) Start(stage adapter.StartStage) error { TunOptions: t.tunOptions, UDPTimeout: t.udpTimeout, ICMPTimeout: C.ICMPTimeout, + UDPMapping: t.udpMapping, + UDPFiltering: t.udpFiltering, + UDPNATMax: t.udpNATMax, Handler: t, Logger: t.logger, ForwarderBindInterface: C.IsDarwin, diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index 9b65ac9cc..e126b9428 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -73,14 +73,19 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL } else { udpTimeout = C.UDPTimeout } + networkManager := service.FromContext[adapter.NetworkManager](ctx) wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{ - Context: ctx, - Logger: logger, - System: options.System, - Handler: ep, - UDPTimeout: udpTimeout, - ICMPTimeout: C.ICMPTimeout, - Dialer: outboundDialer, + Context: ctx, + Logger: logger, + System: options.System, + Handler: ep, + UDPTimeout: udpTimeout, + ICMPTimeout: C.ICMPTimeout, + UDPMapping: tun.NATMapping(options.UDPMapping), + UDPFiltering: tun.NATFiltering(options.UDPFiltering), + UDPNATMax: options.UDPNATMax, + InterfaceFinder: networkManager.InterfaceFinder(), + Dialer: outboundDialer, CreateDialer: func(interfaceName string) N.Dialer { return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{ BindInterface: interfaceName, diff --git a/route/router.go b/route/router.go index aa621d3fc..2359e5c3e 100644 --- a/route/router.go +++ b/route/router.go @@ -42,7 +42,7 @@ type Router struct { ruleSetMap map[string]adapter.RuleSet ruleSetUpdater *R.RuleSetUpdater processSearcher process.Searcher - processCache freelru.Cache[processCacheKey, processCacheEntry] + processCache *freelru.Cache[processCacheKey, processCacheEntry] neighborResolver adapter.NeighborResolver pauseManager pause.Manager trackers []adapter.ConnectionTracker @@ -192,7 +192,7 @@ func (r *Router) Start(stage adapter.StartStage) error { } } if r.processSearcher != nil { - processCache := common.Must1(freelru.NewSharded[processCacheKey, processCacheEntry](256, maphash.NewHasher[processCacheKey]().Hash32)) + processCache := common.Must1(freelru.New[processCacheKey, processCacheEntry](256, maphash.NewHasher[processCacheKey]().Hash32, true)) processCache.SetLifetime(200 * time.Millisecond) r.processCache = processCache } diff --git a/transport/wireguard/device.go b/transport/wireguard/device.go index 0cfbce6ff..37575e4d7 100644 --- a/transport/wireguard/device.go +++ b/transport/wireguard/device.go @@ -6,6 +6,7 @@ import ( "time" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" "github.com/sagernet/wireguard-go/device" @@ -22,17 +23,22 @@ type Device interface { } type DeviceOptions struct { - Context context.Context - Logger logger.ContextLogger - System bool - Handler tun.Handler - UDPTimeout time.Duration - ICMPTimeout time.Duration - CreateDialer func(interfaceName string) N.Dialer - Name string - MTU uint32 - Address []netip.Prefix - AllowedAddress []netip.Prefix + Context context.Context + Logger logger.ContextLogger + System bool + Handler tun.Handler + UDPTimeout time.Duration + ICMPTimeout time.Duration + UDPMapping tun.NATMapping + UDPFiltering tun.NATFiltering + UDPNATMax uint32 + NetworkMonitor tun.NetworkUpdateMonitor + InterfaceFinder control.InterfaceFinder + CreateDialer func(interfaceName string) N.Dialer + Name string + MTU uint32 + Address []netip.Prefix + AllowedAddress []netip.Prefix } func NewDevice(options DeviceOptions) (Device, error) { diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go index 949575e51..3321ef8f0 100644 --- a/transport/wireguard/device_stack.go +++ b/transport/wireguard/device_stack.go @@ -42,6 +42,7 @@ type stackDevice struct { inet4Address netip.Addr inet6Address netip.Addr icmpForwarder *tun.ICMPForwarder + udpForwarder *tun.UDPForwarder } func newStackDevice(options DeviceOptions) (*stackDevice, error) { @@ -79,7 +80,16 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) { tunDevice.stack = ipStack 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) + udpForwarder := tun.NewUDPForwarder(options.Context, ipStack, options.Handler, tun.UDPNatOptions{ + Timeout: options.UDPTimeout, + Shared: true, + Mapping: options.UDPMapping, + Filtering: options.UDPFiltering, + MaxSize: options.UDPNATMax, + InterfaceFinder: options.InterfaceFinder, + }) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) + tunDevice.udpForwarder = udpForwarder icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) @@ -166,6 +176,12 @@ func (w *stackDevice) SetDevice(device *device.Device) { } func (w *stackDevice) Start() error { + if w.udpForwarder != nil { + err := w.udpForwarder.Start() + if err != nil { + return err + } + } w.events <- wgTun.EventUp return nil } @@ -245,6 +261,9 @@ func (w *stackDevice) Close() error { if w.icmpForwarder != nil { w.icmpForwarder.Close() } + if w.udpForwarder != nil { + _ = w.udpForwarder.Close() + } w.stack.Close() for _, endpoint := range w.stack.CleanupEndpoints() { endpoint.Abort() diff --git a/transport/wireguard/device_system_stack.go b/transport/wireguard/device_system_stack.go index 0db1297c5..ae92235b4 100644 --- a/transport/wireguard/device_system_stack.go +++ b/transport/wireguard/device_system_stack.go @@ -27,6 +27,7 @@ type systemStackDevice struct { stack *stack.Stack endpoint *deviceEndpoint icmpForwarder *tun.ICMPForwarder + udpForwarder *tun.UDPForwarder writeBufs [][]byte closeOnce sync.Once } @@ -69,7 +70,17 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, 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) + udpForwarder := tun.NewUDPForwarder(options.Context, ipStack, options.Handler, tun.UDPNatOptions{ + Timeout: options.UDPTimeout, + Shared: true, + Mapping: options.UDPMapping, + Filtering: options.UDPFiltering, + MaxSize: options.UDPNATMax, + InterfaceFinder: options.InterfaceFinder, + ExcludeInterface: []string{options.Name}, + }) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) + stackDevice.udpForwarder = udpForwarder icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) @@ -82,6 +93,20 @@ func (w *systemStackDevice) SetDevice(device *device.Device) { w.endpoint.device = device } +func (w *systemStackDevice) Start() error { + if w.udpForwarder != nil { + err := w.udpForwarder.Start() + if err != nil { + return err + } + } + err := w.systemDevice.Start() + if err != nil && w.udpForwarder != nil { + _ = w.udpForwarder.Close() + } + return err +} + func (w *systemStackDevice) Write(bufs [][]byte, offset int) (count int, err error) { if w.batchDevice != nil { w.writeBufs = w.writeBufs[:0] @@ -118,6 +143,9 @@ func (w *systemStackDevice) Close() error { if w.icmpForwarder != nil { w.icmpForwarder.Close() } + if w.udpForwarder != nil { + _ = w.udpForwarder.Close() + } w.stack.Close() for _, endpoint := range w.stack.CleanupEndpoints() { endpoint.Abort() diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index ce650ef1b..734d5f3c0 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -101,17 +101,21 @@ func NewEndpoint(options EndpointOptions) (*Endpoint, error) { options.MTU = 1408 } deviceOptions := DeviceOptions{ - Context: options.Context, - Logger: options.Logger, - System: options.System, - Handler: options.Handler, - UDPTimeout: options.UDPTimeout, - ICMPTimeout: options.ICMPTimeout, - CreateDialer: options.CreateDialer, - Name: options.Name, - MTU: options.MTU, - Address: options.Address, - AllowedAddress: allowedAddresses, + Context: options.Context, + Logger: options.Logger, + System: options.System, + Handler: options.Handler, + UDPTimeout: options.UDPTimeout, + ICMPTimeout: options.ICMPTimeout, + UDPMapping: options.UDPMapping, + UDPFiltering: options.UDPFiltering, + UDPNATMax: options.UDPNATMax, + InterfaceFinder: options.InterfaceFinder, + CreateDialer: options.CreateDialer, + Name: options.Name, + MTU: options.MTU, + Address: options.Address, + AllowedAddress: allowedAddresses, } tunDevice, err := NewDevice(deviceOptions) if err != nil { @@ -231,8 +235,9 @@ func (e *Endpoint) Close() error { e.device.Down() e.device.Close() e.device = nil + return nil } - return nil + return e.tunDevice.Close() } func (e *Endpoint) Lookup(address netip.Addr) *device.Peer { diff --git a/transport/wireguard/endpoint_options.go b/transport/wireguard/endpoint_options.go index 1f950eff5..817c18cb2 100644 --- a/transport/wireguard/endpoint_options.go +++ b/transport/wireguard/endpoint_options.go @@ -6,6 +6,7 @@ import ( "time" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -18,16 +19,21 @@ type EndpointOptions struct { Handler tun.Handler UDPTimeout time.Duration ICMPTimeout time.Duration - Dialer N.Dialer - CreateDialer func(interfaceName string) N.Dialer - Name string - MTU uint32 - Address []netip.Prefix - PrivateKey string - ListenPort uint16 - ResolvePeer func(domain string) (netip.Addr, error) - Peers []PeerOptions - Workers int + UDPMapping tun.NATMapping + UDPFiltering tun.NATFiltering + UDPNATMax uint32 + + InterfaceFinder control.InterfaceFinder + Dialer N.Dialer + CreateDialer func(interfaceName string) N.Dialer + Name string + MTU uint32 + Address []netip.Prefix + PrivateKey string + ListenPort uint16 + ResolvePeer func(domain string) (netip.Addr, error) + Peers []PeerOptions + Workers int } type PeerOptions struct {