refactor: New tun udpnat
This commit is contained in:
@@ -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}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/)。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/)。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/)。
|
||||
|
||||
@@ -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.
|
||||
@@ -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`。
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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: 端点
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+4
-1
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+12
-9
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user