mosdns/coremain/rule_handlers.go

683 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <https://www.gnu.org/licenses/>.
*/
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, err := m.findRuleFile(name)
if err != nil {
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, err := m.findRuleFile(name)
if err != nil {
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),
zap.String("file", filePath))
m.writeJSONResponse(w, APIResponse{
Success: true,
Message: "规则删除成功,请重启服务使其生效",
})
}
// findRuleFile 查找规则文件(支持多种文件名格式)
// 优先级:{name}.yaml > example-{name}.yaml > {name}-rule.yaml
func (m *Mosdns) findRuleFile(name string) (string, error) {
rulesDir := "./config.d/rules"
// 尝试的文件名模式(按优先级)
patterns := []string{
fmt.Sprintf("%s.yaml", name), // 直接匹配
fmt.Sprintf("example-%s.yaml", name), // 示例前缀
fmt.Sprintf("%s-rule.yaml", name), // 规则后缀
}
for _, pattern := range patterns {
filePath := filepath.Join(rulesDir, pattern)
if _, err := os.Stat(filePath); err == nil {
return filePath, nil
}
}
// 如果都找不到尝试模糊匹配包含name的文件
files, err := filepath.Glob(filepath.Join(rulesDir, "*.yaml"))
if err != nil {
return "", fmt.Errorf("规则文件不存在")
}
for _, file := range files {
baseName := filepath.Base(file)
// 去掉常见前缀和后缀后检查
cleanName := strings.TrimSuffix(baseName, ".yaml")
cleanName = strings.TrimPrefix(cleanName, "example-")
cleanName = strings.TrimSuffix(cleanName, "-rule")
if cleanName == name {
return file, nil
}
}
return "", fmt.Errorf("规则文件不存在")
}
// 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 中定义