mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Add sniff_override_rules
This commit is contained in:
parent
9be5cd99c1
commit
32ec64f781
@ -6,8 +6,8 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/geoip"
|
"github.com/sagernet/sing-box/common/geoip"
|
||||||
"github.com/sagernet/sing-dns"
|
dns "github.com/sagernet/sing-dns"
|
||||||
"github.com/sagernet/sing-tun"
|
tun "github.com/sagernet/sing-tun"
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
@ -84,6 +84,10 @@ type DNSRule interface {
|
|||||||
RewriteTTL() *uint32
|
RewriteTTL() *uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SniffOverrideRule interface {
|
||||||
|
Rule
|
||||||
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
type InterfaceUpdateListener interface {
|
||||||
InterfaceUpdated()
|
InterfaceUpdated()
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
"sniff": false,
|
"sniff": false,
|
||||||
"sniff_override_destination": false,
|
"sniff_override_destination": false,
|
||||||
|
"sniff_override_rules": [],
|
||||||
"sniff_timeout": "300ms",
|
"sniff_timeout": "300ms",
|
||||||
"domain_strategy": "prefer_ipv6",
|
"domain_strategy": "prefer_ipv6",
|
||||||
"udp_timeout": 300,
|
"udp_timeout": 300,
|
||||||
@ -68,6 +69,14 @@ Override the connection destination address with the sniffed domain.
|
|||||||
|
|
||||||
If the domain name is invalid (like tor), this will not work.
|
If the domain name is invalid (like tor), this will not work.
|
||||||
|
|
||||||
|
#### sniff_override_rules
|
||||||
|
|
||||||
|
Pick up the connection that will be overrided destination address with the sniffed domain by rules.
|
||||||
|
|
||||||
|
If the domain name is invalid (like tor), this will not work.
|
||||||
|
|
||||||
|
See [Sniff Override Rule](/configuration/shared/sniff_override_rules/) for details.
|
||||||
|
|
||||||
#### sniff_timeout
|
#### sniff_timeout
|
||||||
|
|
||||||
Timeout for sniffing.
|
Timeout for sniffing.
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
"sniff": false,
|
"sniff": false,
|
||||||
"sniff_override_destination": false,
|
"sniff_override_destination": false,
|
||||||
|
"sniff_override_rules": [],
|
||||||
"sniff_timeout": "300ms",
|
"sniff_timeout": "300ms",
|
||||||
"domain_strategy": "prefer_ipv6",
|
"domain_strategy": "prefer_ipv6",
|
||||||
"udp_timeout": 300,
|
"udp_timeout": 300,
|
||||||
@ -69,6 +70,12 @@
|
|||||||
|
|
||||||
如果域名无效(如 Tor),将不生效。
|
如果域名无效(如 Tor),将不生效。
|
||||||
|
|
||||||
|
#### sniff_override_rules
|
||||||
|
|
||||||
|
根据规则选择处需要用探测出的域名覆盖目标地址的连接。
|
||||||
|
|
||||||
|
参阅 [Sniff Override Rule](/zh/configuration/shared/sniff_override_rules/)
|
||||||
|
|
||||||
#### sniff_timeout
|
#### sniff_timeout
|
||||||
|
|
||||||
探测超时时间。
|
探测超时时间。
|
||||||
|
242
docs/configuration/shared/sniff_override_rule.md
Normal file
242
docs/configuration/shared/sniff_override_rule.md
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"ip_version": 6,
|
||||||
|
"network": [
|
||||||
|
"tcp"
|
||||||
|
],
|
||||||
|
"auth_user": [
|
||||||
|
"usera",
|
||||||
|
"userb"
|
||||||
|
],
|
||||||
|
"protocol": [
|
||||||
|
"tls",
|
||||||
|
"http",
|
||||||
|
"quic"
|
||||||
|
],
|
||||||
|
"domain": [
|
||||||
|
"test.com"
|
||||||
|
],
|
||||||
|
"domain_suffix": [
|
||||||
|
".cn"
|
||||||
|
],
|
||||||
|
"domain_keyword": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"domain_regex": [
|
||||||
|
"^stun\\..+"
|
||||||
|
],
|
||||||
|
"geosite": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"source_geoip": [
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
"geoip": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"source_ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"source_port": [
|
||||||
|
12345
|
||||||
|
],
|
||||||
|
"source_port_range": [
|
||||||
|
"1000:2000",
|
||||||
|
":3000",
|
||||||
|
"4000:"
|
||||||
|
],
|
||||||
|
"port": [
|
||||||
|
80,
|
||||||
|
443
|
||||||
|
],
|
||||||
|
"port_range": [
|
||||||
|
"1000:2000",
|
||||||
|
":3000",
|
||||||
|
"4000:"
|
||||||
|
],
|
||||||
|
"process_name": [
|
||||||
|
"curl"
|
||||||
|
],
|
||||||
|
"process_path": [
|
||||||
|
"/usr/bin/curl"
|
||||||
|
],
|
||||||
|
"package_name": [
|
||||||
|
"com.termux"
|
||||||
|
],
|
||||||
|
"user": [
|
||||||
|
"sekai"
|
||||||
|
],
|
||||||
|
"user_id": [
|
||||||
|
1000
|
||||||
|
],
|
||||||
|
"clash_mode": "direct",
|
||||||
|
"invert": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "logical",
|
||||||
|
"mode": "and",
|
||||||
|
"rules": [],
|
||||||
|
"invert": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
You can ignore the JSON Array [] tag when the content is only one item
|
||||||
|
|
||||||
|
### Default Fields
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
The default rule uses the following matching logic:
|
||||||
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
|
||||||
|
(`port` || `port_range`) &&
|
||||||
|
(`source_geoip` || `source_ip_cidr`) &&
|
||||||
|
(`source_port` || `source_port_range`) &&
|
||||||
|
`other fields`
|
||||||
|
|
||||||
|
#### ip_version
|
||||||
|
|
||||||
|
4 or 6.
|
||||||
|
|
||||||
|
Not limited if empty.
|
||||||
|
|
||||||
|
#### auth_user
|
||||||
|
|
||||||
|
Username, see each inbound for details.
|
||||||
|
|
||||||
|
#### protocol
|
||||||
|
|
||||||
|
Sniffed protocol, see [Sniff](/configuration/route/sniff/) for details.
|
||||||
|
|
||||||
|
#### network
|
||||||
|
|
||||||
|
`tcp` or `udp`.
|
||||||
|
|
||||||
|
#### domain
|
||||||
|
|
||||||
|
Match full domain.
|
||||||
|
|
||||||
|
#### domain_suffix
|
||||||
|
|
||||||
|
Match domain suffix.
|
||||||
|
|
||||||
|
#### domain_keyword
|
||||||
|
|
||||||
|
Match domain using keyword.
|
||||||
|
|
||||||
|
#### domain_regex
|
||||||
|
|
||||||
|
Match domain using regular expression.
|
||||||
|
|
||||||
|
#### geosite
|
||||||
|
|
||||||
|
Match geosite.
|
||||||
|
|
||||||
|
#### source_geoip
|
||||||
|
|
||||||
|
Match source geoip.
|
||||||
|
|
||||||
|
#### geoip
|
||||||
|
|
||||||
|
Match geoip.
|
||||||
|
|
||||||
|
#### source_ip_cidr
|
||||||
|
|
||||||
|
Match source ip cidr.
|
||||||
|
|
||||||
|
#### ip_cidr
|
||||||
|
|
||||||
|
Match ip cidr.
|
||||||
|
|
||||||
|
#### source_port
|
||||||
|
|
||||||
|
Match source port.
|
||||||
|
|
||||||
|
#### source_port_range
|
||||||
|
|
||||||
|
Match source port range.
|
||||||
|
|
||||||
|
#### port
|
||||||
|
|
||||||
|
Match port.
|
||||||
|
|
||||||
|
#### port_range
|
||||||
|
|
||||||
|
Match port range.
|
||||||
|
|
||||||
|
#### process_name
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
Match process name.
|
||||||
|
|
||||||
|
#### process_path
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
Match process path.
|
||||||
|
|
||||||
|
#### package_name
|
||||||
|
|
||||||
|
Match android package name.
|
||||||
|
|
||||||
|
#### user
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
Only supported on Linux.
|
||||||
|
|
||||||
|
Match user name.
|
||||||
|
|
||||||
|
#### user_id
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
Only supported on Linux.
|
||||||
|
|
||||||
|
Match user id.
|
||||||
|
|
||||||
|
#### clash_mode
|
||||||
|
|
||||||
|
Match Clash mode.
|
||||||
|
|
||||||
|
#### invert
|
||||||
|
|
||||||
|
Invert match result.
|
||||||
|
|
||||||
|
### Logical Fields
|
||||||
|
|
||||||
|
#### type
|
||||||
|
|
||||||
|
`logical`
|
||||||
|
|
||||||
|
#### mode
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
`and` or `or`
|
||||||
|
|
||||||
|
#### rules
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Included default rules.
|
240
docs/configuration/shared/sniff_override_rule.zh.md
Normal file
240
docs/configuration/shared/sniff_override_rule.zh.md
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"ip_version": 6,
|
||||||
|
"network": [
|
||||||
|
"tcp"
|
||||||
|
],
|
||||||
|
"auth_user": [
|
||||||
|
"usera",
|
||||||
|
"userb"
|
||||||
|
],
|
||||||
|
"protocol": [
|
||||||
|
"tls",
|
||||||
|
"http",
|
||||||
|
"quic"
|
||||||
|
],
|
||||||
|
"domain": [
|
||||||
|
"test.com"
|
||||||
|
],
|
||||||
|
"domain_suffix": [
|
||||||
|
".cn"
|
||||||
|
],
|
||||||
|
"domain_keyword": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"domain_regex": [
|
||||||
|
"^stun\\..+"
|
||||||
|
],
|
||||||
|
"geosite": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"source_geoip": [
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
"geoip": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"source_ip_cidr": [
|
||||||
|
"10.0.0.0/24"
|
||||||
|
],
|
||||||
|
"ip_cidr": [
|
||||||
|
"10.0.0.0/24"
|
||||||
|
],
|
||||||
|
"source_port": [
|
||||||
|
12345
|
||||||
|
],
|
||||||
|
"source_port_range": [
|
||||||
|
"1000:2000",
|
||||||
|
":3000",
|
||||||
|
"4000:"
|
||||||
|
],
|
||||||
|
"port": [
|
||||||
|
80,
|
||||||
|
443
|
||||||
|
],
|
||||||
|
"port_range": [
|
||||||
|
"1000:2000",
|
||||||
|
":3000",
|
||||||
|
"4000:"
|
||||||
|
],
|
||||||
|
"process_name": [
|
||||||
|
"curl"
|
||||||
|
],
|
||||||
|
"process_path": [
|
||||||
|
"/usr/bin/curl"
|
||||||
|
],
|
||||||
|
"package_name": [
|
||||||
|
"com.termux"
|
||||||
|
],
|
||||||
|
"user": [
|
||||||
|
"sekai"
|
||||||
|
],
|
||||||
|
"user_id": [
|
||||||
|
1000
|
||||||
|
],
|
||||||
|
"clash_mode": "direct",
|
||||||
|
"invert": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "logical",
|
||||||
|
"mode": "and",
|
||||||
|
"rules": [],
|
||||||
|
"invert": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
当内容只有一项时,可以忽略 JSON 数组 [] 标签。
|
||||||
|
|
||||||
|
### Default Fields
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
默认规则使用以下匹配逻辑:
|
||||||
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
|
||||||
|
(`port` || `port_range`) &&
|
||||||
|
(`source_geoip` || `source_ip_cidr`) &&
|
||||||
|
(`source_port` || `source_port_range`) &&
|
||||||
|
`other fields`
|
||||||
|
|
||||||
|
#### ip_version
|
||||||
|
|
||||||
|
4 或 6。
|
||||||
|
|
||||||
|
默认不限制。
|
||||||
|
|
||||||
|
#### auth_user
|
||||||
|
|
||||||
|
认证用户名,参阅入站设置。
|
||||||
|
|
||||||
|
#### protocol
|
||||||
|
|
||||||
|
探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。
|
||||||
|
|
||||||
|
#### network
|
||||||
|
|
||||||
|
`tcp` 或 `udp`。
|
||||||
|
|
||||||
|
#### domain
|
||||||
|
|
||||||
|
匹配完整域名。
|
||||||
|
|
||||||
|
#### domain_suffix
|
||||||
|
|
||||||
|
匹配域名后缀。
|
||||||
|
|
||||||
|
#### domain_keyword
|
||||||
|
|
||||||
|
匹配域名关键字。
|
||||||
|
|
||||||
|
#### domain_regex
|
||||||
|
|
||||||
|
匹配域名正则表达式。
|
||||||
|
|
||||||
|
#### geosite
|
||||||
|
|
||||||
|
匹配 GeoSite。
|
||||||
|
|
||||||
|
#### source_geoip
|
||||||
|
|
||||||
|
匹配源 GeoIP。
|
||||||
|
|
||||||
|
#### geoip
|
||||||
|
|
||||||
|
匹配 GeoIP。
|
||||||
|
|
||||||
|
#### source_ip_cidr
|
||||||
|
|
||||||
|
匹配源 IP CIDR。
|
||||||
|
|
||||||
|
#### ip_cidr
|
||||||
|
|
||||||
|
匹配 IP CIDR。
|
||||||
|
|
||||||
|
#### source_port
|
||||||
|
|
||||||
|
匹配源端口。
|
||||||
|
|
||||||
|
#### source_port_range
|
||||||
|
|
||||||
|
匹配源端口范围。
|
||||||
|
|
||||||
|
#### port
|
||||||
|
|
||||||
|
匹配端口。
|
||||||
|
|
||||||
|
#### port_range
|
||||||
|
|
||||||
|
匹配端口范围。
|
||||||
|
|
||||||
|
#### process_name
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
仅支持 Linux、Windows 和 macOS。
|
||||||
|
|
||||||
|
匹配进程名称。
|
||||||
|
|
||||||
|
#### process_path
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
仅支持 Linux、Windows 和 macOS.
|
||||||
|
|
||||||
|
匹配进程路径。
|
||||||
|
|
||||||
|
#### package_name
|
||||||
|
|
||||||
|
匹配 Android 应用包名。
|
||||||
|
|
||||||
|
#### user
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
仅支持 Linux.
|
||||||
|
|
||||||
|
匹配用户名。
|
||||||
|
|
||||||
|
#### user_id
|
||||||
|
|
||||||
|
!!! error ""
|
||||||
|
|
||||||
|
仅支持 Linux.
|
||||||
|
|
||||||
|
匹配用户 ID。
|
||||||
|
|
||||||
|
#### clash_mode
|
||||||
|
|
||||||
|
匹配 Clash 模式。
|
||||||
|
|
||||||
|
#### invert
|
||||||
|
|
||||||
|
反选匹配结果。
|
||||||
|
|
||||||
|
### 逻辑字段
|
||||||
|
|
||||||
|
#### type
|
||||||
|
|
||||||
|
`logical`
|
||||||
|
|
||||||
|
#### mode
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
`and` 或 `or`
|
||||||
|
|
||||||
|
#### rules
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
包括的默认规则。
|
@ -115,10 +115,11 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InboundOptions struct {
|
type InboundOptions struct {
|
||||||
SniffEnabled bool `json:"sniff,omitempty"`
|
SniffEnabled bool `json:"sniff,omitempty"`
|
||||||
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"`
|
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"`
|
||||||
SniffTimeout Duration `json:"sniff_timeout,omitempty"`
|
SniffOverrideRules []SniffOverrideRule `json:"sniff_override_rules,omitempty"`
|
||||||
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
|
SniffTimeout Duration `json:"sniff_timeout,omitempty"`
|
||||||
|
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListenOptions struct {
|
type ListenOptions struct {
|
||||||
|
97
option/rule_sniff_override.go
Normal file
97
option/rule_sniff_override.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type _SniffOverrideRule struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
DefaultOptions DefaultSniffOverrideRule `json:"-"`
|
||||||
|
LogicalOptions LogicalSniffOverrideRule `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SniffOverrideRule _SniffOverrideRule
|
||||||
|
|
||||||
|
func (r SniffOverrideRule) MarshalJSON() ([]byte, error) {
|
||||||
|
var v any
|
||||||
|
switch r.Type {
|
||||||
|
case C.RuleTypeDefault:
|
||||||
|
r.Type = ""
|
||||||
|
v = r.DefaultOptions
|
||||||
|
case C.RuleTypeLogical:
|
||||||
|
v = r.LogicalOptions
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown rule type: " + r.Type)
|
||||||
|
}
|
||||||
|
return MarshallObjects((_SniffOverrideRule)(r), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SniffOverrideRule) UnmarshalJSON(bytes []byte) error {
|
||||||
|
err := json.Unmarshal(bytes, (*_SniffOverrideRule)(r))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
switch r.Type {
|
||||||
|
case "", C.RuleTypeDefault:
|
||||||
|
r.Type = C.RuleTypeDefault
|
||||||
|
v = &r.DefaultOptions
|
||||||
|
case C.RuleTypeLogical:
|
||||||
|
v = &r.LogicalOptions
|
||||||
|
default:
|
||||||
|
return E.New("unknown rule type: " + r.Type)
|
||||||
|
}
|
||||||
|
err = UnmarshallExcluded(bytes, (*_SniffOverrideRule)(r), v)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "route rule")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultSniffOverrideRule struct {
|
||||||
|
IPVersion int `json:"ip_version,omitempty"`
|
||||||
|
Network Listable[string] `json:"network,omitempty"`
|
||||||
|
AuthUser Listable[string] `json:"auth_user,omitempty"`
|
||||||
|
Protocol Listable[string] `json:"protocol,omitempty"`
|
||||||
|
Domain Listable[string] `json:"domain,omitempty"`
|
||||||
|
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
|
||||||
|
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
|
||||||
|
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
|
||||||
|
Geosite Listable[string] `json:"geosite,omitempty"`
|
||||||
|
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
|
||||||
|
GeoIP Listable[string] `json:"geoip,omitempty"`
|
||||||
|
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||||
|
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
|
||||||
|
SourcePort Listable[uint16] `json:"source_port,omitempty"`
|
||||||
|
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
|
||||||
|
Port Listable[uint16] `json:"port,omitempty"`
|
||||||
|
PortRange Listable[string] `json:"port_range,omitempty"`
|
||||||
|
ProcessName Listable[string] `json:"process_name,omitempty"`
|
||||||
|
ProcessPath Listable[string] `json:"process_path,omitempty"`
|
||||||
|
PackageName Listable[string] `json:"package_name,omitempty"`
|
||||||
|
User Listable[string] `json:"user,omitempty"`
|
||||||
|
UserID Listable[int32] `json:"user_id,omitempty"`
|
||||||
|
ClashMode string `json:"clash_mode,omitempty"`
|
||||||
|
Invert bool `json:"invert,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r DefaultSniffOverrideRule) IsValid() bool {
|
||||||
|
var defaultValue DefaultSniffOverrideRule
|
||||||
|
defaultValue.Invert = r.Invert
|
||||||
|
return !reflect.DeepEqual(r, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogicalSniffOverrideRule struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Rules []DefaultSniffOverrideRule `json:"rules,omitempty"`
|
||||||
|
Invert bool `json:"invert,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r LogicalSniffOverrideRule) IsValid() bool {
|
||||||
|
return len(r.Rules) > 0 && common.All(r.Rules, DefaultSniffOverrideRule.IsValid)
|
||||||
|
}
|
@ -26,9 +26,9 @@ import (
|
|||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/outbound"
|
"github.com/sagernet/sing-box/outbound"
|
||||||
"github.com/sagernet/sing-box/transport/fakeip"
|
"github.com/sagernet/sing-box/transport/fakeip"
|
||||||
"github.com/sagernet/sing-dns"
|
dns "github.com/sagernet/sing-dns"
|
||||||
"github.com/sagernet/sing-tun"
|
tun "github.com/sagernet/sing-tun"
|
||||||
"github.com/sagernet/sing-vmess"
|
vmess "github.com/sagernet/sing-vmess"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
@ -50,6 +50,7 @@ type Router struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
dnsLogger log.ContextLogger
|
dnsLogger log.ContextLogger
|
||||||
|
overrideLogger log.ContextLogger
|
||||||
inboundByTag map[string]adapter.Inbound
|
inboundByTag map[string]adapter.Inbound
|
||||||
outbounds []adapter.Outbound
|
outbounds []adapter.Outbound
|
||||||
outboundByTag map[string]adapter.Outbound
|
outboundByTag map[string]adapter.Outbound
|
||||||
@ -101,6 +102,7 @@ func NewRouter(
|
|||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
logger: logFactory.NewLogger("router"),
|
logger: logFactory.NewLogger("router"),
|
||||||
dnsLogger: logFactory.NewLogger("dns"),
|
dnsLogger: logFactory.NewLogger("dns"),
|
||||||
|
overrideLogger: logFactory.NewLogger("override"),
|
||||||
outboundByTag: make(map[string]adapter.Outbound),
|
outboundByTag: make(map[string]adapter.Outbound),
|
||||||
rules: make([]adapter.Rule, 0, len(options.Rules)),
|
rules: make([]adapter.Rule, 0, len(options.Rules)),
|
||||||
dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)),
|
dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)),
|
||||||
@ -657,9 +659,12 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
|
|||||||
metadata.Protocol = sniffMetadata.Protocol
|
metadata.Protocol = sniffMetadata.Protocol
|
||||||
metadata.Domain = sniffMetadata.Domain
|
metadata.Domain = sniffMetadata.Domain
|
||||||
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
|
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
|
||||||
metadata.Destination = M.Socksaddr{
|
overrideFlag := r.matchSniffOverride(ctx, &metadata)
|
||||||
Fqdn: metadata.Domain,
|
if overrideFlag {
|
||||||
Port: metadata.Destination.Port,
|
metadata.Destination = M.Socksaddr{
|
||||||
|
Fqdn: metadata.Domain,
|
||||||
|
Port: metadata.Destination.Port,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if metadata.Domain != "" {
|
if metadata.Domain != "" {
|
||||||
@ -776,9 +781,12 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
|
|||||||
metadata.Protocol = sniffMetadata.Protocol
|
metadata.Protocol = sniffMetadata.Protocol
|
||||||
metadata.Domain = sniffMetadata.Domain
|
metadata.Domain = sniffMetadata.Domain
|
||||||
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
|
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
|
||||||
metadata.Destination = M.Socksaddr{
|
overrideFlag := r.matchSniffOverride(ctx, &metadata)
|
||||||
Fqdn: metadata.Domain,
|
if overrideFlag {
|
||||||
Port: metadata.Destination.Port,
|
metadata.Destination = M.Socksaddr{
|
||||||
|
Fqdn: metadata.Domain,
|
||||||
|
Port: metadata.Destination.Port,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if metadata.Domain != "" {
|
if metadata.Domain != "" {
|
||||||
|
31
route/router_sniff_override.go
Normal file
31
route/router_sniff_override.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Router) matchSniffOverride(ctx context.Context, metadata *adapter.InboundContext) bool {
|
||||||
|
rules := make([]adapter.SniffOverrideRule, 0, len(metadata.InboundOptions.SniffOverrideRules))
|
||||||
|
for i, sniffOverrideRuleOptions := range metadata.InboundOptions.SniffOverrideRules {
|
||||||
|
sniffOverrideRule, err := NewSniffOverrideRule(r, r.logger, sniffOverrideRuleOptions)
|
||||||
|
if err != nil {
|
||||||
|
E.Cause(err, "parse sniff_override rule[", i, "]")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rules = append(rules, sniffOverrideRule)
|
||||||
|
}
|
||||||
|
if len(rules) == 0 {
|
||||||
|
r.overrideLogger.DebugContext(ctx, "match all")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i, rule := range rules {
|
||||||
|
if rule.Match(metadata) {
|
||||||
|
r.overrideLogger.DebugContext(ctx, "match[", i, "] ", rule.String())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
202
route/rule_sniff_override.go
Normal file
202
route/rule_sniff_override.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSniffOverrideRule(router adapter.Router, logger log.ContextLogger, options option.SniffOverrideRule) (adapter.SniffOverrideRule, error) {
|
||||||
|
switch options.Type {
|
||||||
|
case "", C.RuleTypeDefault:
|
||||||
|
if !options.DefaultOptions.IsValid() {
|
||||||
|
return nil, E.New("missing conditions")
|
||||||
|
}
|
||||||
|
return NewDefaultSniffOverrideRule(router, logger, options.DefaultOptions)
|
||||||
|
case C.RuleTypeLogical:
|
||||||
|
if !options.LogicalOptions.IsValid() {
|
||||||
|
return nil, E.New("missing conditions")
|
||||||
|
}
|
||||||
|
return NewLogicalSniffOverrideRule(router, logger, options.LogicalOptions)
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown rule type: ", options.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.SniffOverrideRule = (*DefaultSniffOverrideRule)(nil)
|
||||||
|
|
||||||
|
type DefaultSniffOverrideRule struct {
|
||||||
|
abstractDefaultRule
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultSniffOverrideRule(router adapter.Router, logger log.ContextLogger, options option.DefaultSniffOverrideRule) (*DefaultSniffOverrideRule, error) {
|
||||||
|
rule := &DefaultSniffOverrideRule{
|
||||||
|
abstractDefaultRule: abstractDefaultRule{
|
||||||
|
invert: options.Invert,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if options.IPVersion > 0 {
|
||||||
|
switch options.IPVersion {
|
||||||
|
case 4, 6:
|
||||||
|
item := NewIPVersionItem(options.IPVersion == 6)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
default:
|
||||||
|
return nil, E.New("invalid ip version: ", options.IPVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(options.Network) > 0 {
|
||||||
|
item := NewNetworkItem(options.Network)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.AuthUser) > 0 {
|
||||||
|
item := NewAuthUserItem(options.AuthUser)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.Protocol) > 0 {
|
||||||
|
item := NewProtocolItem(options.Protocol)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
|
||||||
|
item := NewDomainItem(options.Domain, options.DomainSuffix)
|
||||||
|
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.DomainKeyword) > 0 {
|
||||||
|
item := NewDomainKeywordItem(options.DomainKeyword)
|
||||||
|
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.DomainRegex) > 0 {
|
||||||
|
item, err := NewDomainRegexItem(options.DomainRegex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "domain_regex")
|
||||||
|
}
|
||||||
|
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.Geosite) > 0 {
|
||||||
|
item := NewGeositeItem(router, logger, options.Geosite)
|
||||||
|
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.SourceGeoIP) > 0 {
|
||||||
|
item := NewGeoIPItem(router, logger, true, options.SourceGeoIP)
|
||||||
|
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.GeoIP) > 0 {
|
||||||
|
item := NewGeoIPItem(router, logger, false, options.GeoIP)
|
||||||
|
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.SourceIPCIDR) > 0 {
|
||||||
|
item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "source_ipcidr")
|
||||||
|
}
|
||||||
|
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.IPCIDR) > 0 {
|
||||||
|
item, err := NewIPCIDRItem(false, options.IPCIDR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "ipcidr")
|
||||||
|
}
|
||||||
|
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.SourcePort) > 0 {
|
||||||
|
item := NewPortItem(true, options.SourcePort)
|
||||||
|
rule.sourcePortItems = append(rule.sourcePortItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.SourcePortRange) > 0 {
|
||||||
|
item, err := NewPortRangeItem(true, options.SourcePortRange)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "source_port_range")
|
||||||
|
}
|
||||||
|
rule.sourcePortItems = append(rule.sourcePortItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.Port) > 0 {
|
||||||
|
item := NewPortItem(false, options.Port)
|
||||||
|
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.PortRange) > 0 {
|
||||||
|
item, err := NewPortRangeItem(false, options.PortRange)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "port_range")
|
||||||
|
}
|
||||||
|
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.ProcessName) > 0 {
|
||||||
|
item := NewProcessItem(options.ProcessName)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.ProcessPath) > 0 {
|
||||||
|
item := NewProcessPathItem(options.ProcessPath)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.PackageName) > 0 {
|
||||||
|
item := NewPackageNameItem(options.PackageName)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.User) > 0 {
|
||||||
|
item := NewUserItem(options.User)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if len(options.UserID) > 0 {
|
||||||
|
item := NewUserIDItem(options.UserID)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
if options.ClashMode != "" {
|
||||||
|
item := NewClashModeItem(router, options.ClashMode)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.SniffOverrideRule = (*LogicalSniffOverrideRule)(nil)
|
||||||
|
|
||||||
|
type LogicalSniffOverrideRule struct {
|
||||||
|
abstractLogicalRule
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogicalSniffOverrideRule(router adapter.Router, logger log.ContextLogger, options option.LogicalSniffOverrideRule) (*LogicalSniffOverrideRule, error) {
|
||||||
|
r := &LogicalSniffOverrideRule{
|
||||||
|
abstractLogicalRule: abstractLogicalRule{
|
||||||
|
rules: make([]adapter.Rule, len(options.Rules)),
|
||||||
|
invert: options.Invert,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
switch options.Mode {
|
||||||
|
case C.LogicalTypeAnd:
|
||||||
|
r.mode = C.LogicalTypeAnd
|
||||||
|
case C.LogicalTypeOr:
|
||||||
|
r.mode = C.LogicalTypeOr
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown logical mode: ", options.Mode)
|
||||||
|
}
|
||||||
|
for i, subRule := range options.Rules {
|
||||||
|
rule, err := NewDefaultSniffOverrideRule(router, logger, subRule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "sub rule[", i, "]")
|
||||||
|
}
|
||||||
|
r.rules[i] = rule
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user