diff --git a/adapter/router.go b/adapter/router.go index df74ee0a..56b48458 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -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() } diff --git a/docs/configuration/shared/listen.md b/docs/configuration/shared/listen.md index d4a0e58e..bd526665 100644 --- a/docs/configuration/shared/listen.md +++ b/docs/configuration/shared/listen.md @@ -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. diff --git a/docs/configuration/shared/listen.zh.md b/docs/configuration/shared/listen.zh.md index b25ce295..7e589770 100644 --- a/docs/configuration/shared/listen.zh.md +++ b/docs/configuration/shared/listen.zh.md @@ -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 探测超时时间。 diff --git a/docs/configuration/shared/sniff_override_rule.md b/docs/configuration/shared/sniff_override_rule.md new file mode 100644 index 00000000..782e4dfa --- /dev/null +++ b/docs/configuration/shared/sniff_override_rule.md @@ -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. diff --git a/docs/configuration/shared/sniff_override_rule.zh.md b/docs/configuration/shared/sniff_override_rule.zh.md new file mode 100644 index 00000000..93fd99c1 --- /dev/null +++ b/docs/configuration/shared/sniff_override_rule.zh.md @@ -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 + +==必填== + +包括的默认规则。 \ No newline at end of file diff --git a/option/inbound.go b/option/inbound.go index 64b45e6c..1c980c73 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -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 { diff --git a/option/rule_sniff_override.go b/option/rule_sniff_override.go new file mode 100644 index 00000000..bd85f2e8 --- /dev/null +++ b/option/rule_sniff_override.go @@ -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) +} diff --git a/route/router.go b/route/router.go index f2926f87..271d22b0 100644 --- a/route/router.go +++ b/route/router.go @@ -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 != "" { diff --git a/route/router_sniff_override.go b/route/router_sniff_override.go new file mode 100644 index 00000000..2a1fbf16 --- /dev/null +++ b/route/router_sniff_override.go @@ -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 +} diff --git a/route/rule_sniff_override.go b/route/rule_sniff_override.go new file mode 100644 index 00000000..932ec15c --- /dev/null +++ b/route/rule_sniff_override.go @@ -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 +}