diff --git a/common/pool/buffer_low_memory.go b/common/pool/buffer_low_memory.go new file mode 100644 index 00000000..24e18a75 --- /dev/null +++ b/common/pool/buffer_low_memory.go @@ -0,0 +1,15 @@ +//go:build with_low_memory + +package pool + +const ( + // io.Copy default buffer size is 32 KiB + // but the maximum packet size of vmess/shadowsocks is about 16 KiB + // so define a buffer of 20 KiB to reduce the memory of each TCP relay + RelayBufferSize = 16 * 1024 + + // RelayBufferSize uses 20KiB, but due to the allocator it will actually + // request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU + // set to 9000, so the UDP Buffer size set to 16Kib + UDPBufferSize = 8 * 1024 +) diff --git a/common/pool/buffer_standard.go b/common/pool/buffer_standard.go new file mode 100644 index 00000000..ff758700 --- /dev/null +++ b/common/pool/buffer_standard.go @@ -0,0 +1,15 @@ +//go:build !with_low_memory + +package pool + +const ( + // io.Copy default buffer size is 32 KiB + // but the maximum packet size of vmess/shadowsocks is about 16 KiB + // so define a buffer of 20 KiB to reduce the memory of each TCP relay + RelayBufferSize = 20 * 1024 + + // RelayBufferSize uses 20KiB, but due to the allocator it will actually + // request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU + // set to 9000, so the UDP Buffer size set to 16Kib + UDPBufferSize = 16 * 1024 +) diff --git a/common/pool/pool.go b/common/pool/pool.go index bee4887f..288ea467 100644 --- a/common/pool/pool.go +++ b/common/pool/pool.go @@ -1,17 +1,5 @@ package pool -const ( - // io.Copy default buffer size is 32 KiB - // but the maximum packet size of vmess/shadowsocks is about 16 KiB - // so define a buffer of 20 KiB to reduce the memory of each TCP relay - RelayBufferSize = 20 * 1024 - - // RelayBufferSize uses 20KiB, but due to the allocator it will actually - // request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU - // set to 9000, so the UDP Buffer size set to 16Kib - UDPBufferSize = 16 * 1024 -) - func Get(size int) []byte { return defaultAllocator.Get(size) } diff --git a/common/utils/strings.go b/common/utils/strings.go new file mode 100644 index 00000000..5d5ae596 --- /dev/null +++ b/common/utils/strings.go @@ -0,0 +1,9 @@ +package utils + +func Reverse(s string) string { + a := []rune(s) + for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { + a[i], a[j] = a[j], a[i] + } + return string(a) +} diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index 97d448ce..bf0b1bb3 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -28,8 +28,8 @@ var Dispatcher *SnifferDispatcher type SnifferDispatcher struct { enable bool sniffers map[sniffer.Sniffer]SnifferConfig - forceDomain *trie.DomainTrie[struct{}] - skipSNI *trie.DomainTrie[struct{}] + forceDomain *trie.DomainSet + skipSNI *trie.DomainSet skipList *cache.LruCache[string, uint8] rwMux sync.RWMutex forceDnsMapping bool @@ -37,7 +37,7 @@ type SnifferDispatcher struct { } func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) { - if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Search(metadata.Host) != nil || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) { + if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Has(metadata.Host) || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) { port, err := strconv.ParseUint(metadata.DstPort, 10, 16) if err != nil { log.Debugln("[Sniffer] Dst port is error") @@ -74,7 +74,7 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) return } else { - if sd.skipSNI.Search(host) != nil { + if sd.skipSNI.Has(host) { log.Debugln("[Sniffer] Skip sni[%s]", host) return } @@ -166,8 +166,8 @@ func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) { return &dispatcher, nil } -func NewSnifferDispatcher(snifferConfig map[sniffer.Type]SnifferConfig, forceDomain *trie.DomainTrie[struct{}], - skipSNI *trie.DomainTrie[struct{}], +func NewSnifferDispatcher(snifferConfig map[sniffer.Type]SnifferConfig, + forceDomain *trie.DomainSet, skipSNI *trie.DomainSet, forceDnsMapping bool, parsePureIp bool) (*SnifferDispatcher, error) { dispatcher := SnifferDispatcher{ enable: true, diff --git a/component/tls/config.go b/component/tls/config.go index 91b89f1d..b5b56591 100644 --- a/component/tls/config.go +++ b/component/tls/config.go @@ -15,7 +15,7 @@ import ( ) var trustCerts []*x509.Certificate - +var certPool *x509.CertPool var mutex sync.RWMutex var errNotMacth error = errors.New("certificate fingerprints do not match") @@ -40,10 +40,20 @@ func ResetCertificate() { } func getCertPool() *x509.CertPool { - certPool, err := x509.SystemCertPool() - if err == nil { - for _, cert := range trustCerts { - certPool.AddCert(cert) + if len(trustCerts) == 0 { + return nil + } + if certPool == nil { + mutex.Lock() + defer mutex.Unlock() + if certPool != nil { + return certPool + } + certPool, err := x509.SystemCertPool() + if err == nil { + for _, cert := range trustCerts { + certPool.AddCert(cert) + } } } return certPool diff --git a/component/trie/domain.go b/component/trie/domain.go index d9463c6e..86e5245a 100644 --- a/component/trie/domain.go +++ b/component/trie/domain.go @@ -25,7 +25,7 @@ func ValidAndSplitDomain(domain string) ([]string, bool) { if domain != "" && domain[len(domain)-1] == '.' { return nil, false } - + domain=strings.ToLower(domain) parts := strings.Split(domain, domainStep) if len(parts) == 1 { if parts[0] == "" { @@ -123,6 +123,30 @@ func (t *DomainTrie[T]) Optimize() { t.root.optimize() } +func (t *DomainTrie[T]) Foreach(print func(domain string, data T)) { + for key, data := range t.root.getChildren() { + recursion([]string{key}, data, print) + } +} + +func recursion[T any](items []string, node *Node[T], fn func(domain string, data T)) { + for key, data := range node.getChildren() { + newItems := append([]string{key}, items...) + if data != nil && data.inited { + domain := joinDomain(newItems) + if domain[0] == domainStepByte { + domain = complexWildcard + domain + } + fn(domain, data.Data()) + } + recursion(newItems, data, fn) + } +} + +func joinDomain(items []string) string { + return strings.Join(items, domainStep) +} + // New returns a new, empty Trie. func New[T any]() *DomainTrie[T] { return &DomainTrie[T]{root: newNode[T]()} diff --git a/component/trie/domain_test.go b/component/trie/domain_test.go index c54b3d3b..2dfd1c34 100644 --- a/component/trie/domain_test.go +++ b/component/trie/domain_test.go @@ -105,3 +105,23 @@ func TestTrie_WildcardBoundary(t *testing.T) { assert.NotNil(t, tree.Search("example.com")) } + +func TestTrie_Foreach(t *testing.T) { + tree := New[netip.Addr]() + domainList := []string{ + "google.com", + "stun.*.*.*", + "test.*.google.com", + "+.baidu.com", + "*.baidu.com", + "*.*.baidu.com", + } + for _, domain := range domainList { + tree.Insert(domain, localIP) + } + count := 0 + tree.Foreach(func(domain string, data netip.Addr) { + count++ + }) + assert.Equal(t, 7, count) +} diff --git a/component/trie/node.go b/component/trie/node.go index e19b40ac..3aa2bc7d 100644 --- a/component/trie/node.go +++ b/component/trie/node.go @@ -116,6 +116,18 @@ func (n *Node[T]) setData(data T) { n.inited = true } +func (n *Node[T]) getChildren() map[string]*Node[T] { + if n.childMap == nil { + if n.childNode != nil { + m := make(map[string]*Node[T]) + m[n.childStr] = n.childNode + return m + } + } else { + return n.childMap + } + return nil +} func (n *Node[T]) Data() T { return n.data } diff --git a/component/trie/set_test.go b/component/trie/set_test.go new file mode 100644 index 00000000..346bb31a --- /dev/null +++ b/component/trie/set_test.go @@ -0,0 +1,60 @@ +package trie_test + +import ( + "testing" + + "github.com/Dreamacro/clash/component/trie" + "github.com/stretchr/testify/assert" +) + +func TestDomain(t *testing.T) { + domainSet := []string{ + "baidu.com", + "google.com", + "www.google.com", + "test.a.net", + "test.a.oc", + } + set := trie.NewDomainSet(domainSet) + assert.NotNil(t, set) + assert.True(t, set.Has("test.a.net")) + assert.True(t, set.Has("google.com")) + assert.False(t, set.Has("www.baidu.com")) +} + +func TestDomainComplexWildcard(t *testing.T) { + domainSet := []string{ + "+.baidu.com", + "+.a.baidu.com", + "www.baidu.com", + "+.bb.baidu.com", + "test.a.net", + "test.a.oc", + "www.qq.com", + } + set := trie.NewDomainSet(domainSet) + assert.NotNil(t, set) + assert.False(t, set.Has("google.com")) + assert.True(t, set.Has("www.baidu.com")) + assert.True(t, set.Has("test.test.baidu.com")) +} + +func TestDomainWildcard(t *testing.T) { + domainSet := []string{ + "*.*.*.baidu.com", + "www.baidu.*", + "stun.*.*", + "*.*.qq.com", + "test.*.baidu.com", + } + set := trie.NewDomainSet(domainSet) + assert.NotNil(t, set) + assert.True(t, set.Has("www.baidu.com")) + assert.True(t, set.Has("test.test.baidu.com")) + assert.True(t, set.Has("test.test.qq.com")) + assert.True(t,set.Has("stun.ab.cd")) + assert.False(t, set.Has("test.baidu.com")) + assert.False(t,set.Has("www.google.com")) + assert.False(t, set.Has("test.qq.com")) + assert.False(t, set.Has("test.test.test.qq.com")) +} diff --git a/component/trie/sskv.go b/component/trie/sskv.go new file mode 100644 index 00000000..6a661a85 --- /dev/null +++ b/component/trie/sskv.go @@ -0,0 +1,178 @@ +package trie + +// Package succinct provides several succinct data types. +// Modify from https://github.com/openacid/succinct/blob/d4684c35d123f7528b14e03c24327231723db704/sskv.go + +import ( + "sort" + "strings" + + "github.com/Dreamacro/clash/common/utils" + "github.com/openacid/low/bitmap" +) + +const ( + complexWildcardByte = byte('+') + wildcardByte = byte('*') + domainStepByte = byte('.') +) + +type DomainSet struct { + leaves, labelBitmap []uint64 + labels []byte + ranks, selects []int32 +} + +// NewDomainSet creates a new *DomainSet struct, from a slice of sorted strings. +func NewDomainSet(keys []string) *DomainSet { + domainTrie := New[struct{}]() + for _, domain := range keys { + domainTrie.Insert(domain, struct{}{}) + } + reserveDomains := make([]string, 0, len(keys)) + domainTrie.Foreach(func(domain string, data struct{}) { + reserveDomains = append(reserveDomains, utils.Reverse(domain)) + }) + // ensure that the same prefix is continuous + // and according to the ascending sequence of length + sort.Strings(reserveDomains) + keys = reserveDomains + if len(keys) == 0 { + return nil + } + ss := &DomainSet{} + lIdx := 0 + + type qElt struct{ s, e, col int } + queue := []qElt{{0, len(keys), 0}} + for i := 0; i < len(queue); i++ { + elt := queue[i] + if elt.col == len(keys[elt.s]) { + elt.s++ + // a leaf node + setBit(&ss.leaves, i, 1) + } + + for j := elt.s; j < elt.e; { + + frm := j + + for ; j < elt.e && keys[j][elt.col] == keys[frm][elt.col]; j++ { + } + queue = append(queue, qElt{frm, j, elt.col + 1}) + ss.labels = append(ss.labels, keys[frm][elt.col]) + setBit(&ss.labelBitmap, lIdx, 0) + lIdx++ + } + setBit(&ss.labelBitmap, lIdx, 1) + lIdx++ + } + + ss.init() + return ss +} + +// Has query for a key and return whether it presents in the DomainSet. +func (ss *DomainSet) Has(key string) bool { + if ss == nil { + return false + } + key = utils.Reverse(key) + key = strings.ToLower(key) + // no more labels in this node + // skip character matching + // go to next level + nodeId, bmIdx := 0, 0 + type wildcardCursor struct { + bmIdx, index int + } + stack := make([]wildcardCursor, 0) + for i := 0; i < len(key); i++ { + RESTART: + c := key[i] + for ; ; bmIdx++ { + if getBit(ss.labelBitmap, bmIdx) != 0 { + if len(stack) > 0 { + cursor := stack[len(stack)-1] + stack = stack[0 : len(stack)-1] + // back wildcard and find next node + nextNodeId := countZeros(ss.labelBitmap, ss.ranks, cursor.bmIdx+1) + nextBmIdx := selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nextNodeId-1) + 1 + j := cursor.index + for ; j < len(key) && key[j] != domainStepByte; j++ { + } + if j == len(key) { + if getBit(ss.leaves, nextNodeId) != 0 { + return true + }else { + goto RESTART + } + } + for ; ; nextBmIdx++ { + if ss.labels[nextBmIdx-nextNodeId] == domainStepByte { + bmIdx = nextBmIdx + nodeId = nextNodeId + i = j + goto RESTART + } + } + } + return false + } + // handle wildcard for domain + if ss.labels[bmIdx-nodeId] == complexWildcardByte { + return true + } else if ss.labels[bmIdx-nodeId] == wildcardByte { + cursor := wildcardCursor{} + cursor.bmIdx = bmIdx + cursor.index = i + stack = append(stack, cursor) + } else if ss.labels[bmIdx-nodeId] == c { + break + } + } + nodeId = countZeros(ss.labelBitmap, ss.ranks, bmIdx+1) + bmIdx = selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nodeId-1) + 1 + } + + return getBit(ss.leaves, nodeId) != 0 + +} + +func setBit(bm *[]uint64, i int, v int) { + for i>>6 >= len(*bm) { + *bm = append(*bm, 0) + } + (*bm)[i>>6] |= uint64(v) << uint(i&63) +} + +func getBit(bm []uint64, i int) uint64 { + return bm[i>>6] & (1 << uint(i&63)) +} + +// init builds pre-calculated cache to speed up rank() and select() +func (ss *DomainSet) init() { + ss.selects, ss.ranks = bitmap.IndexSelect32R64(ss.labelBitmap) +} + +// countZeros counts the number of "0" in a bitmap before the i-th bit(excluding +// the i-th bit) on behalf of rank index. +// E.g.: +// +// countZeros("010010", 4) == 3 +// // 012345 +func countZeros(bm []uint64, ranks []int32, i int) int { + a, _ := bitmap.Rank64(bm, ranks, int32(i)) + return i - int(a) +} + +// selectIthOne returns the index of the i-th "1" in a bitmap, on behalf of rank +// and select indexes. +// E.g.: +// +// selectIthOne("010010", 1) == 4 +// // 012345 +func selectIthOne(bm []uint64, ranks, selects []int32, i int) int { + a, _ := bitmap.Select32R64(bm, selects, ranks, int32(i)) + return int(a) +} diff --git a/config/config.go b/config/config.go index c407aad5..d2378822 100644 --- a/config/config.go +++ b/config/config.go @@ -9,7 +9,6 @@ import ( "net/url" "os" "regexp" - "runtime" "strconv" "strings" "time" @@ -136,9 +135,8 @@ type IPTables struct { type Sniffer struct { Enable bool Sniffers map[snifferTypes.Type]SNIFF.SnifferConfig - Reverses *trie.DomainTrie[struct{}] - ForceDomain *trie.DomainTrie[struct{}] - SkipDomain *trie.DomainTrie[struct{}] + ForceDomain *trie.DomainSet + SkipDomain *trie.DomainSet ForceDnsMapping bool ParsePureIp bool } @@ -490,7 +488,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.Hosts = hosts - dnsCfg, err := parseDNS(rawCfg, hosts, rules) + dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders) if err != nil { return nil, err } @@ -822,8 +820,6 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, subRules map[s rules = append(rules, parsed) } - runtime.GC() - return rules, nil } @@ -983,7 +979,7 @@ func parsePureDNSServer(server string) string { } } } -func parseNameServerPolicy(nsPolicy map[string]any, preferH3 bool) (map[string][]dns.NameServer, error) { +func parseNameServerPolicy(nsPolicy map[string]any, ruleProviders map[string]providerTypes.RuleProvider, preferH3 bool) (map[string][]dns.NameServer, error) { policy := map[string][]dns.NameServer{} updatedPolicy := make(map[string]interface{}) re := regexp.MustCompile(`[a-zA-Z0-9\-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?`) @@ -998,6 +994,14 @@ func parseNameServerPolicy(nsPolicy map[string]any, preferH3 bool) (map[string][ newKey := "geosite:" + subkey updatedPolicy[newKey] = v } + } else if strings.Contains(k, "rule-set:") { + subkeys := strings.Split(k, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + for _, subkey := range subkeys { + newKey := "rule-set:" + subkey + updatedPolicy[newKey] = v + } } else if re.MatchString(k) { subkeys := strings.Split(k, ",") for _, subkey := range subkeys { @@ -1021,6 +1025,19 @@ func parseNameServerPolicy(nsPolicy map[string]any, preferH3 bool) (map[string][ if _, valid := trie.ValidAndSplitDomain(domain); !valid { return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain) } + if strings.HasPrefix(domain, "rule-set:") { + domainSetName := domain[9:] + if provider, ok := ruleProviders[domainSetName]; !ok { + return nil, fmt.Errorf("not found rule-set: %s", domainSetName) + } else { + switch provider.Behavior() { + case providerTypes.IPCIDR: + return nil, fmt.Errorf("rule provider type error, except domain,actual %s", provider.Behavior()) + case providerTypes.Classical: + log.Warnln("%s provider is %s, only matching it contain domain rule", provider.Name(), provider.Behavior()) + } + } + } policy[domain] = nameservers } @@ -1073,11 +1090,10 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM log.Infoln("Start initial GeoSite dns fallback filter `%s`, records: %d", country, recordsCount) } } - runtime.GC() return sites, nil } -func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule) (*DNS, error) { +func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) { cfg := rawCfg.DNS if cfg.Enable && len(cfg.NameServer) == 0 { return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") @@ -1104,7 +1120,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul return nil, err } - if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, cfg.PreferH3); err != nil { + if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, ruleProviders, cfg.PreferH3); err != nil { return nil, err } @@ -1324,24 +1340,8 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { } sniffer.Sniffers = loadSniffer - sniffer.ForceDomain = trie.New[struct{}]() - for _, domain := range snifferRaw.ForceDomain { - err := sniffer.ForceDomain.Insert(domain, struct{}{}) - if err != nil { - return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err) - } - } - sniffer.ForceDomain.Optimize() - - sniffer.SkipDomain = trie.New[struct{}]() - for _, domain := range snifferRaw.SkipDomain { - err := sniffer.SkipDomain.Insert(domain, struct{}{}) - if err != nil { - return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err) - } - } - sniffer.SkipDomain.Optimize() - + sniffer.ForceDomain = trie.NewDomainSet(snifferRaw.ForceDomain) + sniffer.SkipDomain = trie.NewDomainSet(snifferRaw.SkipDomain) return sniffer, nil } diff --git a/constant/features/low_memory.go b/constant/features/low_memory.go new file mode 100644 index 00000000..32d10fa6 --- /dev/null +++ b/constant/features/low_memory.go @@ -0,0 +1,5 @@ +package features + +func init() { + TAGS = append(TAGS, "with_low_memory") +} diff --git a/constant/features/no_doq.go b/constant/features/no_doq.go deleted file mode 100644 index c915272f..00000000 --- a/constant/features/no_doq.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build no_doq - -package features - -func init() { - TAGS = append(TAGS, "no_doq") -} diff --git a/constant/features/no_fake_tcp.go b/constant/features/no_fake_tcp.go new file mode 100644 index 00000000..f536a066 --- /dev/null +++ b/constant/features/no_fake_tcp.go @@ -0,0 +1,7 @@ +//go:build no_fake_tcp + +package features + +func init() { + TAGS = append(TAGS, "no_fake_tcp") +} diff --git a/constant/features/no_gvisor.go b/constant/features/no_gvisor.go deleted file mode 100644 index d0d5391a..00000000 --- a/constant/features/no_gvisor.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build no_gvisor - -package features - -func init() { - TAGS = append(TAGS, "no_gvisor") -} diff --git a/constant/features/with_gvisor.go b/constant/features/with_gvisor.go new file mode 100644 index 00000000..1b3417b3 --- /dev/null +++ b/constant/features/with_gvisor.go @@ -0,0 +1,7 @@ +//go:build with_gvisor + +package features + +func init() { + TAGS = append(TAGS, "with_gvisor") +} diff --git a/dns/resolver.go b/dns/resolver.go index c16aad40..b5a09fd0 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -16,6 +16,7 @@ import ( "github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/trie" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/log" D "github.com/miekg/dns" @@ -40,6 +41,11 @@ type geositePolicyRecord struct { inversedMatching bool } +type domainSetPolicyRecord struct { + domainSetProvider provider.RuleProvider + policy *Policy +} + type Resolver struct { ipv6 bool ipv6Timeout time.Duration @@ -51,6 +57,7 @@ type Resolver struct { group singleflight.Group lruCache *cache.LruCache[string, *D.Msg] policy *trie.DomainTrie[*Policy] + domainSetPolicy []domainSetPolicyRecord geositePolicy []geositePolicyRecord proxyServer []dnsClient } @@ -301,6 +308,12 @@ func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient { return geositeRecord.policy.GetData() } } + metadata := &C.Metadata{Host: domain} + for _, domainSetRecord := range r.domainSetPolicy { + if ok := domainSetRecord.domainSetProvider.Match(metadata); ok { + return domainSetRecord.policy.GetData() + } + } return nil } @@ -422,16 +435,18 @@ type FallbackFilter struct { } type Config struct { - Main, Fallback []NameServer - Default []NameServer - ProxyServer []NameServer - IPv6 bool - IPv6Timeout uint - EnhancedMode C.DNSMode - FallbackFilter FallbackFilter - Pool *fakeip.Pool - Hosts *trie.DomainTrie[resolver.HostValue] - Policy map[string][]NameServer + Main, Fallback []NameServer + Default []NameServer + ProxyServer []NameServer + IPv6 bool + IPv6Timeout uint + EnhancedMode C.DNSMode + FallbackFilter FallbackFilter + Pool *fakeip.Pool + Hosts *trie.DomainTrie[resolver.HostValue] + Policy map[string][]NameServer + DomainSetPolicy map[provider.RuleProvider][]NameServer + GeositePolicy map[router.DomainMatcher][]NameServer } func NewResolver(config Config) *Resolver { @@ -483,6 +498,14 @@ func NewResolver(config Config) *Resolver { } r.policy.Optimize() } + if len(config.DomainSetPolicy) > 0 { + for p, n := range config.DomainSetPolicy { + r.domainSetPolicy = append(r.domainSetPolicy, domainSetPolicyRecord{ + domainSetProvider: p, + policy: NewPolicy(transform(n, defaultResolver)), + }) + } + } fallbackIPFilters := []fallbackIPFilter{} if config.FallbackFilter.GeoIP { diff --git a/docs/config.yaml b/docs/config.yaml index 3ef09342..c2acc1d4 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -238,6 +238,7 @@ dns: - https://doh.pub/dns-query - https://dns.alidns.com/dns-query "www.baidu.com,+.google.cn": [223.5.5.5, https://dns.alidns.com/dns-query] + # "rule-set:global,dns": 8.8.8.8 # global,dns 为 rule-providers 中的名为 global 和 dns 的规则提供器名字,且 behavior 必须为 domain proxies: # socks5 - name: "socks" diff --git a/go.mod b/go.mod index c8eb6e10..a5e6220e 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,11 @@ require ( lukechampine.com/blake3 v1.1.7 ) +require ( + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect +) + require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect @@ -67,6 +72,7 @@ require ( github.com/mdlayher/socket v0.4.0 // indirect github.com/metacubex/gvisor v0.0.0-20230323114922-412956fb6a03 // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect + github.com/openacid/low v0.1.21 github.com/oschwald/maxminddb-golang v1.10.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect diff --git a/go.sum b/go.sum index f56aed7a..fe7a51f4 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -34,6 +35,7 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= @@ -75,6 +77,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= @@ -105,13 +109,21 @@ github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c= github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/mroth/weightedrand/v2 v2.0.0 h1:ADehnByWbliEDIazDAKFdBHoqgHSXAkgyKqM/9YsPoo= github.com/mroth/weightedrand/v2 v2.0.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= +github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= +github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= +github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I= +github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= @@ -148,6 +160,7 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -243,7 +256,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d h1:qp0AnQCvRCMlu9jBjtdbTaaEmThIgZOrbVyDEOcmKhQ= google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index 1b2ec572..8ca844d2 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -5,6 +5,7 @@ import ( "net/netip" "os" "runtime" + "strings" "sync" "github.com/Dreamacro/clash/adapter" @@ -91,7 +92,7 @@ func ApplyConfig(cfg *config.Config, force bool) { updateSniffer(cfg.Sniffer) updateHosts(cfg.Hosts) updateGeneral(cfg.General) - updateDNS(cfg.DNS, cfg.General.IPv6) + updateDNS(cfg.DNS, cfg.RuleProviders, cfg.General.IPv6) updateListeners(cfg.General, cfg.Listeners, force) updateIPTables(cfg) updateTun(cfg.General) @@ -104,7 +105,7 @@ func ApplyConfig(cfg *config.Config, force bool) { loadProxyProvider(cfg.Providers) updateProfile(cfg) loadRuleProvider(cfg.RuleProviders) - + runtime.GC() tunnel.OnRunning() log.SetLevel(cfg.General.LogLevel) @@ -175,10 +176,9 @@ func updateListeners(general *config.General, listeners map[string]C.InboundList } func updateExperimental(c *config.Config) { - runtime.GC() } -func updateDNS(c *config.DNS, generalIPv6 bool) { +func updateDNS(c *config.DNS, ruleProvider map[string]provider.RuleProvider, generalIPv6 bool) { if !c.Enable { resolver.DefaultResolver = nil resolver.DefaultHostMapper = nil @@ -186,7 +186,25 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { dns.ReCreateServer("", nil, nil) return } - + policy := make(map[string][]dns.NameServer) + domainSetPolicies := make(map[provider.RuleProvider][]dns.NameServer) + for key, nameservers := range c.NameServerPolicy { + temp := strings.Split(key, ":") + if len(temp) == 2 { + prefix := temp[0] + key := temp[1] + switch strings.ToLower(prefix) { + case "rule-set": + if p, ok := ruleProvider[key]; ok { + domainSetPolicies[p] = nameservers + } + case "geosite": + // TODO: + } + } else { + policy[key] = nameservers + } + } cfg := dns.Config{ Main: c.NameServer, Fallback: c.Fallback, @@ -202,9 +220,10 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { Domain: c.FallbackFilter.Domain, GeoSite: c.FallbackFilter.GeoSite, }, - Default: c.DefaultNameserver, - Policy: c.NameServerPolicy, - ProxyServer: c.ProxyServerNameserver, + Default: c.DefaultNameserver, + Policy: c.NameServerPolicy, + ProxyServer: c.ProxyServerNameserver, + DomainSetPolicy: domainSetPolicies, } r := dns.NewResolver(cfg) diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go index 21dee43c..f2daaf0c 100644 --- a/listener/sing_tun/dns.go +++ b/listener/sing_tun/dns.go @@ -110,7 +110,10 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. conn2 = nil }() for { - buff := buf.NewPacket() + // safe size which is 1232 from https://dnsflagday.net/2020/. + // so 2048 is enough + buff := buf.NewSize(2 * 1024) + _ = conn.SetReadDeadline(time.Now().Add(DefaultDnsReadTimeout)) dest, err := conn.ReadPacket(buff) if err != nil { buff.Release() diff --git a/rules/common/domain.go b/rules/common/domain.go index 6b3eba22..35a06a70 100644 --- a/rules/common/domain.go +++ b/rules/common/domain.go @@ -1,7 +1,6 @@ package common import ( - "golang.org/x/net/idna" "strings" C "github.com/Dreamacro/clash/constant" @@ -11,7 +10,6 @@ type Domain struct { *Base domain string adapter string - isIDNA bool } func (d *Domain) RuleType() C.RuleType { @@ -27,20 +25,14 @@ func (d *Domain) Adapter() string { } func (d *Domain) Payload() string { - domain := d.domain - if d.isIDNA { - domain, _ = idna.ToUnicode(domain) - } - return domain + return d.domain } func NewDomain(domain string, adapter string) *Domain { - actualDomain, _ := idna.ToASCII(domain) return &Domain{ Base: &Base{}, - domain: strings.ToLower(actualDomain), + domain: strings.ToLower(domain), adapter: adapter, - isIDNA: actualDomain != domain, } } diff --git a/rules/common/domain_keyword.go b/rules/common/domain_keyword.go index 94d2a949..d945f200 100644 --- a/rules/common/domain_keyword.go +++ b/rules/common/domain_keyword.go @@ -1,7 +1,6 @@ package common import ( - "golang.org/x/net/idna" "strings" C "github.com/Dreamacro/clash/constant" @@ -11,7 +10,6 @@ type DomainKeyword struct { *Base keyword string adapter string - isIDNA bool } func (dk *DomainKeyword) RuleType() C.RuleType { @@ -28,20 +26,14 @@ func (dk *DomainKeyword) Adapter() string { } func (dk *DomainKeyword) Payload() string { - keyword := dk.keyword - if dk.isIDNA { - keyword, _ = idna.ToUnicode(keyword) - } - return keyword + return dk.keyword } func NewDomainKeyword(keyword string, adapter string) *DomainKeyword { - actualDomainKeyword, _ := idna.ToASCII(keyword) return &DomainKeyword{ Base: &Base{}, - keyword: strings.ToLower(actualDomainKeyword), + keyword: strings.ToLower(keyword), adapter: adapter, - isIDNA: keyword != actualDomainKeyword, } } diff --git a/rules/common/domain_suffix.go b/rules/common/domain_suffix.go index 4bdc2e2e..b13036a3 100644 --- a/rules/common/domain_suffix.go +++ b/rules/common/domain_suffix.go @@ -1,7 +1,6 @@ package common import ( - "golang.org/x/net/idna" "strings" C "github.com/Dreamacro/clash/constant" @@ -11,7 +10,6 @@ type DomainSuffix struct { *Base suffix string adapter string - isIDNA bool } func (ds *DomainSuffix) RuleType() C.RuleType { @@ -28,20 +26,14 @@ func (ds *DomainSuffix) Adapter() string { } func (ds *DomainSuffix) Payload() string { - suffix := ds.suffix - if ds.isIDNA { - suffix, _ = idna.ToUnicode(suffix) - } - return suffix + return ds.suffix } func NewDomainSuffix(suffix string, adapter string) *DomainSuffix { - actualDomainSuffix, _ := idna.ToASCII(suffix) return &DomainSuffix{ Base: &Base{}, - suffix: strings.ToLower(actualDomainSuffix), + suffix: strings.ToLower(suffix), adapter: adapter, - isIDNA: suffix != actualDomainSuffix, } } diff --git a/rules/common/network_type.go b/rules/common/network_type.go index fb6b5077..1184ba89 100644 --- a/rules/common/network_type.go +++ b/rules/common/network_type.go @@ -21,10 +21,8 @@ func NewNetworkType(network, adapter string) (*NetworkType, error) { switch strings.ToUpper(network) { case "TCP": ntType.network = C.TCP - break case "UDP": ntType.network = C.UDP - break default: return nil, fmt.Errorf("unsupported network type, only TCP/UDP") } diff --git a/rules/provider/domain_strategy.go b/rules/provider/domain_strategy.go index add64e76..0b2a5d3c 100644 --- a/rules/provider/domain_strategy.go +++ b/rules/provider/domain_strategy.go @@ -3,13 +3,11 @@ package provider import ( "github.com/Dreamacro/clash/component/trie" C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "golang.org/x/net/idna" ) type domainStrategy struct { count int - domainRules *trie.DomainTrie[struct{}] + domainRules *trie.DomainSet } func (d *domainStrategy) ShouldFindProcess() bool { @@ -17,7 +15,7 @@ func (d *domainStrategy) ShouldFindProcess() bool { } func (d *domainStrategy) Match(metadata *C.Metadata) bool { - return d.domainRules != nil && d.domainRules.Search(metadata.RuleHost()) != nil + return d.domainRules != nil && d.domainRules.Has(metadata.RuleHost()) } func (d *domainStrategy) Count() int { @@ -29,21 +27,9 @@ func (d *domainStrategy) ShouldResolveIP() bool { } func (d *domainStrategy) OnUpdate(rules []string) { - domainTrie := trie.New[struct{}]() - count := 0 - for _, rule := range rules { - actualDomain, _ := idna.ToASCII(rule) - err := domainTrie.Insert(actualDomain, struct{}{}) - if err != nil { - log.Warnln("invalid domain:[%s]", rule) - } else { - count++ - } - } - domainTrie.Optimize() - + domainTrie := trie.NewDomainSet(rules) d.domainRules = domainTrie - d.count = count + d.count = len(rules) } func NewDomainStrategy() *domainStrategy { diff --git a/transport/hysteria/conns/faketcp/tcp_linux.go b/transport/hysteria/conns/faketcp/tcp_linux.go index dadb0912..cdee9fda 100644 --- a/transport/hysteria/conns/faketcp/tcp_linux.go +++ b/transport/hysteria/conns/faketcp/tcp_linux.go @@ -1,5 +1,5 @@ -//go:build linux -// +build linux +//go:build linux && !no_fake_tcp +// +build linux,!no_fake_tcp package faketcp diff --git a/transport/hysteria/conns/faketcp/tcp_stub.go b/transport/hysteria/conns/faketcp/tcp_stub.go index 9bc55077..9f9ff97d 100644 --- a/transport/hysteria/conns/faketcp/tcp_stub.go +++ b/transport/hysteria/conns/faketcp/tcp_stub.go @@ -1,5 +1,5 @@ -//go:build !linux -// +build !linux +//go:build !linux || no_fake_tcp +// +build !linux no_fake_tcp package faketcp