Files
dns-server/shield/manager.go
T
Alex Yang 073f1961b1 更新web
2026-01-21 09:46:49 +08:00

1595 lines
47 KiB
Go

package shield
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"time"
"dns-server/config"
"dns-server/logger"
)
// ShieldStatsData 用于持久化的Shield统计数据
type ShieldStatsData struct {
BlockedDomainsCount map[string]int `json:"blockedDomainsCount"`
ResolvedDomainsCount map[string]int `json:"resolvedDomainsCount"`
LastSaved time.Time `json:"lastSaved"`
}
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
type regexRule struct {
pattern *regexp.Regexp
original string
isLocal bool // 是否为自定义规则
source string // 规则来源
}
// ShieldManager 屏蔽管理器
type ShieldManager struct {
config *config.ShieldConfig
domainRules map[string]bool
domainExceptions map[string]bool
domainRulesIsLocal map[string]bool // 标记域名规则是否为自定义规则
domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为自定义规则
domainRulesSource map[string]string // 标记域名规则来源
domainExceptionsSource map[string]string // 标记域名排除规则来源
domainRulesOriginal map[string]string // 存储域名规则的原始字符串
domainExceptionsOriginal map[string]string // 存储域名排除规则的原始字符串
regexRules []regexRule
regexExceptions []regexRule
hostsMap map[string]string
blockedDomainsCount map[string]int
resolvedDomainsCount map[string]int
rulesMutex sync.RWMutex
updateCtx context.Context
updateCancel context.CancelFunc
updateRunning bool
localRulesCount int // 自定义规则数量
remoteRulesCount int // 远程规则数量
}
// NewShieldManager 创建屏蔽管理器实例
func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
ctx, cancel := context.WithCancel(context.Background())
manager := &ShieldManager{
config: config,
domainRules: make(map[string]bool),
domainExceptions: make(map[string]bool),
domainRulesIsLocal: make(map[string]bool),
domainExceptionsIsLocal: make(map[string]bool),
domainRulesSource: make(map[string]string),
domainExceptionsSource: make(map[string]string),
domainRulesOriginal: make(map[string]string),
domainExceptionsOriginal: make(map[string]string),
regexRules: []regexRule{},
regexExceptions: []regexRule{},
hostsMap: make(map[string]string),
blockedDomainsCount: make(map[string]int),
resolvedDomainsCount: make(map[string]int),
updateCtx: ctx,
updateCancel: cancel,
localRulesCount: 0,
remoteRulesCount: 0,
}
// 加载已保存的计数数据
manager.loadStatsData()
return manager
}
// LoadRules 加载屏蔽规则
func (m *ShieldManager) LoadRules() error {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
// 清空现有规则
m.domainRules = make(map[string]bool)
m.domainExceptions = make(map[string]bool)
m.domainRulesIsLocal = make(map[string]bool)
m.domainExceptionsIsLocal = make(map[string]bool)
m.domainRulesSource = make(map[string]string)
m.domainExceptionsSource = make(map[string]string)
m.domainRulesOriginal = make(map[string]string)
m.domainExceptionsOriginal = make(map[string]string)
m.regexRules = []regexRule{}
m.regexExceptions = []regexRule{}
m.hostsMap = make(map[string]string)
m.localRulesCount = 0
m.remoteRulesCount = 0
// 保留计数数据,不随规则重新加载而清空
// 加载自定义规则文件
if err := m.loadLocalRules(); err != nil {
logger.Error("加载自定义规则失败", "error", err)
// 继续执行,不返回错误
}
// 加载远程规则
if err := m.loadRemoteRules(); err != nil {
logger.Error("加载远程规则失败", "error", err)
// 继续执行,不返回错误
}
// 加载hosts文件
if err := m.loadHosts(); err != nil {
logger.Error("加载hosts文件失败", "error", err)
// 继续执行,不返回错误
}
logger.Info(fmt.Sprintf("规则加载完成,域名规则: %d, 排除规则: %d, 正则规则: %d, hosts规则: %d",
len(m.domainRules), len(m.domainExceptions), len(m.regexRules), len(m.hostsMap)))
return nil
}
// LoadLocalRulesOnly 只加载本地规则
func (m *ShieldManager) LoadLocalRulesOnly() error {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
// 清空现有规则
m.domainRules = make(map[string]bool)
m.domainExceptions = make(map[string]bool)
m.domainRulesIsLocal = make(map[string]bool)
m.domainExceptionsIsLocal = make(map[string]bool)
m.domainRulesSource = make(map[string]string)
m.domainExceptionsSource = make(map[string]string)
m.domainRulesOriginal = make(map[string]string)
m.domainExceptionsOriginal = make(map[string]string)
m.regexRules = []regexRule{}
m.regexExceptions = []regexRule{}
m.hostsMap = make(map[string]string)
m.localRulesCount = 0
m.remoteRulesCount = 0
// 保留计数数据,不随规则重新加载而清空
// 加载自定义规则文件
if err := m.loadLocalRules(); err != nil {
logger.Error("加载自定义规则失败", "error", err)
// 继续执行,不返回错误
}
// 加载hosts文件
if err := m.loadHosts(); err != nil {
logger.Error("加载hosts文件失败", "error", err)
// 继续执行,不返回错误
}
logger.Info(fmt.Sprintf("本地规则加载完成,域名规则: %d, 排除规则: %d, 正则规则: %d, hosts规则: %d",
len(m.domainRules), len(m.domainExceptions), len(m.regexRules), len(m.hostsMap)))
return nil
}
// loadLocalRules 加载自定义规则文件
func (m *ShieldManager) loadLocalRules() error {
file, err := os.Open("data/rules.txt")
if err != nil {
return err
}
defer file.Close()
// 记录加载前的规则数量,用于计算自定义规则数量
beforeDomainRules := len(m.domainRules)
beforeRegexRules := len(m.regexRules)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
m.parseRule(line, true, "自定义规则") // 自定义规则,isLocal=true,来源为"自定义规则"
}
// 更新自定义规则计数
m.localRulesCount = (len(m.domainRules) - beforeDomainRules) + (len(m.regexRules) - beforeRegexRules)
return scanner.Err()
}
// loadRemoteRules 加载远程规则
func (m *ShieldManager) loadRemoteRules() error {
for _, blacklist := range m.config.Blacklists {
if blacklist.Enabled {
if err := m.fetchRemoteRules(blacklist.URL); err != nil {
logger.Error("获取远程规则失败", "url", blacklist.URL, "error", err)
continue
}
}
}
return nil
}
// getCacheFilePath 根据URL生成缓存文件路径
func (m *ShieldManager) getCacheFilePath(url string) string {
// 使用URL的哈希值作为文件名,避免文件名冲突
hash := fmt.Sprintf("%x", url)
// 简单处理,移除特殊字符,确保文件名合法
hash = strings.ReplaceAll(hash, "/", "_")
hash = strings.ReplaceAll(hash, "\\", "_")
return filepath.Join("data/remote_rules", hash+".rules")
}
// shouldUpdateCache 检查缓存是否需要更新
func (m *ShieldManager) shouldUpdateCache(cacheFile string) bool {
// 检查文件是否存在
if _, err := os.Stat(cacheFile); os.IsNotExist(err) {
return true
}
// 检查文件修改时间
fileInfo, err := os.Stat(cacheFile)
if err != nil {
return true
}
// 如果缓存文件超过更新间隔时间,则需要更新
return time.Since(fileInfo.ModTime()) > time.Duration(m.config.UpdateInterval)*time.Second
}
// fetchRemoteRules 从远程URL获取规则
func (m *ShieldManager) fetchRemoteRules(url string) error {
// 获取缓存文件路径
cacheFile := m.getCacheFilePath(url)
// 检查缓存是否存在且不需要更新
if !m.shouldUpdateCache(cacheFile) {
// 从缓存加载规则
if err := m.loadCachedRules(cacheFile, url); err == nil {
logger.Info("从缓存加载远程规则", "url", url)
return nil // 缓存有效且加载成功,直接返回
}
}
// 缓存不存在或需要更新,从远程获取规则
resp, err := http.Get(url)
if err != nil {
// 如果从远程获取失败,尝试从缓存加载(即使缓存过期)
logger.Warn("远程规则获取失败,尝试使用过期缓存", "url", url, "error", err)
if err := m.loadCachedRules(cacheFile, url); err == nil {
logger.Info("从过期缓存加载远程规则", "url", url)
return nil
}
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// 如果状态码不正确,尝试从缓存加载(即使缓存过期)
logger.Warn("远程规则获取失败,尝试使用过期缓存", "url", url, "statusCode", resp.StatusCode)
if err := m.loadCachedRules(cacheFile, url); err == nil {
logger.Info("从过期缓存加载远程规则", "url", url)
return nil
}
return fmt.Errorf("远程服务器返回错误状态码: %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// 如果读取响应失败,尝试从缓存加载(即使缓存过期)
logger.Warn("远程规则读取失败,尝试使用过期缓存", "url", url, "error", err)
if err := m.loadCachedRules(cacheFile, url); err == nil {
logger.Info("从过期缓存加载远程规则", "url", url)
return nil
}
return err
}
// 保存规则到缓存
if err := m.saveRemoteRulesToCache(cacheFile, body); err != nil {
logger.Warn("保存远程规则缓存失败", "url", url, "error", err)
// 继续处理,不返回错误
}
// 解析并加载规则
lines := strings.Split(string(body), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
m.parseRule(line, false, url) // 远程规则,isLocal=false,来源为URL
}
return nil
}
// loadCachedRules 从缓存文件加载规则
func (m *ShieldManager) loadCachedRules(filePath string, source string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// 记录加载前的规则数量,用于计算远程规则数量
beforeDomainRules := len(m.domainRules)
beforeRegexRules := len(m.regexRules)
body, err := ioutil.ReadAll(file)
if err != nil {
return err
}
lines := strings.Split(string(body), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
m.parseRule(line, false, source) // 远程规则,isLocal=false,来源为URL
}
// 更新远程规则计数
remoteRulesAdded := (len(m.domainRules) - beforeDomainRules) + (len(m.regexRules) - beforeRegexRules)
m.remoteRulesCount += remoteRulesAdded
return nil
}
// saveRemoteRulesToCache 保存远程规则到缓存文件
func (m *ShieldManager) saveRemoteRulesToCache(filePath string, data []byte) error {
// 确保缓存目录存在
if err := os.MkdirAll("data/remote_rules", 0755); err != nil {
return err
}
// 写入文件
return ioutil.WriteFile(filePath, data, 0644)
}
// loadHosts 加载hosts文件
func (m *ShieldManager) loadHosts() error {
file, err := os.Open("data/hosts.txt")
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.Fields(line)
if len(parts) >= 2 {
ip := parts[0]
for i := 1; i < len(parts); i++ {
m.hostsMap[parts[i]] = ip
}
}
}
return scanner.Err()
}
// parseRule 解析规则行
func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
// 保存原始规则用于后续使用
originalLine := line
// 处理注释
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
return
}
// 移除规则选项部分(暂时不处理规则选项)
if strings.Contains(line, "$") {
parts := strings.SplitN(line, "$", 2)
line = parts[0]
// 规则选项暂时不处理
}
// 处理排除规则 (@@前缀表示取消屏蔽)
isException := false
if strings.HasPrefix(line, "@@") {
isException = true
line = strings.TrimPrefix(line, "@@")
}
// 处理不同类型的规则
switch {
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
// AdGuardHome域名规则格式: ||example.com^
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
m.addDomainRule(domain, !isException, isLocal, source, originalLine)
case strings.HasPrefix(line, "||"):
// 精确域名匹配规则
domain := strings.TrimPrefix(line, "||")
m.addDomainRule(domain, !isException, isLocal, source, originalLine)
case strings.HasPrefix(line, "*"):
// 通配符规则,转换为正则表达式
pattern := strings.ReplaceAll(line, "*", ".*")
pattern = "^" + pattern + "$"
if re, err := regexp.Compile(pattern); err == nil {
// 保存原始规则字符串
m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
// 正则表达式匹配规则:/regex/ 格式,不区分大小写
pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
// 编译为不区分大小写的正则表达式,确保能匹配域名中任意位置
// 对于像 /domain/ 这样的规则,应该匹配包含 domain 字符串的任何域名
if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(pattern) + ".*"); err == nil {
// 保存原始规则字符串
m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
// 完整URL匹配规则
urlPattern := strings.TrimPrefix(strings.TrimSuffix(line, "|"), "|")
// 将URL模式转换为正则表达式
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
case strings.HasPrefix(line, "|"):
// URL开头匹配规则
urlPattern := strings.TrimPrefix(line, "|")
pattern := "^" + regexp.QuoteMeta(urlPattern)
if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
case strings.HasSuffix(line, "|"):
// URL结尾匹配规则
urlPattern := strings.TrimSuffix(line, "|")
pattern := regexp.QuoteMeta(urlPattern) + "$"
if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
default:
// 默认作为普通域名规则
m.addDomainRule(line, !isException, isLocal, source, originalLine)
}
}
// parseRuleOptions 解析规则选项
func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string {
options := make(map[string]string)
parts := strings.Split(optionsStr, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if strings.Contains(part, "=") {
kv := strings.SplitN(part, "=", 2)
options[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
} else {
options[part] = ""
}
}
return options
}
// addDomainRule 添加域名规则,支持是否为阻止规则
func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) {
if block {
// 如果是远程规则,检查是否已经存在自定义规则(阻止或排除),如果存在则不覆盖
if !isLocal {
// 检查是否存在自定义阻止规则
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
// 已经存在自定义规则,不覆盖
return
}
// 检查是否存在自定义排除规则
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
// 已经存在自定义规则,不覆盖
return
}
}
m.domainRules[domain] = true
m.domainRulesIsLocal[domain] = isLocal
m.domainRulesSource[domain] = source
m.domainRulesOriginal[domain] = original
} else {
// 添加到排除规则
// 如果是远程规则,检查是否已经存在自定义规则(阻止或排除),如果存在则不覆盖
if !isLocal {
// 检查是否存在自定义阻止规则
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
// 已经存在自定义规则,不覆盖
return
}
// 检查是否存在自定义排除规则
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
// 已经存在自定义规则,不覆盖
return
}
}
m.domainExceptions[domain] = true
m.domainExceptionsIsLocal[domain] = isLocal
m.domainExceptionsSource[domain] = source
m.domainExceptionsOriginal[domain] = original
}
}
// addRegexRule 添加正则表达式规则,支持是否为阻止规则
func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool, isLocal bool, source string) {
rule := regexRule{
pattern: re,
original: original,
isLocal: isLocal,
source: source,
}
if block {
// 如果是远程规则,检查是否已经存在任何自定义规则,如果存在则不添加
if !isLocal {
// 检查是否存在自定义阻止规则
for _, existingRule := range m.regexRules {
if existingRule.pattern.String() == re.String() && existingRule.isLocal {
// 已经存在相同的自定义阻止规则,不添加
return
}
}
// 检查是否存在自定义排除规则
for _, existingRule := range m.regexExceptions {
if existingRule.pattern.String() == re.String() && existingRule.isLocal {
// 已经存在相同的自定义排除规则,不添加
return
}
}
}
m.regexRules = append(m.regexRules, rule)
} else {
// 添加到排除规则
// 如果是远程规则,检查是否已经存在任何自定义规则,如果存在则不添加
if !isLocal {
// 检查是否存在自定义阻止规则
for _, existingRule := range m.regexRules {
if existingRule.pattern.String() == re.String() && existingRule.isLocal {
// 已经存在相同的自定义阻止规则,不添加
return
}
}
// 检查是否存在自定义排除规则
for _, existingRule := range m.regexExceptions {
if existingRule.pattern.String() == re.String() && existingRule.isLocal {
// 已经存在相同的自定义排除规则,不添加
return
}
}
}
m.regexExceptions = append(m.regexExceptions, rule)
}
}
// IsBlocked 检查域名是否被屏蔽
// CheckDomainBlockDetails 检查域名是否被屏蔽,并返回详细信息
func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 预处理域名,去除可能的端口号
if strings.Contains(domain, ":") {
parts := strings.Split(domain, ":")
domain = parts[0]
}
result := map[string]interface{}{
"domain": domain,
"blocked": false,
"blockRule": "",
"blockRuleType": "",
"blocksource": "",
"isGFWList": false,
"excluded": false,
"excludeRule": "",
"excludeRuleType": "",
"hasHosts": false,
"hostsIP": "",
}
// 检查hosts记录
hostsIP, hasHosts := m.GetHostsIP(domain)
result["hasHosts"] = hasHosts
result["hostsIP"] = hostsIP
// 检查排除规则(优先级最高)
// 1. 先检查本地域名排除规则
if m.domainExceptions[domain] && m.domainExceptionsIsLocal[domain] {
result["excluded"] = true
result["excludeRule"] = m.domainExceptionsOriginal[domain]
result["excludeRuleType"] = "exact_domain"
result["blocksource"] = m.domainExceptionsSource[domain]
return result
}
// 2. 再检查远程域名排除规则
if m.domainExceptions[domain] && !m.domainExceptionsIsLocal[domain] {
result["excluded"] = true
result["excludeRule"] = m.domainExceptionsOriginal[domain]
result["excludeRuleType"] = "exact_domain"
result["blocksource"] = m.domainExceptionsSource[domain]
return result
}
// 检查子域名排除规则
parts := strings.Split(domain, ".")
// 3. 先检查本地子域名排除规则
for i := 0; i < len(parts)-1; i++ {
subdomain := strings.Join(parts[i:], ".")
if m.domainExceptions[subdomain] && m.domainExceptionsIsLocal[subdomain] {
result["excluded"] = true
result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
result["excludeRuleType"] = "subdomain"
result["blocksource"] = m.domainExceptionsSource[subdomain]
return result
}
}
// 4. 再检查远程子域名排除规则
for i := 0; i < len(parts)-1; i++ {
subdomain := strings.Join(parts[i:], ".")
if m.domainExceptions[subdomain] && !m.domainExceptionsIsLocal[subdomain] {
result["excluded"] = true
result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
result["excludeRuleType"] = "subdomain"
result["blocksource"] = m.domainExceptionsSource[subdomain]
return result
}
}
// 5. 先检查本地正则表达式排除规则
for _, re := range m.regexExceptions {
if re.isLocal && re.pattern.MatchString(domain) {
result["excluded"] = true
result["excludeRule"] = re.original
result["excludeRuleType"] = "regex"
result["blocksource"] = re.source
return result
}
}
// 6. 再检查远程正则表达式排除规则
for _, re := range m.regexExceptions {
if !re.isLocal && re.pattern.MatchString(domain) {
result["excluded"] = true
result["excludeRule"] = re.original
result["excludeRuleType"] = "regex"
result["blocksource"] = re.source
return result
}
}
// 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配
// 7. 先检查本地域名阻止规则
if m.domainRules[domain] && m.domainRulesIsLocal[domain] {
result["blocked"] = true
result["blockRule"] = m.domainRulesOriginal[domain]
result["blockRuleType"] = "exact_domain"
result["blocksource"] = m.domainRulesSource[domain]
result["isGFWList"] = m.domainRulesSource[domain] == "GFWList"
return result
}
// 8. 再检查远程域名阻止规则
if m.domainRules[domain] && !m.domainRulesIsLocal[domain] {
result["blocked"] = true
result["blockRule"] = m.domainRulesOriginal[domain]
result["blockRuleType"] = "exact_domain"
result["blocksource"] = m.domainRulesSource[domain]
result["isGFWList"] = m.domainRulesSource[domain] == "GFWList"
return result
}
// 检查子域名匹配(AdGuardHome风格)
// 从最长的子域名开始匹配,确保优先级正确
// 9. 先检查本地子域名阻止规则
for i := 0; i < len(parts)-1; i++ {
subdomain := strings.Join(parts[i:], ".")
if m.domainRules[subdomain] && m.domainRulesIsLocal[subdomain] {
result["blocked"] = true
result["blockRule"] = m.domainRulesOriginal[subdomain]
result["blockRuleType"] = "subdomain"
result["blocksource"] = m.domainRulesSource[subdomain]
result["isGFWList"] = m.domainRulesSource[subdomain] == "GFWList"
return result
}
}
// 10. 再检查远程子域名阻止规则
for i := 0; i < len(parts)-1; i++ {
subdomain := strings.Join(parts[i:], ".")
if m.domainRules[subdomain] && !m.domainRulesIsLocal[subdomain] {
result["blocked"] = true
result["blockRule"] = m.domainRulesOriginal[subdomain]
result["blockRuleType"] = "subdomain"
result["blocksource"] = m.domainRulesSource[subdomain]
result["isGFWList"] = m.domainRulesSource[subdomain] == "GFWList"
return result
}
}
// 11. 先检查本地正则表达式阻止规则
for _, re := range m.regexRules {
if re.isLocal && re.pattern.MatchString(domain) {
result["blocked"] = true
result["blockRule"] = re.original
result["blockRuleType"] = "regex"
result["blocksource"] = re.source
result["isGFWList"] = re.source == "GFWList"
return result
}
}
// 12. 再检查远程正则表达式阻止规则
for _, re := range m.regexRules {
if !re.isLocal && re.pattern.MatchString(domain) {
result["blocked"] = true
result["blockRule"] = re.original
result["blockRuleType"] = "regex"
result["blocksource"] = re.source
result["isGFWList"] = re.source == "GFWList"
return result
}
}
return result
}
// IsBlocked 检查域名是否被屏蔽(保留原有方法以保持兼容性)
func (m *ShieldManager) IsBlocked(domain string) bool {
details := m.CheckDomainBlockDetails(domain)
return details["blocked"].(bool)
}
// RecordBlockedDomain 记录被屏蔽的域名
func (m *ShieldManager) RecordBlockedDomain(domain string) {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
m.blockedDomainsCount[domain]++
}
// RecordResolvedDomain 记录被解析的域名
func (m *ShieldManager) RecordResolvedDomain(domain string) {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
m.resolvedDomainsCount[domain]++
}
// GetTopBlockedDomains 获取最常被屏蔽的域名
func (m *ShieldManager) GetTopBlockedDomains(limit int) []map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 如果没有数据,返回空数组
if len(m.blockedDomainsCount) == 0 {
return []map[string]interface{}{}
}
// 转换为切片以便排序
type domainCount struct {
Domain string
Count int
}
var domains []domainCount
for domain, count := range m.blockedDomainsCount {
domains = append(domains, domainCount{Domain: domain, Count: count})
}
// 按计数降序排序
sort.Slice(domains, func(i, j int) bool {
return domains[i].Count > domains[j].Count
})
// 限制返回数量
if len(domains) > limit {
domains = domains[:limit]
}
// 转换为API响应格式
result := make([]map[string]interface{}, len(domains))
for i, item := range domains {
result[i] = map[string]interface{}{
"domain": item.Domain,
"count": item.Count,
}
}
return result
}
// GetTopResolvedDomains 获取最常被解析的域名
func (m *ShieldManager) GetTopResolvedDomains(limit int) []map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 如果没有数据,返回空数组
if len(m.resolvedDomainsCount) == 0 {
return []map[string]interface{}{}
}
// 转换为切片以便排序
type domainCount struct {
Domain string
Count int
}
var domains []domainCount
for domain, count := range m.resolvedDomainsCount {
domains = append(domains, domainCount{Domain: domain, Count: count})
}
// 按计数降序排序
sort.Slice(domains, func(i, j int) bool {
return domains[i].Count > domains[j].Count
})
// 限制返回数量
if len(domains) > limit {
domains = domains[:limit]
}
// 转换为API响应格式
result := make([]map[string]interface{}, len(domains))
for i, item := range domains {
result[i] = map[string]interface{}{
"domain": item.Domain,
"count": item.Count,
}
}
return result
}
// GetHostsIP 获取hosts文件中的IP映射
func (m *ShieldManager) GetHostsIP(domain string) (string, bool) {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
ip, exists := m.hostsMap[domain]
return ip, exists
}
// AddRule 添加屏蔽规则,用户添加的规则是自定义规则
func (m *ShieldManager) AddRule(rule string) error {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
// 解析并添加规则到内存,isLocal=true表示自定义规则,来源为"自定义规则"
m.parseRule(rule, true, "自定义规则")
// 持久化保存规则到文件
if err := m.saveRulesToFile(); err != nil {
logger.Error("保存规则到文件失败", "error", err)
return err
}
return nil
}
// RemoveRule 删除屏蔽规则
func (m *ShieldManager) RemoveRule(rule string) error {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
removed := false
// 清理规则,移除可能的修饰符
cleanRule := rule
// 移除规则结束符
cleanRule = strings.TrimSuffix(cleanRule, "^")
// 尝试多种可能的规则格式
formatsToTry := []string{cleanRule}
// 根据规则类型添加可能的格式变体
if strings.HasPrefix(cleanRule, "@@||") {
// 已有的排除规则格式,也尝试去掉前缀
formatsToTry = append(formatsToTry, strings.TrimPrefix(cleanRule, "@@||"))
} else if strings.HasPrefix(cleanRule, "||") {
// 已有的域名规则格式,也尝试去掉前缀
formatsToTry = append(formatsToTry, strings.TrimPrefix(cleanRule, "||"))
} else {
// 可能是裸域名,尝试添加前缀
formatsToTry = append(formatsToTry, "||"+cleanRule, "@@||"+cleanRule)
}
// 尝试所有可能的格式变体来删除规则
for _, format := range formatsToTry {
if removed {
break
}
// 尝试删除排除规则
if strings.HasPrefix(format, "@@||") {
domain := strings.TrimPrefix(format, "@@||")
if _, exists := m.domainExceptions[domain]; exists {
delete(m.domainExceptions, domain)
delete(m.domainExceptionsIsLocal, domain)
delete(m.domainExceptionsSource, domain)
removed = true
break
}
} else if strings.HasPrefix(format, "||") {
// 尝试删除域名规则
domain := strings.TrimPrefix(format, "||")
if _, exists := m.domainRules[domain]; exists {
// 删除主域名规则
delete(m.domainRules, domain)
delete(m.domainRulesIsLocal, domain)
delete(m.domainRulesSource, domain)
removed = true
break
}
} else {
// 尝试直接作为域名删除
if _, exists := m.domainRules[format]; exists {
// 删除主域名规则
delete(m.domainRules, format)
delete(m.domainRulesIsLocal, format)
delete(m.domainRulesSource, format)
removed = true
break
}
if _, exists := m.domainExceptions[format]; exists {
// 删除主排除规则
delete(m.domainExceptions, format)
delete(m.domainExceptionsIsLocal, format)
delete(m.domainExceptionsSource, format)
removed = true
break
}
}
}
// 处理正则表达式规则
if !removed && strings.HasPrefix(cleanRule, "/") && strings.HasSuffix(cleanRule, "/") {
// 检查是否在正则表达式规则中
newRegexRules := []regexRule{}
for _, re := range m.regexRules {
if re.original != rule && re.original != cleanRule {
newRegexRules = append(newRegexRules, re)
} else {
removed = true
}
}
m.regexRules = newRegexRules
// 如果没有从正则规则中找到,检查是否在正则排除规则中
if !removed {
newRegexExceptions := []regexRule{}
for _, re := range m.regexExceptions {
if re.original != rule && re.original != cleanRule {
newRegexExceptions = append(newRegexExceptions, re)
} else {
removed = true
}
}
m.regexExceptions = newRegexExceptions
}
}
// 处理通配符和URL匹配规则
if !removed && (strings.HasPrefix(cleanRule, "*") || strings.HasSuffix(cleanRule, "*") || strings.HasPrefix(cleanRule, "|") || strings.HasSuffix(cleanRule, "|")) {
// 遍历所有规则,找到匹配的规则进行删除
for domain := range m.domainRules {
if domain == cleanRule || domain == rule {
delete(m.domainRules, domain)
delete(m.domainRulesIsLocal, domain)
delete(m.domainRulesSource, domain)
removed = true
break
}
}
if !removed {
for domain := range m.domainExceptions {
if domain == cleanRule || domain == rule {
delete(m.domainExceptions, domain)
delete(m.domainExceptionsIsLocal, domain)
delete(m.domainExceptionsSource, domain)
removed = true
break
}
}
}
}
// 如果没有删除任何规则,尝试删除可能的子域名规则
if !removed {
// 解析原始规则,提取可能的主域名
originalRule := cleanRule
// 移除可能的前缀
originalRule = strings.TrimPrefix(originalRule, "@@||")
originalRule = strings.TrimPrefix(originalRule, "||")
// 检查是否有子域名规则需要删除
// 遍历所有域名规则,删除包含原始规则作为后缀的子域名规则
for domain := range m.domainRules {
if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
delete(m.domainRules, domain)
delete(m.domainRulesIsLocal, domain)
delete(m.domainRulesSource, domain)
removed = true
}
}
// 遍历所有排除规则,删除包含原始规则作为后缀的子域名规则
for domain := range m.domainExceptions {
if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
delete(m.domainExceptions, domain)
delete(m.domainExceptionsIsLocal, domain)
delete(m.domainExceptionsSource, domain)
removed = true
}
}
}
// 如果有规则被删除,持久化保存更改
if removed {
if err := m.saveRulesToFile(); err != nil {
logger.Error("保存规则到文件失败", "error", err)
return err
}
}
return nil
}
// StartAutoUpdate 启动自动更新
func (m *ShieldManager) StartAutoUpdate() {
if m.updateRunning {
return
}
m.updateRunning = true
go func() {
ticker := time.NewTicker(time.Duration(m.config.UpdateInterval) * time.Second)
defer ticker.Stop()
// 启动自动保存计数数据
go m.startAutoSaveStats()
for {
select {
case <-ticker.C:
logger.Info("开始自动更新规则")
if err := m.LoadRules(); err != nil {
logger.Error("自动更新规则失败", "error", err)
} else {
logger.Info("自动更新规则成功")
}
case <-m.updateCtx.Done():
// 保存计数数据
m.saveStatsData()
m.updateRunning = false
return
}
}
}()
logger.Info("规则自动更新已启动", "interval", m.config.UpdateInterval)
// 如果是首次启动,先保存一次数据确保目录存在
go m.saveStatsData()
}
// StopAutoUpdate 停止自动更新
func (m *ShieldManager) StopAutoUpdate() {
m.updateRunning = false
m.updateCancel()
// 保存计数数据
m.saveStatsData()
logger.Info("规则自动更新已停止")
}
// saveRulesToFile 保存规则到文件,只保存自定义规则
func (m *ShieldManager) saveRulesToFile() error {
var rules []string
// 添加本地域名规则
for domain, isLocal := range m.domainRulesIsLocal {
if isLocal {
rules = append(rules, "||"+domain)
}
}
// 添加本地正则表达式规则
for _, re := range m.regexRules {
if re.isLocal {
rules = append(rules, re.original)
}
}
// 添加本地排除规则
for domain, isLocal := range m.domainExceptionsIsLocal {
if isLocal {
rules = append(rules, "@@||"+domain)
}
}
// 添加本地正则表达式排除规则
for _, re := range m.regexExceptions {
if re.isLocal {
rules = append(rules, re.original)
}
}
// 写入文件
content := strings.Join(rules, "\n")
return ioutil.WriteFile("data/rules.txt", []byte(content), 0644)
}
// AddHostsEntry 添加hosts条目
func (m *ShieldManager) AddHostsEntry(ip, domain string) error {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
m.hostsMap[domain] = ip
// 持久化保存到hosts文件
if err := m.saveHostsToFile(); err != nil {
logger.Error("保存hosts到文件失败", "error", err)
return err
}
return nil
}
// RemoveHostsEntry 删除hosts条目
func (m *ShieldManager) RemoveHostsEntry(domain string) error {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
if _, exists := m.hostsMap[domain]; exists {
delete(m.hostsMap, domain)
// 持久化保存到hosts文件
if err := m.saveHostsToFile(); err != nil {
logger.Error("保存hosts到文件失败", "error", err)
return err
}
}
return nil
}
// saveHostsToFile 保存hosts到文件
func (m *ShieldManager) saveHostsToFile() error {
var lines []string
// 添加hosts头部注释
lines = append(lines, "# DNS Server Hosts File")
lines = append(lines, "# Generated by DNS Server")
lines = append(lines, "")
// 添加localhost条目(如果不存在)
if _, exists := m.hostsMap["localhost"]; !exists {
lines = append(lines, "127.0.0.1 localhost")
lines = append(lines, "::1 localhost")
lines = append(lines, "")
}
// 添加所有hosts条目
for domain, ip := range m.hostsMap {
lines = append(lines, ip+"\t"+domain)
}
// 写入文件
content := strings.Join(lines, "\n")
return ioutil.WriteFile("data/hosts.txt", []byte(content), 0644)
}
// GetStats 获取规则统计信息
func (m *ShieldManager) GetStats() map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
return map[string]interface{}{
"domainRules": len(m.domainRules),
"domainExceptions": len(m.domainExceptions),
"regexRules": len(m.regexRules),
"regexExceptions": len(m.regexExceptions),
"hostsRules": len(m.hostsMap),
"updateInterval": m.config.UpdateInterval,
}
}
// loadStatsData 从文件加载计数数据
func (m *ShieldManager) loadStatsData() {
// 获取绝对路径以避免工作目录问题
statsFilePath, err := filepath.Abs("data/shield_stats.json")
if err != nil {
logger.Error("获取Shield统计文件绝对路径失败", "path", "data/shield_stats.json", "error", err)
return
}
logger.Debug("尝试加载Shield统计数据", "file", statsFilePath)
// 检查文件是否存在
fileInfo, err := os.Stat(statsFilePath)
if err != nil {
if os.IsNotExist(err) {
logger.Info("Shield统计文件不存在,将创建新文件", "file", statsFilePath)
// 初始化空的计数数据
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
// 尝试立即保存一个有效的空文件
m.saveStatsData()
} else {
logger.Error("检查Shield统计文件失败", "file", statsFilePath, "error", err)
}
return
}
// 检查文件大小
if fileInfo.Size() == 0 {
logger.Warn("Shield统计文件为空,将重新初始化", "file", statsFilePath)
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
m.saveStatsData()
return
}
// 读取文件内容
data, err := ioutil.ReadFile(statsFilePath)
if err != nil {
logger.Error("读取Shield计数数据文件失败", "file", statsFilePath, "error", err)
return
}
// 检查数据长度
if len(data) == 0 {
logger.Warn("读取到的Shield统计数据为空", "file", statsFilePath)
return
}
// 尝试解析JSON
var statsData ShieldStatsData
err = json.Unmarshal(data, &statsData)
if err != nil {
// 记录更详细的错误信息,包括数据前50个字符
dataSample := string(data)
if len(dataSample) > 50 {
dataSample = dataSample[:50] + "..."
}
logger.Error("解析Shield计数数据失败",
"file", statsFilePath,
"error", err,
"data_length", len(data),
"data_sample", dataSample)
// 重置为默认空数据
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
// 尝试保存一个有效的空文件
m.saveStatsData()
return
}
// 恢复计数数据
m.rulesMutex.Lock()
if statsData.BlockedDomainsCount != nil {
m.blockedDomainsCount = statsData.BlockedDomainsCount
} else {
m.blockedDomainsCount = make(map[string]int)
}
if statsData.ResolvedDomainsCount != nil {
m.resolvedDomainsCount = statsData.ResolvedDomainsCount
} else {
m.resolvedDomainsCount = make(map[string]int)
}
m.rulesMutex.Unlock()
logger.Info("Shield计数数据加载成功", "blocked_entries", len(m.blockedDomainsCount), "resolved_entries", len(m.resolvedDomainsCount))
}
// saveStatsData 保存计数数据到文件
func (m *ShieldManager) saveStatsData() {
// 获取绝对路径以避免工作目录问题
statsFilePath, err := filepath.Abs("data/shield_stats.json")
if err != nil {
logger.Error("获取Shield统计文件绝对路径失败", "path", "data/shield_stats.json", "error", err)
return
}
// 创建数据目录
statsDir := filepath.Dir(statsFilePath)
err = os.MkdirAll(statsDir, 0755)
if err != nil {
logger.Error("创建Shield统计数据目录失败", "dir", statsDir, "error", err)
return
}
// 收集计数数据
m.rulesMutex.RLock()
statsData := &ShieldStatsData{
BlockedDomainsCount: make(map[string]int),
ResolvedDomainsCount: make(map[string]int),
LastSaved: time.Now(),
}
// 复制数据
for k, v := range m.blockedDomainsCount {
statsData.BlockedDomainsCount[k] = v
}
for k, v := range m.resolvedDomainsCount {
statsData.ResolvedDomainsCount[k] = v
}
m.rulesMutex.RUnlock()
// 序列化数据
jsonData, err := json.MarshalIndent(statsData, "", " ")
if err != nil {
logger.Error("序列化Shield计数数据失败", "error", err)
return
}
// 使用临时文件先写入,然后重命名,避免文件损坏
tempFilePath := statsFilePath + ".tmp"
err = ioutil.WriteFile(tempFilePath, jsonData, 0644)
if err != nil {
logger.Error("写入临时Shield统计文件失败", "file", tempFilePath, "error", err)
return
}
// 原子操作重命名文件
err = os.Rename(tempFilePath, statsFilePath)
if err != nil {
logger.Error("重命名Shield统计文件失败", "temp", tempFilePath, "dest", statsFilePath, "error", err)
// 尝试清理临时文件
os.Remove(tempFilePath)
return
}
logger.Info("Shield计数数据保存成功", "file", statsFilePath, "blocked_entries", len(statsData.BlockedDomainsCount), "resolved_entries", len(statsData.ResolvedDomainsCount))
}
// startAutoSaveStats 启动计数数据自动保存功能
func (m *ShieldManager) startAutoSaveStats() {
if m.config.StatsSaveInterval <= 0 {
return
}
ticker := time.NewTicker(time.Duration(m.config.StatsSaveInterval) * time.Second)
defer ticker.Stop()
logger.Info("启动Shield计数数据自动保存功能", "interval", m.config.StatsSaveInterval, "file", "data/shield_stats.json")
// 定期保存数据
for {
select {
case <-ticker.C:
m.saveStatsData()
case <-m.updateCtx.Done():
return
}
}
}
// GetBlacklists 获取所有黑名单配置
func (m *ShieldManager) GetBlacklists() []config.BlacklistEntry {
return m.config.Blacklists
}
// UpdateBlacklist 更新黑名单配置
func (m *ShieldManager) UpdateBlacklist(blacklists []config.BlacklistEntry) {
m.config.Blacklists = blacklists
}
// GetAllHosts 获取所有hosts条目
func (m *ShieldManager) GetAllHosts() map[string]string {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 返回hostsMap的副本,避免并发问题
hostsCopy := make(map[string]string, len(m.hostsMap))
for domain, ip := range m.hostsMap {
hostsCopy[domain] = ip
}
return hostsCopy
}
// GetHostsCount 获取hosts条目数量
func (m *ShieldManager) GetHostsCount() int {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
return len(m.hostsMap)
}
// GetLocalRules 获取仅自定义规则
func (m *ShieldManager) GetLocalRules() map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 转换map和slice为字符串列表,只包含自定义规则
domainRulesList := make([]string, 0)
for domain, isLocal := range m.domainRulesIsLocal {
if isLocal && m.domainRules[domain] {
domainRulesList = append(domainRulesList, "||"+domain+"^")
}
}
domainExceptionsList := make([]string, 0)
for domain, isLocal := range m.domainExceptionsIsLocal {
if isLocal && m.domainExceptions[domain] {
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
}
}
// 获取本地正则规则原始字符串
regexRulesList := make([]string, 0)
for _, re := range m.regexRules {
if re.isLocal {
regexRulesList = append(regexRulesList, re.original)
}
}
// 获取本地正则排除规则原始字符串
regexExceptionsList := make([]string, 0)
for _, re := range m.regexExceptions {
if re.isLocal {
regexExceptionsList = append(regexExceptionsList, re.original)
}
}
// 计算自定义规则数量
localDomainRulesCount := 0
for _, isLocal := range m.domainRulesIsLocal {
if isLocal {
localDomainRulesCount++
}
}
localRegexRulesCount := 0
for _, re := range m.regexRules {
if re.isLocal {
localRegexRulesCount++
}
}
localRulesCount := localDomainRulesCount + localRegexRulesCount
return map[string]interface{}{
"domainRules": domainRulesList,
"domainExceptions": domainExceptionsList,
"regexRules": regexRulesList,
"regexExceptions": regexExceptionsList,
"localRulesCount": localRulesCount,
"localDomainRulesCount": localDomainRulesCount,
"localRegexRulesCount": localRegexRulesCount,
}
}
// GetRemoteRules 获取仅远程规则
func (m *ShieldManager) GetRemoteRules() map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 转换map和slice为字符串列表,只包含远程规则
domainRulesList := make([]string, 0)
for domain, isLocal := range m.domainRulesIsLocal {
if !isLocal && m.domainRules[domain] {
domainRulesList = append(domainRulesList, "||"+domain+"^")
}
}
domainExceptionsList := make([]string, 0)
for domain, isLocal := range m.domainExceptionsIsLocal {
if !isLocal && m.domainExceptions[domain] {
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
}
}
// 获取远程正则规则原始字符串
regexRulesList := make([]string, 0)
for _, re := range m.regexRules {
if !re.isLocal {
regexRulesList = append(regexRulesList, re.original)
}
}
// 获取远程正则排除规则原始字符串
regexExceptionsList := make([]string, 0)
for _, re := range m.regexExceptions {
if !re.isLocal {
regexExceptionsList = append(regexExceptionsList, re.original)
}
}
// 计算远程规则数量
remoteDomainRulesCount := 0
for _, isLocal := range m.domainRulesIsLocal {
if !isLocal {
remoteDomainRulesCount++
}
}
remoteRegexRulesCount := 0
for _, re := range m.regexRules {
if !re.isLocal {
remoteRegexRulesCount++
}
}
remoteRulesCount := remoteDomainRulesCount + remoteRegexRulesCount
return map[string]interface{}{
"domainRules": domainRulesList,
"domainExceptions": domainExceptionsList,
"regexRules": regexRulesList,
"regexExceptions": regexExceptionsList,
"remoteRulesCount": remoteRulesCount,
"remoteDomainRulesCount": remoteDomainRulesCount,
"remoteRegexRulesCount": remoteRegexRulesCount,
"blacklists": m.config.Blacklists,
}
}
// GetRules 获取所有规则
func (m *ShieldManager) GetRules() map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 转换map和slice为字符串列表
domainRulesList := make([]string, 0, len(m.domainRules))
for domain := range m.domainRules {
domainRulesList = append(domainRulesList, "||"+domain+"^")
}
domainExceptionsList := make([]string, 0, len(m.domainExceptions))
for domain := range m.domainExceptions {
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
}
// 获取正则规则原始字符串
regexRulesList := make([]string, 0, len(m.regexRules))
for _, re := range m.regexRules {
regexRulesList = append(regexRulesList, re.original)
}
// 获取正则排除规则原始字符串
regexExceptionsList := make([]string, 0, len(m.regexExceptions))
for _, re := range m.regexExceptions {
regexExceptionsList = append(regexExceptionsList, re.original)
}
// 获取hosts规则
hostsRulesList := make([]string, 0, len(m.hostsMap))
for domain, ip := range m.hostsMap {
hostsRulesList = append(hostsRulesList, ip+"\t"+domain)
}
// 计算总规则数量
totalRulesCount := len(m.domainRules) + len(m.regexRules)
return map[string]interface{}{
"domainRules": domainRulesList,
"domainExceptions": domainExceptionsList,
"regexRules": regexRulesList,
"regexExceptions": regexExceptionsList,
"hostsRules": hostsRulesList,
"blacklists": m.config.Blacklists,
"localRulesCount": m.localRulesCount,
"remoteRulesCount": m.remoteRulesCount,
"totalRulesCount": totalRulesCount,
}
}