Add sniff_override_rules

This commit is contained in:
PuerNya 2023-08-22 19:41:50 +08:00
parent 9be5cd99c1
commit 32ec64f781
10 changed files with 856 additions and 15 deletions

View File

@ -6,8 +6,8 @@ import (
"net/netip"
"github.com/sagernet/sing-box/common/geoip"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
dns "github.com/sagernet/sing-dns"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network"
@ -84,6 +84,10 @@ type DNSRule interface {
RewriteTTL() *uint32
}
type SniffOverrideRule interface {
Rule
}
type InterfaceUpdateListener interface {
InterfaceUpdated()
}

View File

@ -9,6 +9,7 @@
"udp_fragment": false,
"sniff": false,
"sniff_override_destination": false,
"sniff_override_rules": [],
"sniff_timeout": "300ms",
"domain_strategy": "prefer_ipv6",
"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.
#### 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
Timeout for sniffing.

View File

@ -9,6 +9,7 @@
"udp_fragment": false,
"sniff": false,
"sniff_override_destination": false,
"sniff_override_rules": [],
"sniff_timeout": "300ms",
"domain_strategy": "prefer_ipv6",
"udp_timeout": 300,
@ -69,6 +70,12 @@
如果域名无效(如 Tor将不生效。
#### sniff_override_rules
根据规则选择处需要用探测出的域名覆盖目标地址的连接。
参阅 [Sniff Override Rule](/zh/configuration/shared/sniff_override_rules/)
#### sniff_timeout
探测超时时间。

View 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.

View 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
==必填==
包括的默认规则。

View File

@ -115,10 +115,11 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
}
type InboundOptions struct {
SniffEnabled bool `json:"sniff,omitempty"`
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"`
SniffTimeout Duration `json:"sniff_timeout,omitempty"`
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
SniffEnabled bool `json:"sniff,omitempty"`
SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"`
SniffOverrideRules []SniffOverrideRule `json:"sniff_override_rules,omitempty"`
SniffTimeout Duration `json:"sniff_timeout,omitempty"`
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
}
type ListenOptions struct {

View 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)
}

View File

@ -26,9 +26,9 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/outbound"
"github.com/sagernet/sing-box/transport/fakeip"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-vmess"
dns "github.com/sagernet/sing-dns"
tun "github.com/sagernet/sing-tun"
vmess "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
@ -50,6 +50,7 @@ type Router struct {
ctx context.Context
logger log.ContextLogger
dnsLogger log.ContextLogger
overrideLogger log.ContextLogger
inboundByTag map[string]adapter.Inbound
outbounds []adapter.Outbound
outboundByTag map[string]adapter.Outbound
@ -101,6 +102,7 @@ func NewRouter(
ctx: ctx,
logger: logFactory.NewLogger("router"),
dnsLogger: logFactory.NewLogger("dns"),
overrideLogger: logFactory.NewLogger("override"),
outboundByTag: make(map[string]adapter.Outbound),
rules: make([]adapter.Rule, 0, len(options.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.Domain = sniffMetadata.Domain
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
metadata.Destination = M.Socksaddr{
Fqdn: metadata.Domain,
Port: metadata.Destination.Port,
overrideFlag := r.matchSniffOverride(ctx, &metadata)
if overrideFlag {
metadata.Destination = M.Socksaddr{
Fqdn: metadata.Domain,
Port: metadata.Destination.Port,
}
}
}
if metadata.Domain != "" {
@ -776,9 +781,12 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
metadata.Protocol = sniffMetadata.Protocol
metadata.Domain = sniffMetadata.Domain
if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) {
metadata.Destination = M.Socksaddr{
Fqdn: metadata.Domain,
Port: metadata.Destination.Port,
overrideFlag := r.matchSniffOverride(ctx, &metadata)
if overrideFlag {
metadata.Destination = M.Socksaddr{
Fqdn: metadata.Domain,
Port: metadata.Destination.Port,
}
}
}
if metadata.Domain != "" {

View 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
}

View 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
}