diff --git a/option/route.go b/option/route.go index 4a3fdb9f..7c07bf88 100644 --- a/option/route.go +++ b/option/route.go @@ -83,6 +83,7 @@ type DefaultRule struct { 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"` SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` GeoIP Listable[string] `json:"geoip,omitempty"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` @@ -108,6 +109,7 @@ func (r DefaultRule) Equals(other DefaultRule) bool { common.ComparableSliceEquals(r.Domain, other.Domain) && common.ComparableSliceEquals(r.DomainSuffix, other.DomainSuffix) && common.ComparableSliceEquals(r.DomainKeyword, other.DomainKeyword) && + common.ComparableSliceEquals(r.DomainRegex, other.DomainRegex) && common.ComparableSliceEquals(r.SourceGeoIP, other.SourceGeoIP) && common.ComparableSliceEquals(r.GeoIP, other.GeoIP) && common.ComparableSliceEquals(r.SourceIPCIDR, other.SourceIPCIDR) && diff --git a/route/rule.go b/route/rule.go index 225ea210..38a9c737 100644 --- a/route/rule.go +++ b/route/rule.go @@ -83,6 +83,13 @@ func NewDefaultRule(router adapter.Router, logger log.Logger, options option.Def if len(options.DomainKeyword) > 0 { rule.items = append(rule.items, NewDomainKeywordItem(options.DomainKeyword)) } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.items = append(rule.items, item) + } if len(options.SourceGeoIP) > 0 { rule.items = append(rule.items, NewGeoIPItem(router, logger, true, options.SourceGeoIP)) } @@ -92,14 +99,14 @@ func NewDefaultRule(router adapter.Router, logger log.Logger, options option.Def if len(options.SourceIPCIDR) > 0 { item, err := NewIPCIDRItem(true, options.SourceIPCIDR) if err != nil { - return nil, err + return nil, E.Cause(err, "source_ipcidr") } rule.items = append(rule.items, item) } if len(options.IPCIDR) > 0 { item, err := NewIPCIDRItem(false, options.IPCIDR) if err != nil { - return nil, err + return nil, E.Cause(err, "ipcidr") } rule.items = append(rule.items, item) } diff --git a/route/rule_cidr.go b/route/rule_cidr.go index bc91c8f5..1d3bfd3c 100644 --- a/route/rule_cidr.go +++ b/route/rule_cidr.go @@ -6,6 +6,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) @@ -18,10 +19,10 @@ type IPCIDRItem struct { func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { prefixes := make([]netip.Prefix, 0, len(prefixStrings)) - for _, prefixString := range prefixStrings { + for i, prefixString := range prefixStrings { prefix, err := netip.ParsePrefix(prefixString) if err != nil { - return nil, err + return nil, E.Cause(err, "parse prefix [", i, "]") } prefixes = append(prefixes, prefix) } diff --git a/route/rule_domain.go b/route/rule_domain.go index ffb1b114..dfdee095 100644 --- a/route/rule_domain.go +++ b/route/rule_domain.go @@ -11,8 +11,8 @@ import ( var _ RuleItem = (*DomainItem)(nil) type DomainItem struct { - description string matcher *domain.Matcher + description string } func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { @@ -41,8 +41,8 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } return &DomainItem{ - description, domain.NewMatcher(domains, domainSuffixes), + description, } } diff --git a/route/rule_domain_regex.go b/route/rule_domain_regex.go new file mode 100644 index 00000000..ae04b4d9 --- /dev/null +++ b/route/rule_domain_regex.go @@ -0,0 +1,60 @@ +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 = (*DomainRegexItem)(nil) + +type DomainRegexItem struct { + matchers []*regexp.Regexp + description string +} + +func NewDomainRegexItem(expressions []string) (*DomainRegexItem, 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 := "domain_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 &DomainRegexItem{matchers, description}, nil +} + +func (r *DomainRegexItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost == "" { + return false + } + for _, matcher := range r.matchers { + if matcher.MatchString(domainHost) { + return true + } + } + return false +} + +func (r *DomainRegexItem) String() string { + return r.description +}