Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 241ae92

Browse files
authored
feat: support DOMAIN-WILDCARD rule (#2124)
only support asterisk(*) and question mark(?)
1 parent 91985c1 commit 241ae92

File tree

6 files changed

+253
-0
lines changed

6 files changed

+253
-0
lines changed

component/wildcard/wildcard.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package wildcard
2+
3+
// copy and modified from https://github.com/IGLOU-EU/go-wildcard/tree/ce22b7af48e487517a492d3727d9386492043e21
4+
// which is licensed under OpenBSD's ISC-style license.
5+
// Copyright (c) 2023 Iglou.eu [email protected] Copyright (c) 2023 Adrien Kara [email protected]
6+
7+
func Match(pattern, s string) bool {
8+
if pattern == "" {
9+
return s == pattern
10+
}
11+
if pattern == "*" || s == pattern {
12+
return true
13+
}
14+
15+
return matchByString(pattern, s)
16+
}
17+
18+
func matchByString(pattern, s string) bool {
19+
var lastErotemeCluster byte
20+
var patternIndex, sIndex, lastStar, lastEroteme int
21+
patternLen := len(pattern)
22+
sLen := len(s)
23+
star := -1
24+
eroteme := -1
25+
26+
Loop:
27+
if sIndex >= sLen {
28+
goto checkPattern
29+
}
30+
31+
if patternIndex >= patternLen {
32+
if star != -1 {
33+
patternIndex = star + 1
34+
lastStar++
35+
sIndex = lastStar
36+
goto Loop
37+
}
38+
return false
39+
}
40+
switch pattern[patternIndex] {
41+
// Removed dot matching as it conflicts with dot in domains.
42+
// case '.':
43+
// It matches any single character. So, we don't need to check anything.
44+
case '?':
45+
// '?' matches one character. Store its position and match exactly one character in the string.
46+
eroteme = patternIndex
47+
lastEroteme = sIndex
48+
lastErotemeCluster = byte(s[sIndex])
49+
case '*':
50+
// '*' matches zero or more characters. Store its position and increment the pattern index.
51+
star = patternIndex
52+
lastStar = sIndex
53+
patternIndex++
54+
goto Loop
55+
default:
56+
// If the characters don't match, check if there was a previous '?' or '*' to backtrack.
57+
if pattern[patternIndex] != s[sIndex] {
58+
if eroteme != -1 {
59+
patternIndex = eroteme + 1
60+
sIndex = lastEroteme
61+
eroteme = -1
62+
goto Loop
63+
}
64+
65+
if star != -1 {
66+
patternIndex = star + 1
67+
lastStar++
68+
sIndex = lastStar
69+
goto Loop
70+
}
71+
72+
return false
73+
}
74+
75+
// If the characters match, check if it was not the same to validate the eroteme.
76+
if eroteme != -1 && lastErotemeCluster != byte(s[sIndex]) {
77+
eroteme = -1
78+
}
79+
}
80+
81+
patternIndex++
82+
sIndex++
83+
goto Loop
84+
85+
// Check if the remaining pattern characters are '*' or '?', which can match the end of the string.
86+
checkPattern:
87+
if patternIndex < patternLen {
88+
if pattern[patternIndex] == '*' {
89+
patternIndex++
90+
goto checkPattern
91+
} else if pattern[patternIndex] == '?' {
92+
if sIndex >= sLen {
93+
sIndex--
94+
}
95+
patternIndex++
96+
goto checkPattern
97+
}
98+
}
99+
100+
return patternIndex == patternLen
101+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package wildcard
2+
3+
/*
4+
* copy and modified from https://github.com/IGLOU-EU/go-wildcard/tree/ce22b7af48e487517a492d3727d9386492043e21
5+
*
6+
* Copyright (c) 2023 Iglou.eu <[email protected]>
7+
* Copyright (c) 2023 Adrien Kara <[email protected]>
8+
*
9+
* Licensed under the BSD 3-Clause License,
10+
*/
11+
12+
import (
13+
"testing"
14+
)
15+
16+
// TestMatch validates the logic of wild card matching,
17+
// it need to support '*', '?' and only validate for byte comparison
18+
// over string, not rune or grapheme cluster
19+
func TestMatch(t *testing.T) {
20+
cases := []struct {
21+
s string
22+
pattern string
23+
result bool
24+
}{
25+
{"", "", true},
26+
{"", "*", true},
27+
{"", "**", true},
28+
{"", "?", true},
29+
{"", "??", true},
30+
{"", "?*", true},
31+
{"", "*?", true},
32+
{"", ".", false},
33+
{"", ".?", false},
34+
{"", "?.", false},
35+
{"", ".*", false},
36+
{"", "*.", false},
37+
{"", "*.?", false},
38+
{"", "?.*", false},
39+
40+
{"a", "", false},
41+
{"a", "a", true},
42+
{"a", "*", true},
43+
{"a", "**", true},
44+
{"a", "?", true},
45+
{"a", "??", true},
46+
{"a", ".", false},
47+
{"a", ".?", false},
48+
{"a", "?.", false},
49+
{"a", ".*", false},
50+
{"a", "*.", false},
51+
{"a", "*.?", false},
52+
{"a", "?.*", false},
53+
54+
{"match the exact string", "match the exact string", true},
55+
{"do not match a different string", "this is a different string", false},
56+
{"Match The Exact String WITH DIFFERENT CASE", "Match The Exact String WITH DIFFERENT CASE", true},
57+
{"do not match a different string WITH DIFFERENT CASE", "this is a different string WITH DIFFERENT CASE", false},
58+
{"Do Not Match The Exact String With Different Case", "do not match the exact string with different case", false},
59+
{"match an emoji 😃", "match an emoji 😃", true},
60+
{"do not match because of different emoji 😃", "do not match because of different emoji 😄", false},
61+
{"🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", "🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", true},
62+
{"🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", "🦌🐇🦡🐿️🌲🌳🏰🌳🌲🌞🌧️❄️🌬️⛈️🔥🎄🎅🎁🎉🎊🥳👨‍👩‍👧‍👦💏👪💖👩‍💼🛀", false},
63+
64+
{"match a string with a *", "match a string *", true},
65+
{"match a string with a * at the beginning", "* at the beginning", true},
66+
{"match a string with two *", "match * with *", true},
67+
{"do not match a string with extra and a *", "do not match a string * with more", false},
68+
69+
{"match a string with a ?", "match ? string with a ?", true},
70+
{"match a string with a ? at the beginning", "?atch a string with a ? at the beginning", true},
71+
{"match a string with two ?", "match a string with two ??", true},
72+
{"match a optional char with a ?", "match a optional? char with a ?", true},
73+
{"match a optional char with a ?", "match a optional? char with a ?", true},
74+
{"do not match a string with extra and a ?", "do not match ? string with extra and a ? like this", false},
75+
76+
{"do not match a string with a .", "do not match . string with a .", false},
77+
{"do not match a string with a . at the beginning", "do not .atch a string with a . at the beginning", false},
78+
{"do not match a string with two .", "do not match a ..ring with two .", false},
79+
{"do not match a string with extra .", "do not match a string with extra ..", false},
80+
81+
{"A big brown fox jumps over the lazy dog, with all there wildcards friends", ". big?brown fox jumps over * wildcard. friend??", false},
82+
{"A big brown fox fails to jump over the lazy dog, with all there wildcards friends", ". big?brown fox jumps over * wildcard. friend??", false},
83+
84+
{"domain a.b.c", "domain a.b.c", true},
85+
{"domain adb.c", "domain a.b.c", false},
86+
{"aaaa", "a*a", true},
87+
}
88+
89+
for i, c := range cases {
90+
t.Run(c.s, func(t *testing.T) {
91+
result := Match(c.pattern, c.s)
92+
if c.result != result {
93+
t.Errorf("Test %d: Expected `%v`, found `%v`; With Pattern: `%s` and String: `%s`", i+1, c.result, result, c.pattern, c.s)
94+
}
95+
})
96+
}
97+
}
98+
99+
func FuzzMatch(f *testing.F) {
100+
f.Fuzz(func(t *testing.T, s string) {
101+
if !Match(string(s), string(s)) {
102+
t.Fatalf("%s does not match %s", s, s)
103+
}
104+
})
105+
}

constant/rule.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const (
66
DomainSuffix
77
DomainKeyword
88
DomainRegex
9+
DomainWildcard
910
GEOSITE
1011
GEOIP
1112
SrcGEOIP
@@ -48,6 +49,8 @@ func (rt RuleType) String() string {
4849
return "DomainKeyword"
4950
case DomainRegex:
5051
return "DomainRegex"
52+
case DomainWildcard:
53+
return "DomainWildcard"
5154
case GEOSITE:
5255
return "GeoSite"
5356
case GEOIP:

docs/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,7 @@ rules:
11191119
- DOMAIN-REGEX,^abc,DIRECT
11201120
- DOMAIN-SUFFIX,baidu.com,DIRECT
11211121
- DOMAIN-KEYWORD,google,ss1
1122+
- DOMAIN-WILDCARD,test.*.mihomo.com,ss1
11221123
- IP-CIDR,1.1.1.1/32,ss1
11231124
- IP-CIDR6,2409::/64,DIRECT
11241125
# 当满足条件是 TCP 或 UDP 流量时,使用名为 sub-rule-name1 的规则集

rules/common/domain_wildcard.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package common
2+
3+
import (
4+
"strings"
5+
6+
"github.com/metacubex/mihomo/component/wildcard"
7+
C "github.com/metacubex/mihomo/constant"
8+
)
9+
10+
type DomainWildcard struct {
11+
*Base
12+
pattern string
13+
adapter string
14+
}
15+
16+
func (dw *DomainWildcard) RuleType() C.RuleType {
17+
return C.DomainWildcard
18+
}
19+
20+
func (dw *DomainWildcard) Match(metadata *C.Metadata, _ C.RuleMatchHelper) (bool, string) {
21+
return wildcard.Match(dw.pattern, metadata.Host), dw.adapter
22+
}
23+
24+
func (dw *DomainWildcard) Adapter() string {
25+
return dw.adapter
26+
}
27+
28+
func (dw *DomainWildcard) Payload() string {
29+
return dw.pattern
30+
}
31+
32+
var _ C.Rule = (*DomainWildcard)(nil)
33+
34+
func NewDomainWildcard(pattern string, adapter string) (*DomainWildcard, error) {
35+
pattern = strings.ToLower(pattern)
36+
return &DomainWildcard{
37+
Base: &Base{},
38+
pattern: pattern,
39+
adapter: adapter,
40+
}, nil
41+
}

rules/parser.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string]
1919
parsed = RC.NewDomainKeyword(payload, target)
2020
case "DOMAIN-REGEX":
2121
parsed, parseErr = RC.NewDomainRegex(payload, target)
22+
case "DOMAIN-WILDCARD":
23+
parsed, parseErr = RC.NewDomainWildcard(payload, target)
2224
case "GEOSITE":
2325
parsed, parseErr = RC.NewGEOSITE(payload, target)
2426
case "GEOIP":

0 commit comments

Comments
 (0)