diff --git a/common/srs/binary.go b/common/srs/binary.go index 7e4f402b39..489dc22e2f 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -37,6 +37,7 @@ const ( ruleItemWIFISSID ruleItemWIFIBSSID ruleItemAdGuardDomain + ruleItemProcessPathRegex ruleItemFinal uint8 = 0xFF ) @@ -207,6 +208,8 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea rule.ProcessName, err = readRuleItemString(reader) case ruleItemProcessPath: rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemProcessPathRegex: + rule.ProcessPathRegex, err = readRuleItemString(reader) case ruleItemPackageName: rule.PackageName, err = readRuleItemString(reader) case ruleItemWIFISSID: @@ -326,6 +329,12 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen return err } } + if len(rule.ProcessPathRegex) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPathRegex, rule.ProcessPathRegex) + if err != nil { + return err + } + } if len(rule.PackageName) > 0 { err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) if err != nil { diff --git a/docs/clients/android/features.md b/docs/clients/android/features.md index 8fe84add26..f7f2caeec4 100644 --- a/docs/clients/android/features.md +++ b/docs/clients/android/features.md @@ -40,6 +40,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService. |-----------------------|------------------|-----------------------------------| | `process_name` | :material-close: | No permission | | `process_path` | :material-close: | No permission | +| `process_path_regex` | :material-close: | No permission | | `package_name` | :material-check: | / | | `user` | :material-close: | Use `package_name` instead | | `user_id` | :material-close: | Use `package_name` instead | diff --git a/docs/clients/apple/features.md b/docs/clients/apple/features.md index 7d419103b5..d638517322 100644 --- a/docs/clients/apple/features.md +++ b/docs/clients/apple/features.md @@ -42,6 +42,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension |-----------------------|------------------|-----------------------| | `process_name` | :material-close: | No permission | | `process_path` | :material-close: | No permission | +| `process_path_regex` | :material-close: | No permission | | `package_name` | :material-close: | / | | `user` | :material-close: | No permission | | `user_id` | :material-close: | No permission | diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 6b3d65194f..03ca66bcc3 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -6,7 +6,8 @@ icon: material/new-box :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) - :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [process_path_regex](#process_path_regex) !!! quote "Changes in sing-box 1.9.0" @@ -103,6 +104,9 @@ icon: material/new-box "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -268,6 +272,16 @@ Match process name. Match process path. +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + #### package_name Match android package name. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index eecddb3816..b484cbed6b 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -6,7 +6,8 @@ icon: material/new-box :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) - :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [process_path_regex](#process_path_regex) !!! quote "sing-box 1.9.0 中的更改" @@ -103,6 +104,9 @@ icon: material/new-box "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -266,6 +270,16 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配进程路径。 +#### process_path_regex + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +使用正则表达式匹配进程路径。 + #### package_name 匹配 Android 应用包名。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 8e61e51df2..91b432df1a 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -7,6 +7,7 @@ icon: material/alert-decagram :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [process_path_regex](#process_path_regex) !!! quote "Changes in sing-box 1.8.0" @@ -101,6 +102,9 @@ icon: material/alert-decagram "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -277,6 +281,16 @@ Match process name. Match process path. +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + #### package_name Match android package name. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 68e66cf5c9..a93ce5e53d 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -6,7 +6,8 @@ icon: material/alert-decagram :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) - :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [process_path_regex](#process_path_regex) + !!! quote "sing-box 1.8.0 中的更改" @@ -99,6 +100,9 @@ icon: material/alert-decagram "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -275,6 +279,16 @@ icon: material/alert-decagram 匹配进程路径。 +#### process_path_regex + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +使用正则表达式匹配进程路径。 + #### package_name 匹配 Android 应用包名。 diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md index e766904b5f..891b127840 100644 --- a/docs/configuration/rule-set/headless-rule.md +++ b/docs/configuration/rule-set/headless-rule.md @@ -57,6 +57,9 @@ "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -160,6 +163,16 @@ Match process name. Match process path. +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + #### package_name Match android package name. diff --git a/option/rule.go b/option/rule.go index 5c69ef00e3..5f15645c9b 100644 --- a/option/rule.go +++ b/option/rule.go @@ -88,6 +88,7 @@ type _DefaultRule struct { PortRange Listable[string] `json:"port_range,omitempty"` ProcessName Listable[string] `json:"process_name,omitempty"` ProcessPath Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex Listable[string] `json:"process_path_regex,omitempty"` PackageName Listable[string] `json:"package_name,omitempty"` User Listable[string] `json:"user,omitempty"` UserID Listable[int32] `json:"user_id,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index 2afe245ea4..2e03b4252e 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -88,6 +88,7 @@ type _DefaultDNSRule struct { PortRange Listable[string] `json:"port_range,omitempty"` ProcessName Listable[string] `json:"process_name,omitempty"` ProcessPath Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex Listable[string] `json:"process_path_regex,omitempty"` PackageName Listable[string] `json:"package_name,omitempty"` User Listable[string] `json:"user,omitempty"` UserID Listable[int32] `json:"user_id,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go index a83b2c47b6..b6ec113e35 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -144,24 +144,25 @@ func (r HeadlessRule) IsValid() bool { } type DefaultHeadlessRule struct { - QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,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"` - 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"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,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"` + 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"` + ProcessPathRegex Listable[string] `json:"process_path_regex,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` diff --git a/route/rule_default.go b/route/rule_default.go index 4bbc2b6b2e..40b93e5f96 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -179,6 +179,14 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + 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) diff --git a/route/rule_dns.go b/route/rule_dns.go index 1b79d30b25..616f956aaf 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -183,6 +183,14 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + 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) diff --git a/route/rule_headless.go b/route/rule_headless.go index d537df572c..23a98c7237 100644 --- a/route/rule_headless.go +++ b/route/rule_headless.go @@ -123,6 +123,14 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + 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) diff --git a/route/rule_item_process_path_regex.go b/route/rule_item_process_path_regex.go new file mode 100644 index 0000000000..01b2723cc6 --- /dev/null +++ b/route/rule_item_process_path_regex.go @@ -0,0 +1,54 @@ +package route + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*ProcessPathRegexItem)(nil) + +type ProcessPathRegexItem struct { + matchers []*regexp.Regexp + description string +} + +func NewProcessPathRegexItem(expressions []string) (*ProcessPathRegexItem, error) { + matchers := make([]*regexp.Regexp, 0, len(expressions)) + for i, regex := range expressions { + matcher, err := regexp.Compile(regex) + if err != nil { + return nil, E.Cause(err, "parse expression ", i) + } + matchers = append(matchers, matcher) + } + description := "process_path_regex=" + eLen := len(expressions) + if eLen == 1 { + description += expressions[0] + } else if eLen > 3 { + description += F.ToString("[", strings.Join(expressions[:3], " "), "]") + } else { + description += F.ToString("[", strings.Join(expressions, " "), "]") + } + return &ProcessPathRegexItem{matchers, description}, nil +} + +func (r *ProcessPathRegexItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" { + return false + } + for _, matcher := range r.matchers { + if matcher.MatchString(metadata.ProcessInfo.ProcessPath) { + return true + } + } + return false +} + +func (r *ProcessPathRegexItem) String() string { + return r.description +}