/* * Copyright (C) 2020-2022, IrineSistiana * * This file is part of mosdns. * * mosdns is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * mosdns is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package coremain import ( "encoding/json" "fmt" "net/http" "os" "path/filepath" "strings" "github.com/go-chi/chi/v5" "go.uber.org/zap" "gopkg.in/yaml.v3" ) // RuleConfig 域名路由规则配置 type RuleConfig struct { Name string `json:"name"` // 规则名称(唯一标识) DomainFile string `json:"domain_file"` // 域名文件路径 DNSStrategy string `json:"dns_strategy"` // DNS 策略:china / cloudflare / google / hybrid EnableMikrotik bool `json:"enable_mikrotik"` // 是否启用 MikroTik 同步 MikrotikConfig MikrotikConfig `json:"mikrotik_config"` // MikroTik 配置 Description string `json:"description"` // 规则描述 Enabled bool `json:"enabled"` // 是否启用 } // MikrotikConfig MikroTik 设备配置 type MikrotikConfig struct { Host string `json:"host"` // MikroTik 地址 Port int `json:"port"` // API 端口 Username string `json:"username"` // 用户名 Password string `json:"password"` // 密码 AddressList string `json:"address_list"` // 地址列表名称 Mask int `json:"mask"` // IP 掩码(24/32) MaxIPs int `json:"max_ips"` // 最大 IP 数量 CacheTTL int `json:"cache_ttl"` // 缓存时间(秒) TimeoutAddr int `json:"timeout_addr"` // 地址超时时间(秒) Comment string `json:"comment"` // 备注 } // RuleInfo 规则信息(列表显示) type RuleInfo struct { Name string `json:"name"` DomainFile string `json:"domain_file"` DNSStrategy string `json:"dns_strategy"` EnableMikrotik bool `json:"enable_mikrotik"` MikrotikDevice string `json:"mikrotik_device"` // 简化显示:host:port Description string `json:"description"` Enabled bool `json:"enabled"` FilePath string `json:"file_path"` // YAML 文件路径 } // handleListRules 列出所有规则 func (m *Mosdns) handleListRules(w http.ResponseWriter, r *http.Request) { // 扫描 config.d/rules 目录 rulesDir := "./config.d/rules" files, err := filepath.Glob(filepath.Join(rulesDir, "*.yaml")) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "扫描规则目录失败: " + err.Error(), }) return } var rules []RuleInfo for _, file := range files { ruleInfo, err := m.parseRuleFile(file) if err != nil { m.logger.Warn("解析规则文件失败", zap.String("file", file), zap.Error(err)) continue } rules = append(rules, ruleInfo) } m.writeJSONResponse(w, APIResponse{ Success: true, Data: rules, Message: fmt.Sprintf("找到 %d 条规则", len(rules)), }) } // handleGetRule 获取规则详情 func (m *Mosdns) handleGetRule(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") if name == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "规则名称不能为空", }) return } filePath := fmt.Sprintf("./config.d/rules/%s.yaml", name) if _, err := os.Stat(filePath); os.IsNotExist(err) { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "规则不存在: " + name, }) return } ruleConfig, err := m.parseRuleFileToConfig(filePath) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "解析规则失败: " + err.Error(), }) return } m.writeJSONResponse(w, APIResponse{ Success: true, Data: ruleConfig, }) } // handleAddRule 添加新规则 func (m *Mosdns) handleAddRule(w http.ResponseWriter, r *http.Request) { var rule RuleConfig if err := json.NewDecoder(r.Body).Decode(&rule); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "解析请求失败: " + err.Error(), }) return } // 验证必填字段 if rule.Name == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "规则名称不能为空", }) return } if rule.DomainFile == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "域名文件路径不能为空", }) return } if rule.DNSStrategy == "" { rule.DNSStrategy = "smart-fallback" // 默认使用智能防污染 } // 检查规则是否已存在 filePath := fmt.Sprintf("./config.d/rules/%s.yaml", rule.Name) if _, err := os.Stat(filePath); err == nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "规则已存在: " + rule.Name, }) return } // 使用配置构建器添加规则 builder := NewConfigBuilder(m.config, m.logger) domainRule := DomainRule{ Name: rule.Name, Description: rule.Description, DomainFile: rule.DomainFile, DNSStrategy: rule.DNSStrategy, EnableMikroTik: rule.EnableMikrotik, MikroTikConfig: MikroTikConfig{ Host: rule.MikrotikConfig.Host, Port: rule.MikrotikConfig.Port, Username: rule.MikrotikConfig.Username, Password: rule.MikrotikConfig.Password, AddressList: rule.MikrotikConfig.AddressList, Mask: rule.MikrotikConfig.Mask, MaxIPs: rule.MikrotikConfig.MaxIPs, CacheTTL: rule.MikrotikConfig.CacheTTL, TimeoutAddr: rule.MikrotikConfig.TimeoutAddr, Comment: rule.MikrotikConfig.Comment, }, Enabled: rule.Enabled, } if err := builder.AddDomainRule(domainRule); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "添加规则失败: " + err.Error(), }) return } // 保存主配置 if err := builder.Save(); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "保存主配置失败: " + err.Error(), }) return } m.logger.Info("规则已添加", zap.String("name", rule.Name), zap.String("domain_file", rule.DomainFile), zap.String("dns_strategy", rule.DNSStrategy)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: "规则添加成功,请重启服务使其生效", Data: map[string]interface{}{ "name": rule.Name, "domain_file": rule.DomainFile, "dns_strategy": rule.DNSStrategy, "mikrotik_enabled": rule.EnableMikrotik, }, }) } // handleUpdateRule 更新规则 func (m *Mosdns) handleUpdateRule(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") if name == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "规则名称不能为空", }) return } var rule RuleConfig if err := json.NewDecoder(r.Body).Decode(&rule); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "解析请求失败: " + err.Error(), }) return } rule.Name = name // 确保名称一致 // 使用配置构建器更新规则 builder := NewConfigBuilder(m.config, m.logger) domainRule := DomainRule{ Name: rule.Name, Description: rule.Description, DomainFile: rule.DomainFile, DNSStrategy: rule.DNSStrategy, EnableMikroTik: rule.EnableMikrotik, MikroTikConfig: MikroTikConfig{ Host: rule.MikrotikConfig.Host, Port: rule.MikrotikConfig.Port, Username: rule.MikrotikConfig.Username, Password: rule.MikrotikConfig.Password, AddressList: rule.MikrotikConfig.AddressList, Mask: rule.MikrotikConfig.Mask, MaxIPs: rule.MikrotikConfig.MaxIPs, CacheTTL: rule.MikrotikConfig.CacheTTL, TimeoutAddr: rule.MikrotikConfig.TimeoutAddr, Comment: rule.MikrotikConfig.Comment, }, Enabled: rule.Enabled, } if err := builder.UpdateDomainRule(name, domainRule); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "更新规则失败: " + err.Error(), }) return } // 保存主配置 if err := builder.Save(); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "保存主配置失败: " + err.Error(), }) return } m.logger.Info("规则已更新", zap.String("name", name), zap.String("domain_file", rule.DomainFile), zap.String("dns_strategy", rule.DNSStrategy)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: "规则更新成功,请重启服务使其生效", }) } // handleDeleteRule 删除规则 func (m *Mosdns) handleDeleteRule(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") if name == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "规则名称不能为空", }) return } filePath := fmt.Sprintf("./config.d/rules/%s.yaml", name) if _, err := os.Stat(filePath); os.IsNotExist(err) { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "规则不存在: " + name, }) return } // 删除文件 if err := os.Remove(filePath); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "删除规则文件失败: " + err.Error(), }) return } m.logger.Info("规则已删除", zap.String("name", name)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: "规则删除成功,请重启服务使其生效", }) } // generateRuleYAML 生成规则 YAML 内容 func (m *Mosdns) generateRuleYAML(rule RuleConfig) string { var sb strings.Builder // 文件头注释 sb.WriteString(fmt.Sprintf(`# ============================================ # %s 域名解析规则 # 由 Web UI 自动生成 `, rule.Name)) if rule.Description != "" { sb.WriteString(fmt.Sprintf("# 描述:%s\n", rule.Description)) } sb.WriteString("# ============================================\n\n") sb.WriteString("plugins:\n") // 1. 域名集合 sb.WriteString(fmt.Sprintf(` # 域名集合定义 - tag: domains_%s type: domain_set args: files: - "%s" `, rule.Name, rule.DomainFile)) // 2. 解析策略序列 dnsExec := m.getDNSExec(rule.DNSStrategy) sb.WriteString(fmt.Sprintf(` # 解析策略序列 - tag: rule_%s type: sequence args: # 匹配域名 - matches: qname $domains_%s exec: prefer_ipv4 # 使用指定的 DNS 策略解析 - matches: qname $domains_%s exec: $%s `, rule.Name, rule.Name, rule.Name, dnsExec)) // 3. MikroTik 配置(可选) if rule.EnableMikrotik { sb.WriteString(fmt.Sprintf(` # 推送到 MikroTik - matches: - qname $domains_%s - has_resp exec: $mikrotik_%s `, rule.Name, rule.Name)) } // 4. 返回结果 sb.WriteString(fmt.Sprintf(` # 返回结果 - matches: - qname $domains_%s - has_resp exec: accept # 记录日志 - matches: qname $domains_%s exec: query_summary %s_resolved `, rule.Name, rule.Name, rule.Name)) // 5. MikroTik 插件配置(可选) if rule.EnableMikrotik { cfg := rule.MikrotikConfig // 设置默认值 if cfg.Port == 0 { cfg.Port = 9728 } if cfg.Mask == 0 { cfg.Mask = 24 } if cfg.MaxIPs == 0 { cfg.MaxIPs = 50 } if cfg.CacheTTL == 0 { cfg.CacheTTL = 3600 } if cfg.TimeoutAddr == 0 { cfg.TimeoutAddr = 43200 } if cfg.Comment == "" { cfg.Comment = fmt.Sprintf("%s-AutoAdd", rule.Name) } sb.WriteString(fmt.Sprintf(` # MikroTik 地址列表同步配置 - tag: mikrotik_%s type: mikrotik_addresslist args: domain_files: - "%s" host: "%s" port: %d username: "%s" password: "%s" use_tls: false timeout: 3 address_list4: "%s" mask4: %d comment: "%s" timeout_addr: %d cache_ttl: %d verify_add: false add_all_ips: true max_ips: %d `, rule.Name, rule.DomainFile, cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.AddressList, cfg.Mask, cfg.Comment, cfg.TimeoutAddr, cfg.CacheTTL, cfg.MaxIPs, )) } return sb.String() } // getDNSExec 获取 DNS 策略对应的执行标签 func (m *Mosdns) getDNSExec(strategy string) string { switch strategy { case "china": return "china-dns" case "cloudflare": return "overseas-dns-cloudflare" case "google": return "overseas-dns-google" case "hybrid": return "hybrid-dns" case "anti-pollution": return "smart_anti_pollution" default: return "china-dns" } } // parseRuleFile 解析规则文件为 RuleInfo func (m *Mosdns) parseRuleFile(filePath string) (RuleInfo, error) { data, err := os.ReadFile(filePath) if err != nil { return RuleInfo{}, err } var config struct { Plugins []PluginConfig `yaml:"plugins"` } if err := yaml.Unmarshal(data, &config); err != nil { return RuleInfo{}, err } info := RuleInfo{ FilePath: filePath, Enabled: true, } // 从文件名提取规则名 baseName := filepath.Base(filePath) info.Name = strings.TrimSuffix(baseName, ".yaml") info.Name = strings.TrimPrefix(info.Name, "example-") // 解析插件配置 for _, plugin := range config.Plugins { if plugin.Type == "domain_set" { if args, ok := plugin.Args.(map[string]interface{}); ok { if files, ok := args["files"].([]interface{}); ok && len(files) > 0 { if file, ok := files[0].(string); ok { info.DomainFile = file } } } } else if plugin.Type == "sequence" { // 尝试提取 DNS 策略 if args, ok := plugin.Args.([]interface{}); ok { for _, arg := range args { if argMap, ok := arg.(map[string]interface{}); ok { if exec, ok := argMap["exec"].(string); ok { if strings.HasPrefix(exec, "$") { dnsTag := strings.TrimPrefix(exec, "$") info.DNSStrategy = m.getDNSStrategyName(dnsTag) } } } } } } else if plugin.Type == "mikrotik_addresslist" { info.EnableMikrotik = true if args, ok := plugin.Args.(map[string]interface{}); ok { if host, ok := args["host"].(string); ok { if port, ok := args["port"].(int); ok { info.MikrotikDevice = fmt.Sprintf("%s:%d", host, port) } else { info.MikrotikDevice = fmt.Sprintf("%s:9728", host) } } } } } return info, nil } // parseRuleFileToConfig 解析规则文件为完整配置 func (m *Mosdns) parseRuleFileToConfig(filePath string) (RuleConfig, error) { data, err := os.ReadFile(filePath) if err != nil { return RuleConfig{}, err } var yamlConfig struct { Plugins []PluginConfig `yaml:"plugins"` } if err := yaml.Unmarshal(data, &yamlConfig); err != nil { return RuleConfig{}, err } config := RuleConfig{ Enabled: true, } // 从文件名提取规则名 baseName := filepath.Base(filePath) config.Name = strings.TrimSuffix(baseName, ".yaml") config.Name = strings.TrimPrefix(config.Name, "example-") // 解析插件配置 for _, plugin := range yamlConfig.Plugins { if plugin.Type == "domain_set" { if args, ok := plugin.Args.(map[string]interface{}); ok { if files, ok := args["files"].([]interface{}); ok && len(files) > 0 { if file, ok := files[0].(string); ok { config.DomainFile = file } } } } else if plugin.Type == "mikrotik_addresslist" { config.EnableMikrotik = true if args, ok := plugin.Args.(map[string]interface{}); ok { config.MikrotikConfig = MikrotikConfig{ Host: getStringValue(args, "host"), Port: getIntValue(args, "port"), Username: getStringValue(args, "username"), Password: getStringValue(args, "password"), AddressList: getStringValue(args, "address_list4"), Mask: getIntValue(args, "mask4"), MaxIPs: getIntValue(args, "max_ips"), CacheTTL: getIntValue(args, "cache_ttl"), TimeoutAddr: getIntValue(args, "timeout_addr"), Comment: getStringValue(args, "comment"), } } } } return config, nil } // getDNSStrategyName 将 DNS 标签转换为策略名称 func (m *Mosdns) getDNSStrategyName(dnsTag string) string { switch dnsTag { case "china-dns": return "china" case "overseas-dns-cloudflare": return "cloudflare" case "overseas-dns-google": return "google" case "hybrid-dns": return "hybrid" case "smart_anti_pollution": return "anti-pollution" default: return "china" } } // getStringValue 和 getIntValue 已在 config_builder.go 中定义