commit 0072e8a5c2363d66ca913036d76be4b8c4ddc31a Author: Alex Yang Date: Sun Nov 23 18:21:29 2025 +0800 新建DNS服务器 diff --git a/config.json b/config.json new file mode 100644 index 0000000..318c696 --- /dev/null +++ b/config.json @@ -0,0 +1,27 @@ +{ + "dns": { + "port": 53, + "upstreamDNS": ["223.5.5.5:53", "223.6.6.6:53"], + "timeout": 5000 + }, + "http": { + "port": 8080, + "host": "0.0.0.0", + "enableAPI": true + }, + "shield": { + "localRulesFile": "rules.txt", + "remoteRules": [ + "https://example.com/rules.txt" + ], + "updateInterval": 3600, + "hostsFile": "hosts.txt" + }, + "log": { + "file": "dns-server.log", + "level": "debug", + "maxSize": 100, + "maxBackups": 10, + "maxAge": 30 + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..59b406e --- /dev/null +++ b/config/config.go @@ -0,0 +1,86 @@ +package config + +import ( + "encoding/json" + "io/ioutil" +) + +// DNSConfig DNS服务器配置 +type DNSConfig struct { + Port int `json:"port"` + UpstreamDNS []string `json:"upstreamDNS"` + Timeout int `json:"timeout"` +} + +// HTTPConfig HTTP控制台配置 +type HTTPConfig struct { + Port int `json:"port"` + Host string `json:"host"` + EnableAPI bool `json:"enableAPI"` +} + +// ShieldConfig 屏蔽规则配置 +type ShieldConfig struct { + LocalRulesFile string `json:"localRulesFile"` + RemoteRules []string `json:"remoteRules"` + UpdateInterval int `json:"updateInterval"` + HostsFile string `json:"hostsFile"` + BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP" + CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用 +} + +// LogConfig 日志配置 +type LogConfig struct { + File string `json:"file"` + Level string `json:"level"` + MaxSize int `json:"maxSize"` + MaxBackups int `json:"maxBackups"` + MaxAge int `json:"maxAge"` +} + +// Config 整体配置 +type Config struct { + DNS DNSConfig `json:"dns"` + HTTP HTTPConfig `json:"http"` + Shield ShieldConfig `json:"shield"` + Log LogConfig `json:"log"` +} + +// LoadConfig 加载配置文件 +func LoadConfig(path string) (*Config, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var config Config + err = json.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + // 设置默认值 + if config.DNS.Port == 0 { + config.DNS.Port = 53 + } + if len(config.DNS.UpstreamDNS) == 0 { + config.DNS.UpstreamDNS = []string{"8.8.8.8:53", "1.1.1.1:53"} + } + if config.HTTP.Port == 0 { + config.HTTP.Port = 8080 + } + if config.HTTP.Host == "" { + config.HTTP.Host = "0.0.0.0" + } + if config.Shield.UpdateInterval == 0 { + config.Shield.UpdateInterval = 3600 + } + if config.Shield.BlockMethod == "" { + config.Shield.BlockMethod = "NXDOMAIN" // 默认屏蔽方法为NXDOMAIN + } + if config.Log.Level == "" { + config.Log.Level = "info" + } + + return &config, nil +} diff --git a/dns-server b/dns-server new file mode 100755 index 0000000..7af9e36 Binary files /dev/null and b/dns-server differ diff --git a/dns-server.log b/dns-server.log new file mode 100644 index 0000000..9e0805a --- /dev/null +++ b/dns-server.log @@ -0,0 +1,133 @@ +time="2025-11-23T18:04:23+08:00" level=error msg="获取远程规则失败" error="远程服务器返回错误状态码: 404" url="https://example.com/rules.txt" +time="2025-11-23T18:04:23+08:00" level=info msg="规则加载完成,域名规则: 2, 排除规则: 0, 正则规则: 2, hosts规则: 3" +time="2025-11-23T18:04:23+08:00" level=info msg="DNS服务器已启动,监听端口: 53" +time="2025-11-23T18:04:23+08:00" level=info msg="HTTP控制台已启动,监听端口: 8080" +time="2025-11-23T18:04:23+08:00" level=info msg="DNS TCP服务器启动,监听端口: 53" +time="2025-11-23T18:04:23+08:00" level=info msg="HTTP控制台服务器启动,监听地址: 0.0.0.0:8080" +time="2025-11-23T18:04:23+08:00" level=info msg="DNS UDP服务器启动,监听端口: 53" +time="2025-11-23T18:05:21+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:60351" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:05:21+08:00" level=debug msg="DNS查询成功" domain=adjust.net.amazehome.xyz rtt=30.003763ms server="223.5.5.5:53" +time="2025-11-23T18:05:21+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:60352" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:05:21+08:00" level=debug msg="DNS查询成功" domain=adjust.net.amazehome.xyz rtt=40.103225ms server="223.6.6.6:53" +time="2025-11-23T18:05:21+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:60353" domain=adjust.net type=1 +time="2025-11-23T18:05:21+08:00" level=debug msg="DNS查询成功" domain=adjust.net rtt=475.840213ms server="223.5.5.5:53" +time="2025-11-23T18:05:21+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:51453" domain=adjust.net type=28 +time="2025-11-23T18:05:22+08:00" level=debug msg="DNS查询成功" domain=adjust.net rtt=352.101947ms server="223.5.5.5:53" +time="2025-11-23T18:05:37+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52048" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:05:37+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52048" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:37+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52049" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:05:37+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52049" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:37+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52050" domain=adjust.net type=1 +time="2025-11-23T18:05:37+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52050" domain=adjust.net +time="2025-11-23T18:05:37+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52051" domain=adjust.net type=28 +time="2025-11-23T18:05:37+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52051" domain=adjust.net +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59825" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:59825" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59826" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:59826" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59827" domain=adjust.net type=1 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:59827" domain=adjust.net +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59828" domain=adjust.net type=28 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:59828" domain=adjust.net +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59829" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:59829" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59830" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:59830" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59831" domain=adjust.net type=1 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:59831" domain=adjust.net +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59832" domain=adjust.net type=28 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:59832" domain=adjust.net +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:61343" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:61343" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:61344" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:61344" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:61345" domain=adjust.net type=1 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:61345" domain=adjust.net +time="2025-11-23T18:05:38+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:61346" domain=adjust.net type=28 +time="2025-11-23T18:05:38+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:61346" domain=adjust.net +time="2025-11-23T18:05:42+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:63485" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:05:42+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:63485" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:42+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:63486" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:05:42+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:63486" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:42+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:63487" domain=adjust.net type=1 +time="2025-11-23T18:05:42+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:63487" domain=adjust.net +time="2025-11-23T18:05:42+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:63488" domain=adjust.net type=28 +time="2025-11-23T18:05:42+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:63488" domain=adjust.net +time="2025-11-23T18:05:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52914" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:05:43+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52914" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52915" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:05:43+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52915" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52916" domain=adjust.net type=1 +time="2025-11-23T18:05:43+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52916" domain=adjust.net +time="2025-11-23T18:05:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52917" domain=adjust.net type=28 +time="2025-11-23T18:05:43+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52917" domain=adjust.net +time="2025-11-23T18:05:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52918" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:05:43+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52918" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52919" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:05:43+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52919" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:05:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52920" domain=adjust.net type=1 +time="2025-11-23T18:05:43+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52920" domain=adjust.net +time="2025-11-23T18:05:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:52921" domain=adjust.net type=28 +time="2025-11-23T18:05:43+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:52921" domain=adjust.net +time="2025-11-23T18:06:49+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59892" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:06:49+08:00" level=debug msg="DNS查询成功" domain=adjust.net.amazehome.xyz rtt=4.586467ms server="223.5.5.5:53" +time="2025-11-23T18:06:49+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59893" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:06:50+08:00" level=debug msg="DNS查询成功" domain=adjust.net.amazehome.xyz rtt=26.530637ms server="223.5.5.5:53" +time="2025-11-23T18:06:50+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59894" domain=adjust.net type=1 +time="2025-11-23T18:06:50+08:00" level=debug msg="DNS查询成功" domain=adjust.net rtt=5.219233ms server="223.5.5.5:53" +time="2025-11-23T18:06:50+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:59895" domain=adjust.net type=28 +time="2025-11-23T18:06:50+08:00" level=debug msg="DNS查询成功" domain=adjust.net rtt=14.858315ms server="223.5.5.5:53" +time="2025-11-23T18:07:01+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:54828" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:07:01+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:54828" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:07:01+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:54829" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:07:01+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:54829" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:07:01+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:54830" domain=adjust.net type=1 +time="2025-11-23T18:07:01+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:54830" domain=adjust.net +time="2025-11-23T18:07:01+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:54831" domain=adjust.net type=28 +time="2025-11-23T18:07:01+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:54831" domain=adjust.net +time="2025-11-23T18:07:01+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:54832" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:07:01+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:54832" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:07:01+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:54833" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:07:01+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:54833" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:07:01+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:54834" domain=adjust.net type=1 +time="2025-11-23T18:07:01+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:54834" domain=adjust.net +time="2025-11-23T18:07:01+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:54835" domain=adjust.net type=28 +time="2025-11-23T18:07:01+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:54835" domain=adjust.net +time="2025-11-23T18:07:02+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:57109" domain=adjust.net.amazehome.xyz type=1 +time="2025-11-23T18:07:02+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:57109" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:07:02+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:57110" domain=adjust.net.amazehome.xyz type=28 +time="2025-11-23T18:07:02+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:57110" domain=adjust.net.amazehome.xyz +time="2025-11-23T18:07:02+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:57111" domain=adjust.net type=1 +time="2025-11-23T18:07:02+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:57111" domain=adjust.net +time="2025-11-23T18:07:02+08:00" level=debug msg="接收到DNS查询" client="10.35.10.78:57112" domain=adjust.net type=28 +time="2025-11-23T18:07:02+08:00" level=info msg="域名被屏蔽" client="10.35.10.78:57112" domain=adjust.net +time="2025-11-23T18:07:15+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:32001" domain=apd-pcdnwxstat.teg.tencent-cloud.net type=1 +time="2025-11-23T18:07:15+08:00" level=debug msg="DNS查询成功" domain=apd-pcdnwxstat.teg.tencent-cloud.net rtt=6.522705ms server="223.5.5.5:53" +time="2025-11-23T18:08:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:49391" domain=magazine-drcn.theme.dbankcloud.cn type=1 +time="2025-11-23T18:08:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:26101" domain=api-drcn.theme.dbankcloud.cn type=1 +time="2025-11-23T18:08:43+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:37270" domain=contentcenter-drcn.dbankcdn.cn type=1 +time="2025-11-23T18:08:43+08:00" level=debug msg="DNS查询成功" domain=contentcenter-drcn.dbankcdn.cn rtt=4.599463ms server="223.5.5.5:53" +time="2025-11-23T18:08:43+08:00" level=debug msg="DNS查询成功" domain=magazine-drcn.theme.dbankcloud.cn rtt=5.641158ms server="223.5.5.5:53" +time="2025-11-23T18:08:43+08:00" level=debug msg="DNS查询成功" domain=api-drcn.theme.dbankcloud.cn rtt=5.674991ms server="223.5.5.5:53" +time="2025-11-23T18:08:44+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:65533" domain=tsms-drcn.security.dbankcloud.cn type=1 +time="2025-11-23T18:08:44+08:00" level=debug msg="DNS查询成功" domain=tsms-drcn.security.dbankcloud.cn rtt=4.626637ms server="223.5.5.5:53" +time="2025-11-23T18:08:46+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:4437" domain=events.op.hicloud.com type=1 +time="2025-11-23T18:08:46+08:00" level=debug msg="DNS查询成功" domain=events.op.hicloud.com rtt=5.908197ms server="223.5.5.5:53" +time="2025-11-23T18:08:47+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:34826" domain=metrics1-drcn.dt.dbankcloud.cn type=1 +time="2025-11-23T18:08:47+08:00" level=debug msg="DNS查询成功" domain=metrics1-drcn.dt.dbankcloud.cn rtt=6.027459ms server="223.5.5.5:53" +time="2025-11-23T18:08:47+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:31858" domain=sdkserver.op.hicloud.com type=1 +time="2025-11-23T18:08:47+08:00" level=debug msg="DNS查询成功" domain=sdkserver.op.hicloud.com rtt=19.413443ms server="223.5.5.5:53" +time="2025-11-23T18:08:47+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:12581" domain=acd.op.hicloud.com type=1 +time="2025-11-23T18:08:47+08:00" level=debug msg="DNS查询成功" domain=acd.op.hicloud.com rtt=6.202825ms server="223.5.5.5:53" +time="2025-11-23T18:08:48+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:4139" domain=api.cloud.huawei.com type=1 +time="2025-11-23T18:08:48+08:00" level=debug msg="DNS查询成功" domain=api.cloud.huawei.com rtt=5.729772ms server="223.5.5.5:53" +time="2025-11-23T18:08:48+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:1352" domain=datacollabo-drcn.platform.dbankcloud.cn type=1 +time="2025-11-23T18:08:48+08:00" level=debug msg="DNS查询成功" domain=datacollabo-drcn.platform.dbankcloud.cn rtt=5.429196ms server="223.5.5.5:53" +time="2025-11-23T18:08:48+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:51418" domain=rcm-cus-drcn.platform.dbankcloud.cn type=1 +time="2025-11-23T18:08:48+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:45468" domain=abt-drcn.platform.dbankcloud.com type=1 +time="2025-11-23T18:08:48+08:00" level=debug msg="DNS查询成功" domain=rcm-cus-drcn.platform.dbankcloud.cn rtt=6.646444ms server="223.5.5.5:53" +time="2025-11-23T18:08:48+08:00" level=debug msg="DNS查询成功" domain=abt-drcn.platform.dbankcloud.com rtt=7.082723ms server="223.5.5.5:53" +time="2025-11-23T18:08:48+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:31362" domain=h5hosting.dbankcdn.com type=1 +time="2025-11-23T18:08:48+08:00" level=debug msg="DNS查询成功" domain=h5hosting.dbankcdn.com rtt=7.025853ms server="223.5.5.5:53" +time="2025-11-23T18:08:48+08:00" level=debug msg="接收到DNS查询" client="10.35.10.11:12618" domain=metrics1.data.hicloud.com type=1 +time="2025-11-23T18:08:48+08:00" level=debug msg="DNS查询成功" domain=metrics1.data.hicloud.com rtt=5.85395ms server="223.5.5.5:53" diff --git a/dns/server.go b/dns/server.go new file mode 100644 index 0000000..b23f016 --- /dev/null +++ b/dns/server.go @@ -0,0 +1,396 @@ +package dns + +import ( + "context" + "fmt" + "net" + "sort" + "sync" + "time" + + "dns-server/config" + "dns-server/logger" + "dns-server/shield" + + "github.com/miekg/dns" +) + +// BlockedDomain 屏蔽域名统计 + +type BlockedDomain struct { + Domain string + Count int64 + LastSeen time.Time +} + +// Server DNS服务器 +type Server struct { + config *config.DNSConfig + shieldConfig *config.ShieldConfig + shieldManager *shield.ShieldManager + server *dns.Server + resolver *dns.Client + ctx context.Context + cancel context.CancelFunc + statsMutex sync.Mutex + stats *Stats + blockedDomainsMutex sync.RWMutex + blockedDomains map[string]*BlockedDomain + hourlyStatsMutex sync.RWMutex + hourlyStats map[string]int64 // 按小时统计屏蔽数量 +} + +// Stats DNS服务器统计信息 +type Stats struct { + Queries int64 + Blocked int64 + Allowed int64 + Errors int64 + LastQuery time.Time +} + +// NewServer 创建DNS服务器实例 +func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shieldManager *shield.ShieldManager) *Server { + ctx, cancel := context.WithCancel(context.Background()) + return &Server{ + config: config, + shieldConfig: shieldConfig, + shieldManager: shieldManager, + resolver: &dns.Client{ + Net: "udp", + Timeout: time.Duration(config.Timeout) * time.Millisecond, + }, + ctx: ctx, + cancel: cancel, + stats: &Stats{ + Queries: 0, + Blocked: 0, + Allowed: 0, + Errors: 0, + }, + blockedDomains: make(map[string]*BlockedDomain), + hourlyStats: make(map[string]int64), + } +} + +// Start 启动DNS服务器 +func (s *Server) Start() error { + s.server = &dns.Server{ + Addr: fmt.Sprintf(":%d", s.config.Port), + Net: "udp", + Handler: dns.HandlerFunc(s.handleDNSRequest), + } + + // 启动TCP服务器(用于大型响应) + tcpServer := &dns.Server{ + Addr: fmt.Sprintf(":%d", s.config.Port), + Net: "tcp", + Handler: dns.HandlerFunc(s.handleDNSRequest), + } + + // 启动UDP服务 + go func() { + logger.Info(fmt.Sprintf("DNS UDP服务器启动,监听端口: %d", s.config.Port)) + if err := s.server.ListenAndServe(); err != nil { + logger.Error("DNS UDP服务器启动失败", "error", err) + s.cancel() + } + }() + + // 启动TCP服务 + go func() { + logger.Info(fmt.Sprintf("DNS TCP服务器启动,监听端口: %d", s.config.Port)) + if err := tcpServer.ListenAndServe(); err != nil { + logger.Error("DNS TCP服务器启动失败", "error", err) + s.cancel() + } + }() + + // 等待停止信号 + <-s.ctx.Done() + return nil +} + +// Stop 停止DNS服务器 +func (s *Server) Stop() { + if s.server != nil { + s.server.Shutdown() + } + s.cancel() + logger.Info("DNS服务器已停止") +} + +// handleDNSRequest 处理DNS请求 +func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { + s.updateStats(func(stats *Stats) { + stats.Queries++ + stats.LastQuery = time.Now() + }) + + // 只处理递归查询 + if r.RecursionDesired == false { + response := new(dns.Msg) + response.SetReply(r) + response.RecursionAvailable = true + response.SetRcode(r, dns.RcodeRefused) + w.WriteMsg(response) + return + } + + // 获取查询域名 + var domain string + if len(r.Question) > 0 { + domain = r.Question[0].Name + // 移除末尾的点 + if len(domain) > 0 && domain[len(domain)-1] == '.' { + domain = domain[:len(domain)-1] + } + } + + logger.Debug("接收到DNS查询", "domain", domain, "type", r.Question[0].Qtype, "client", w.RemoteAddr()) + + // 检查hosts文件是否有匹配 + if ip, exists := s.shieldManager.GetHostsIP(domain); exists { + s.handleHostsResponse(w, r, ip) + return + } + + // 检查是否被屏蔽 + if s.shieldManager.IsBlocked(domain) { + s.handleBlockedResponse(w, r, domain) + return + } + + // 转发到上游DNS服务器 + s.forwardDNSRequest(w, r, domain) +} + +// handleHostsResponse 处理hosts文件匹配的响应 +func (s *Server) handleHostsResponse(w dns.ResponseWriter, r *dns.Msg, ip string) { + response := new(dns.Msg) + response.SetReply(r) + response.RecursionAvailable = true + + if len(r.Question) > 0 { + q := r.Question[0] + answer := new(dns.A) + answer.Hdr = dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 300, + } + answer.A = net.ParseIP(ip) + response.Answer = append(response.Answer, answer) + } + + w.WriteMsg(response) + s.updateStats(func(stats *Stats) { + stats.Allowed++ + }) +} + +// handleBlockedResponse 处理被屏蔽的域名响应 +func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain string) { + logger.Info("域名被屏蔽", "domain", domain, "client", w.RemoteAddr()) + + // 更新被屏蔽域名统计 + s.updateBlockedDomainStats(domain) + + // 更新总体统计 + s.updateStats(func(stats *Stats) { + stats.Blocked++ + }) + + response := new(dns.Msg) + response.SetReply(r) + response.RecursionAvailable = true + + // 获取屏蔽方法配置 + blockMethod := "NXDOMAIN" // 默认值 + customBlockIP := "" // 默认值 + + // 从Server结构体的shieldConfig字段获取配置 + if s.shieldConfig != nil { + blockMethod = s.shieldConfig.BlockMethod + customBlockIP = s.shieldConfig.CustomBlockIP + } + + // 根据屏蔽方法返回不同的响应 + switch blockMethod { + case "refused": + // 返回拒绝查询响应 + response.SetRcode(r, dns.RcodeRefused) + case "emptyIP": + // 返回空IP响应 + if len(r.Question) > 0 && r.Question[0].Qtype == dns.TypeA { + answer := new(dns.A) + answer.Hdr = dns.RR_Header{ + Name: r.Question[0].Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 300, + } + answer.A = net.ParseIP("0.0.0.0") // 空IP + response.Answer = append(response.Answer, answer) + } + case "customIP": + // 返回自定义IP响应 + if len(r.Question) > 0 && r.Question[0].Qtype == dns.TypeA && customBlockIP != "" { + answer := new(dns.A) + answer.Hdr = dns.RR_Header{ + Name: r.Question[0].Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 300, + } + answer.A = net.ParseIP(customBlockIP) + response.Answer = append(response.Answer, answer) + } + case "NXDOMAIN", "": + fallthrough // 默认使用NXDOMAIN + default: + // 返回NXDOMAIN响应(域名不存在) + response.SetRcode(r, dns.RcodeNameError) + } + + w.WriteMsg(response) + s.updateStats(func(stats *Stats) { + stats.Blocked++ + }) +} + +// forwardDNSRequest 转发DNS请求到上游服务器 +func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain string) { + // 尝试所有上游DNS服务器 + for _, upstream := range s.config.UpstreamDNS { + response, rtt, err := s.resolver.Exchange(r, upstream) + if err == nil && response != nil && response.Rcode == dns.RcodeSuccess { + // 设置递归可用标志 + response.RecursionAvailable = true + + w.WriteMsg(response) + logger.Debug("DNS查询成功", "domain", domain, "rtt", rtt, "server", upstream) + + s.updateStats(func(stats *Stats) { + stats.Allowed++ + }) + return + } + } + + // 所有上游服务器都失败,返回服务器失败错误 + response := new(dns.Msg) + response.SetReply(r) + response.RecursionAvailable = true + response.SetRcode(r, dns.RcodeServerFailure) + w.WriteMsg(response) + + logger.Error("DNS查询失败", "domain", domain) + s.updateStats(func(stats *Stats) { + stats.Errors++ + }) +} + +// updateBlockedDomainStats 更新被屏蔽域名统计 +func (s *Server) updateBlockedDomainStats(domain string) { + // 更新被屏蔽域名计数 + s.blockedDomainsMutex.Lock() + defer s.blockedDomainsMutex.Unlock() + + if entry, exists := s.blockedDomains[domain]; exists { + entry.Count++ + entry.LastSeen = time.Now() + } else { + s.blockedDomains[domain] = &BlockedDomain{ + Domain: domain, + Count: 1, + LastSeen: time.Now(), + } + } + + // 更新小时统计 + hourKey := time.Now().Format("2006-01-02-15") + s.hourlyStats[hourKey]++ +} + +// updateStats 更新统计信息 +func (s *Server) updateStats(update func(*Stats)) { + s.statsMutex.Lock() + defer s.statsMutex.Unlock() + update(s.stats) +} + +// GetStats 获取DNS服务器统计信息 +func (s *Server) GetStats() *Stats { + s.statsMutex.Lock() + defer s.statsMutex.Unlock() + + // 返回统计信息的副本 + return &Stats{ + Queries: s.stats.Queries, + Blocked: s.stats.Blocked, + Allowed: s.stats.Allowed, + Errors: s.stats.Errors, + LastQuery: s.stats.LastQuery, + } +} + +// GetTopBlockedDomains 获取TOP屏蔽域名列表 +func (s *Server) GetTopBlockedDomains(limit int) []BlockedDomain { + s.blockedDomainsMutex.RLock() + defer s.blockedDomainsMutex.RUnlock() + + // 转换为切片 + domains := make([]BlockedDomain, 0, len(s.blockedDomains)) + for _, entry := range s.blockedDomains { + domains = append(domains, *entry) + } + + // 按计数排序 + sort.Slice(domains, func(i, j int) bool { + return domains[i].Count > domains[j].Count + }) + + // 返回限制数量 + if len(domains) > limit { + return domains[:limit] + } + return domains +} + +// GetRecentBlockedDomains 获取最近屏蔽的域名列表 +func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain { + s.blockedDomainsMutex.RLock() + defer s.blockedDomainsMutex.RUnlock() + + // 转换为切片 + domains := make([]BlockedDomain, 0, len(s.blockedDomains)) + for _, entry := range s.blockedDomains { + domains = append(domains, *entry) + } + + // 按时间排序 + sort.Slice(domains, func(i, j int) bool { + return domains[i].LastSeen.After(domains[j].LastSeen) + }) + + // 返回限制数量 + if len(domains) > limit { + return domains[:limit] + } + return domains +} + +// GetHourlyStats 获取24小时屏蔽统计 +func (s *Server) GetHourlyStats() map[string]int64 { + s.hourlyStatsMutex.RLock() + defer s.hourlyStatsMutex.RUnlock() + + // 返回副本 + result := make(map[string]int64) + for k, v := range s.hourlyStats { + result[k] = v + } + return result +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e92e1a0 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module dns-server + +go 1.23.0 + +toolchain go1.24.10 + +require ( + github.com/miekg/dns v1.1.68 + github.com/sirupsen/logrus v1.9.3 +) + +// 清理不需要的依赖 +// 之前的go.sum可能包含lumberjack的记录,但现在已经不再使用 + +require ( + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/tools v0.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5891810 --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hosts.txt b/hosts.txt new file mode 100644 index 0000000..6f4c647 --- /dev/null +++ b/hosts.txt @@ -0,0 +1,6 @@ +# DNS Server Hosts File +# Generated by DNS Server + +::1 localhost +ad.qq.com 127.0.0.1 +ad.qq.com 0.0.0.0 \ No newline at end of file diff --git a/http/server.go b/http/server.go new file mode 100644 index 0000000..98f800a --- /dev/null +++ b/http/server.go @@ -0,0 +1,444 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "dns-server/config" + "dns-server/dns" + "dns-server/logger" + "dns-server/shield" +) + +// Server HTTP控制台服务器 +type Server struct { + globalConfig *config.Config + config *config.HTTPConfig + dnsServer *dns.Server + shieldManager *shield.ShieldManager + server *http.Server +} + +// NewServer 创建HTTP服务器实例 +func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager *shield.ShieldManager) *Server { + return &Server{ + globalConfig: globalConfig, + config: &globalConfig.HTTP, + dnsServer: dnsServer, + shieldManager: shieldManager, + } +} + +// Start 启动HTTP服务器 +func (s *Server) Start() error { + mux := http.NewServeMux() + + // API路由 + if s.config.EnableAPI { + mux.HandleFunc("/api/stats", s.handleStats) + mux.HandleFunc("/api/shield", s.handleShield) + mux.HandleFunc("/api/shield/hosts", s.handleShieldHosts) + mux.HandleFunc("/api/query", s.handleQuery) + mux.HandleFunc("/api/status", s.handleStatus) + mux.HandleFunc("/api/config", s.handleConfig) + // 添加统计相关接口 + mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains) + mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains) + mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats) + } + + // 静态文件服务(可后续添加前端界面) + mux.Handle("/", http.FileServer(http.Dir("./static"))) + + s.server = &http.Server{ + Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + + logger.Info(fmt.Sprintf("HTTP控制台服务器启动,监听地址: %s:%d", s.config.Host, s.config.Port)) + return s.server.ListenAndServe() +} + +// Stop 停止HTTP服务器 +func (s *Server) Stop() { + if s.server != nil { + s.server.Close() + } + logger.Info("HTTP控制台服务器已停止") +} + +// handleStats 处理统计信息请求 +func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + dnsStats := s.dnsServer.GetStats() + shieldStats := s.shieldManager.GetStats() + + stats := map[string]interface{}{ + "dns": dnsStats, + "shield": shieldStats, + "time": time.Now(), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(stats) +} + +// handleTopBlockedDomains 处理TOP屏蔽域名请求 +func (s *Server) handleTopBlockedDomains(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + domains := s.dnsServer.GetTopBlockedDomains(10) + + // 转换为前端需要的格式 + result := make([]map[string]interface{}, len(domains)) + for i, domain := range domains { + result[i] = map[string]interface{}{ + "domain": domain.Domain, + "count": domain.Count, + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// handleRecentBlockedDomains 处理最近屏蔽域名请求 +func (s *Server) handleRecentBlockedDomains(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + domains := s.dnsServer.GetRecentBlockedDomains(10) + + // 转换为前端需要的格式 + result := make([]map[string]interface{}, len(domains)) + for i, domain := range domains { + result[i] = map[string]interface{}{ + "domain": domain.Domain, + "time": domain.LastSeen.Format("15:04:05"), + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// handleHourlyStats 处理24小时统计请求 +func (s *Server) handleHourlyStats(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + hourlyStats := s.dnsServer.GetHourlyStats() + + // 生成最近24小时的数据 + now := time.Now() + labels := make([]string, 24) + data := make([]int64, 24) + + for i := 23; i >= 0; i-- { + hour := now.Add(time.Duration(-i) * time.Hour) + hourKey := hour.Format("2006-01-02-15") + labels[23-i] = hour.Format("15:00") + data[23-i] = hourlyStats[hourKey] + } + + result := map[string]interface{}{ + "labels": labels, + "data": data, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// handleShield 处理屏蔽规则管理请求 +func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // 处理hosts管理子路由 + if strings.HasPrefix(r.URL.Path, "/api/shield/hosts") { + s.handleShieldHosts(w, r) + return + } + + switch r.Method { + case http.MethodGet: + // 获取完整规则列表 + rules := s.shieldManager.GetRules() + json.NewEncoder(w).Encode(rules) + + case http.MethodPost: + // 添加屏蔽规则 + var req struct { + Rule string `json:"rule"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := s.shieldManager.AddRule(req.Rule); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + + case http.MethodDelete: + // 删除屏蔽规则 + var req struct { + Rule string `json:"rule"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := s.shieldManager.RemoveRule(req.Rule); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + + case http.MethodPut: + // 重新加载规则 + if err := s.shieldManager.LoadRules(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "规则重新加载成功"}) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// handleShieldHosts 处理hosts管理请求 +func (s *Server) handleShieldHosts(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodPost: + // 添加hosts条目 + var req struct { + IP string `json:"ip"` + Domain string `json:"domain"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if req.IP == "" || req.Domain == "" { + http.Error(w, "IP and Domain are required", http.StatusBadRequest) + return + } + + if err := s.shieldManager.AddHostsEntry(req.IP, req.Domain); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + + case http.MethodDelete: + // 删除hosts条目 + var req struct { + Domain string `json:"domain"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := s.shieldManager.RemoveHostsEntry(req.Domain); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + + case http.MethodGet: + // 获取hosts条目列表 + // 注意:这需要在shieldManager中添加一个获取所有hosts条目的方法 + // 暂时返回统计信息 + stats := s.shieldManager.GetStats() + json.NewEncoder(w).Encode(map[string]interface{}{ + "hostsCount": stats["hostsRules"], + "message": "获取hosts列表功能待实现", + }) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// handleQuery 处理DNS查询请求 +func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + domain := r.URL.Query().Get("domain") + if domain == "" { + http.Error(w, "Domain parameter is required", http.StatusBadRequest) + return + } + + // 检查域名是否被屏蔽 + blocked := s.shieldManager.IsBlocked(domain) + + // 检查hosts文件是否有匹配 + hostsIP, hasHosts := s.shieldManager.GetHostsIP(domain) + + result := map[string]interface{}{ + "domain": domain, + "blocked": blocked, + "hasHosts": hasHosts, + "hostsIP": hostsIP, + "timestamp": time.Now(), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// handleStatus 处理系统状态请求 +func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + stats := s.dnsServer.GetStats() + status := map[string]interface{}{ + "status": "running", + "queries": stats.Queries, + "lastQuery": stats.LastQuery, + "uptime": time.Since(stats.LastQuery), + "timestamp": time.Now(), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(status) +} + +// handleConfig 处理配置请求 +func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodGet: + // 返回当前配置(只返回必要的部分) + config := map[string]interface{}{ + "shield": map[string]string{ + "blockMethod": s.globalConfig.Shield.BlockMethod, + "customBlockIP": s.globalConfig.Shield.CustomBlockIP, + }, + } + json.NewEncoder(w).Encode(config) + + case http.MethodPost: + // 更新配置 + var req struct { + Shield struct { + BlockMethod string `json:"blockMethod"` + CustomBlockIP string `json:"customBlockIP"` + } `json:"shield"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "无效的请求体", http.StatusBadRequest) + return + } + + // 更新屏蔽配置 + if req.Shield.BlockMethod != "" { + // 验证屏蔽方法是否有效 + validMethods := map[string]bool{ + "NXDOMAIN": true, + "refused": true, + "emptyIP": true, + "customIP": true, + } + + if !validMethods[req.Shield.BlockMethod] { + http.Error(w, "无效的屏蔽方法", http.StatusBadRequest) + return + } + + s.globalConfig.Shield.BlockMethod = req.Shield.BlockMethod + + // 如果选择了customIP,验证IP地址 + if req.Shield.BlockMethod == "customIP" { + if req.Shield.CustomBlockIP == "" { + http.Error(w, "自定义IP不能为空", http.StatusBadRequest) + return + } + // 简单的IP地址验证 + if !isValidIP(req.Shield.CustomBlockIP) { + http.Error(w, "无效的IP地址", http.StatusBadRequest) + return + } + } + } + + if req.Shield.CustomBlockIP != "" { + s.globalConfig.Shield.CustomBlockIP = req.Shield.CustomBlockIP + } + + // 返回成功响应 + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "配置已更新", + }) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// isValidIP 简单验证IP地址格式 +func isValidIP(ip string) bool { + // 简单的IPv4地址验证 + parts := strings.Split(ip, ".") + if len(parts) != 4 { + return false + } + + for _, part := range parts { + // 检查是否为数字 + for _, char := range part { + if char < '0' || char > '9' { + return false + } + } + // 检查数字范围 + var num int + if _, err := fmt.Sscanf(part, "%d", &num); err != nil || num < 0 || num > 255 { + return false + } + } + return true +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..da04325 --- /dev/null +++ b/index.html @@ -0,0 +1,1574 @@ + + + + + + DNS服务器管理中心 + + + + + + +
+
+

DNS服务器管理中心

+

高性能DNS服务器,支持规则屏蔽和Hosts管理

+
+ +
+
+ + + + +
+ + +
+

服务器状态

+
+
+ +
--
+
屏蔽规则数
+
+ +
+
+
+ +
--
+
排除规则数
+
+ +
+
+
+ +
--
+
Hosts条目数
+
+ +
+
+
+ +
--
+
DNS查询次数
+
+ +
+
+
+ +
--
+
屏蔽次数
+
+ +
+
+
+ +
+
+

24小时屏蔽统计

+
+
+
+ +
+
+
+
+

服务器信息

+

服务器地址: --

+

当前时间: --

+

运行状态: 正常运行

+
+
+ + +
+
+
+

屏蔽设置

+
+
+
+ + + +
+ + NXDOMAIN: 返回域名不存在错误
+ refused: 返回查询拒绝错误
+ emptyIP: 返回0.0.0.0
+ customIP: 返回自定义IP地址 +
+
+
+
+
+

添加屏蔽规则

+
+
+
+ + + +
+ 支持AdGuardHome规则格式:域名规则(||example.com^)、排除规则(@@||example.com^)、正则规则(/regex/)、通配符规则(*example.com)等 +
+
+
+
+

规则列表

+
+
+
+
+ +

规则列表加载中...

+
+
+
+
+
+ + +
+
+
+

添加Hosts条目

+
+
+
+ + + +
+
+
+
+
+

当前Hosts条目

+
+
+
+
+ +

Hosts列表加载中...

+
+
+
+
+
+ + +
+
+
+

DNS查询

+
+
+
+ + +
+
+
+
+
+

查询结果

+
+
+
请输入域名并点击查询按钮
+
+
+
+
+
+ + + + diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..0d3e9c2 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,134 @@ +package logger + +import ( + "io" + "os" + "sync" + + "github.com/sirupsen/logrus" +) + +var ( + log *logrus.Logger + logMutex sync.Mutex + initialized bool +) + +// InitLogger 初始化日志系统 +func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error { + logMutex.Lock() + defer logMutex.Unlock() + + if initialized { + return nil + } + + log = logrus.New() + + // 配置日志格式 + log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + }) + + // 设置输出目标 + if logFile != "" { + // 使用标准库打开文件,支持追加写入 + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + logrus.Warn("无法打开日志文件,将使用标准输出", "error", err) + log.SetOutput(os.Stdout) + } else { + // 同时输出到文件和标准输出 + log.SetOutput(io.MultiWriter(file, os.Stdout)) + } + } else { + log.SetOutput(os.Stdout) + } + + // 设置日志级别 + logLevel, err := logrus.ParseLevel(level) + if err != nil { + logLevel = logrus.InfoLevel + } + log.SetLevel(logLevel) + + initialized = true + return nil +} + +// Close 关闭日志系统 +func Close() { + logMutex.Lock() + defer logMutex.Unlock() + + if !initialized { + return + } + + // 执行日志刷新 + log.Warn("日志系统已关闭") + initialized = false +} + +// Info 记录信息级别日志 +func Info(msg string, fields ...interface{}) { + if !initialized { + return + } + + if len(fields) > 0 { + log.WithFields(toFields(fields)).Info(msg) + } else { + log.Info(msg) + } +} + +// Error 记录错误级别日志 +func Error(msg string, fields ...interface{}) { + if !initialized { + return + } + + if len(fields) > 0 { + log.WithFields(toFields(fields)).Error(msg) + } else { + log.Error(msg) + } +} + +// Debug 记录调试级别日志 +func Debug(msg string, fields ...interface{}) { + if !initialized { + return + } + + if len(fields) > 0 { + log.WithFields(toFields(fields)).Debug(msg) + } else { + log.Debug(msg) + } +} + +// Warn 记录警告级别日志 +func Warn(msg string, fields ...interface{}) { + if !initialized { + return + } + + if len(fields) > 0 { + log.WithFields(toFields(fields)).Warn(msg) + } else { + log.Warn(msg) + } +} + +// toFields 将键值对转换为logrus字段 +func toFields(keyValues []interface{}) logrus.Fields { + fields := make(logrus.Fields) + for i := 0; i < len(keyValues); i += 2 { + if i+1 < len(keyValues) { + fields[keyValues[i].(string)] = keyValues[i+1] + } + } + return fields +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..6f253e4 --- /dev/null +++ b/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "dns-server/config" + "dns-server/dns" + "dns-server/http" + "dns-server/logger" + "dns-server/shield" +) + +func main() { + // 解析命令行参数 + configPath := flag.String("config", "config.json", "配置文件路径") + flag.Parse() + + // 初始化配置 + cfg, err := config.LoadConfig(*configPath) + if err != nil { + log.Fatalf("加载配置失败: %v", err) + } + + // 初始化日志系统 + if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil { + log.Fatalf("初始化日志系统失败: %v", err) + } + defer logger.Close() + + // 初始化屏蔽管理系统 + shieldManager := shield.NewShieldManager(&cfg.Shield) + if err := shieldManager.LoadRules(); err != nil { + logger.Error("加载屏蔽规则失败", "error", err) + } + + // 启动DNS服务器 + dnsServer := dns.NewServer(&cfg.DNS, &cfg.Shield, shieldManager) + go func() { + if err := dnsServer.Start(); err != nil { + logger.Error("DNS服务器启动失败", "error", err) + os.Exit(1) + } + }() + + // 启动HTTP控制台服务器 + httpServer := http.NewServer(cfg, dnsServer, shieldManager) + go func() { + if err := httpServer.Start(); err != nil { + logger.Error("HTTP控制台服务器启动失败", "error", err) + } + }() + + // 启动定时更新任务 + go shieldManager.StartAutoUpdate() + + logger.Info(fmt.Sprintf("DNS服务器已启动,监听端口: %d", cfg.DNS.Port)) + logger.Info(fmt.Sprintf("HTTP控制台已启动,监听端口: %d", cfg.HTTP.Port)) + + // 等待退出信号 + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + logger.Info("正在关闭服务...") + dnsServer.Stop() + httpServer.Stop() + shieldManager.StopAutoUpdate() + logger.Info("所有服务已关闭") +} diff --git a/rules.txt b/rules.txt new file mode 100644 index 0000000..4865f49 --- /dev/null +++ b/rules.txt @@ -0,0 +1,3 @@ +||hm.baidu.com +/.*tracking.*/ +/adjust.net/ \ No newline at end of file diff --git a/shield/manager.go b/shield/manager.go new file mode 100644 index 0000000..68a6f7a --- /dev/null +++ b/shield/manager.go @@ -0,0 +1,719 @@ +package shield + +import ( + "bufio" + "context" + "fmt" + "io/ioutil" + "net/http" + "os" + "regexp" + "strings" + "sync" + "time" + + "dns-server/config" + "dns-server/logger" +) + +// regexRule 正则规则结构,包含编译后的表达式和原始字符串 +type regexRule struct { + pattern *regexp.Regexp + original string +} + +// ShieldManager 屏蔽管理器 +type ShieldManager struct { + config *config.ShieldConfig + domainRules map[string]bool + domainExceptions map[string]bool + regexRules []regexRule + regexExceptions []regexRule + hostsMap map[string]string + rulesMutex sync.RWMutex + updateCtx context.Context + updateCancel context.CancelFunc + updateRunning bool +} + +// NewShieldManager 创建屏蔽管理器实例 +func NewShieldManager(config *config.ShieldConfig) *ShieldManager { + ctx, cancel := context.WithCancel(context.Background()) + return &ShieldManager{ + config: config, + domainRules: make(map[string]bool), + domainExceptions: make(map[string]bool), + regexRules: []regexRule{}, + regexExceptions: []regexRule{}, + hostsMap: make(map[string]string), + updateCtx: ctx, + updateCancel: cancel, + } +} + +// 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.regexRules = []regexRule{} + m.regexExceptions = []regexRule{} + m.hostsMap = make(map[string]string) + + // 加载本地规则文件 + 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 +} + +// loadLocalRules 加载本地规则文件 +func (m *ShieldManager) loadLocalRules() error { + if m.config.LocalRulesFile == "" { + return nil + } + + file, err := os.Open(m.config.LocalRulesFile) + 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 + } + m.parseRule(line) + } + + return scanner.Err() +} + +// loadRemoteRules 加载远程规则 +func (m *ShieldManager) loadRemoteRules() error { + for _, url := range m.config.RemoteRules { + if err := m.fetchRemoteRules(url); err != nil { + logger.Error("获取远程规则失败", "url", url, "error", err) + continue + } + } + return nil +} + +// fetchRemoteRules 从远程URL获取规则 +func (m *ShieldManager) fetchRemoteRules(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("远程服务器返回错误状态码: %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + 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) + } + + return nil +} + +// loadHosts 加载hosts文件 +func (m *ShieldManager) loadHosts() error { + if m.config.HostsFile == "" { + return nil + } + + file, err := os.Open(m.config.HostsFile) + 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) { + // 处理注释 + 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) + + case strings.HasPrefix(line, "||"): + // 精确域名匹配规则 + domain := strings.TrimPrefix(line, "||") + m.addDomainRule(domain, !isException) + + case strings.HasPrefix(line, "*"): + // 通配符规则,转换为正则表达式 + pattern := strings.ReplaceAll(line, "*", ".*") + pattern = "^" + pattern + "$" + if re, err := regexp.Compile(pattern); err == nil { + // 保存原始规则字符串 + m.addRegexRule(re, line, !isException) + } + + case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"): + // 正则表达式规则 + pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/") + if re, err := regexp.Compile(pattern); err == nil { + // 保存原始规则字符串 + m.addRegexRule(re, line, !isException) + } + + 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, line, !isException) + } + + case strings.HasPrefix(line, "|"): + // URL开头匹配规则 + urlPattern := strings.TrimPrefix(line, "|") + pattern := "^" + regexp.QuoteMeta(urlPattern) + if re, err := regexp.Compile(pattern); err == nil { + m.addRegexRule(re, line, !isException) + } + + case strings.HasSuffix(line, "|"): + // URL结尾匹配规则 + urlPattern := strings.TrimSuffix(line, "|") + pattern := regexp.QuoteMeta(urlPattern) + "$" + if re, err := regexp.Compile(pattern); err == nil { + m.addRegexRule(re, line, !isException) + } + + default: + // 默认作为普通域名规则 + m.addDomainRule(line, !isException) + } +} + +// 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) { + if block { + m.domainRules[domain] = true + // 添加所有子域名的匹配支持 + parts := strings.Split(domain, ".") + if len(parts) > 1 { + // 为二级域名和顶级域名添加规则 + for i := 0; i < len(parts)-1; i++ { + subdomain := strings.Join(parts[i:], ".") + m.domainRules[subdomain] = true + } + } + } else { + // 添加到排除规则 + m.domainExceptions[domain] = true + // 为子域名也添加排除规则 + parts := strings.Split(domain, ".") + if len(parts) > 1 { + for i := 0; i < len(parts)-1; i++ { + subdomain := strings.Join(parts[i:], ".") + m.domainExceptions[subdomain] = true + } + } + } +} + +// addRegexRule 添加正则表达式规则,支持是否为阻止规则 +func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool) { + rule := regexRule{ + pattern: re, + original: original, + } + if block { + m.regexRules = append(m.regexRules, rule) + } else { + // 添加到排除规则 + m.regexExceptions = append(m.regexExceptions, rule) + } +} + +// IsBlocked 检查域名是否被屏蔽 +func (m *ShieldManager) IsBlocked(domain string) bool { + m.rulesMutex.RLock() + defer m.rulesMutex.RUnlock() + + // 预处理域名,去除可能的端口号 + if strings.Contains(domain, ":") { + parts := strings.Split(domain, ":") + domain = parts[0] + } + + // 首先检查排除规则(优先级最高) + // 检查域名排除规则 + if m.domainExceptions[domain] { + return false + } + + // 检查子域名排除规则 + parts := strings.Split(domain, ".") + for i := 0; i < len(parts)-1; i++ { + subdomain := strings.Join(parts[i:], ".") + if m.domainExceptions[subdomain] { + return false + } + } + + // 检查正则表达式排除规则 + for _, re := range m.regexExceptions { + if re.pattern.MatchString(domain) { + return false + } + } + + // 然后检查阻止规则 + // 检查精确域名匹配 + if m.domainRules[domain] { + return true + } + + // 检查子域名匹配(AdGuardHome风格) + // 从最长的子域名开始匹配,确保优先级正确 + for i := 0; i < len(parts)-1; i++ { + subdomain := strings.Join(parts[i:], ".") + if m.domainRules[subdomain] { + return true + } + } + + // 检查正则表达式匹配 + for _, re := range m.regexRules { + if re.pattern.MatchString(domain) { + return true + } + } + + return false +} + +// 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() + + // 解析并添加规则到内存 + m.parseRule(rule) + + // 持久化保存规则到文件 + if m.config.LocalRulesFile != "" { + 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) + removed = true + break + } + } else if strings.HasPrefix(format, "||") { + // 尝试删除域名规则 + domain := strings.TrimPrefix(format, "||") + if _, exists := m.domainRules[domain]; exists { + delete(m.domainRules, domain) + removed = true + break + } + } else { + // 尝试直接作为域名删除 + if _, exists := m.domainRules[format]; exists { + delete(m.domainRules, format) + removed = true + break + } + if _, exists := m.domainExceptions[format]; exists { + delete(m.domainExceptions, format) + removed = true + break + } + } + } + + // 处理正则表达式规则 + if !removed && strings.HasPrefix(cleanRule, "/") && strings.HasSuffix(cleanRule, "/") { + pattern := strings.TrimPrefix(strings.TrimSuffix(cleanRule, "/"), "/") + + // 检查是否在正则表达式规则中 + newRegexRules := []regexRule{} + for _, re := range m.regexRules { + if re.pattern.String() != pattern { + newRegexRules = append(newRegexRules, re) + } else { + removed = true + } + } + m.regexRules = newRegexRules + + // 如果没有从正则规则中找到,检查是否在正则排除规则中 + if !removed { + newRegexExceptions := []regexRule{} + for _, re := range m.regexExceptions { + if re.pattern.String() != pattern { + 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) + removed = true + break + } + } + + if !removed { + for domain := range m.domainExceptions { + if domain == cleanRule || domain == rule { + delete(m.domainExceptions, domain) + removed = true + break + } + } + } + } + + // 如果有规则被删除,持久化保存更改 + if removed && m.config.LocalRulesFile != "" { + 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() + + 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.updateRunning = false + return + } + } + }() +} + +// StopAutoUpdate 停止自动更新 +func (m *ShieldManager) StopAutoUpdate() { + m.updateCancel() +} + +// saveRulesToFile 保存规则到文件 +func (m *ShieldManager) saveRulesToFile() error { + var rules []string + + // 添加域名规则 + for domain := range m.domainRules { + rules = append(rules, "||"+domain) + } + + // 添加正则表达式规则 + for _, re := range m.regexRules { + rules = append(rules, re.original) + } + + // 添加排除规则 + for domain := range m.domainExceptions { + rules = append(rules, "@@||"+domain) + } + + // 添加正则表达式排除规则 + for _, re := range m.regexExceptions { + rules = append(rules, re.original) + } + + // 写入文件 + content := strings.Join(rules, "\n") + return ioutil.WriteFile(m.config.LocalRulesFile, []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 m.config.HostsFile != "" { + 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 m.config.HostsFile != "" { + 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(m.config.HostsFile, []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, + } +} + +// 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) + } + + return map[string]interface{}{ + "domainRules": domainRulesList, + "domainExceptions": domainExceptionsList, + "regexRules": regexRulesList, + "regexExceptions": regexExceptionsList, + "hostsRules": hostsRulesList, + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..bd74d67 --- /dev/null +++ b/static/index.html @@ -0,0 +1,1727 @@ + + + + + + DNS服务器管理中心 + + + + + + +
+
+

DNS服务器管理中心

+

高性能DNS服务器,支持规则屏蔽和Hosts管理

+
+ +
+
+ + + + +
+ + +
+

服务器状态

+
+
+ +
--
+
屏蔽规则数
+
+ +
+
+ +
+ +
--
+
Hosts条目数
+
+ +
+
+
+ +
--
+
DNS查询次数
+
+ +
+
+
+ +
--
+
屏蔽次数
+
+ +
+
+
+ +

TOP域名统计

+
+
+
+

TOP 10 屏蔽域名

+
+
+
+
+ +

加载中...

+
+
+
+
+ +
+
+

TOP 10 解析域名

+
+
+
+
+ +

加载中...

+
+
+
+
+
+ +
+
+

24小时屏蔽统计

+
+
+
+ +
+
+
+
+

服务器信息

+

服务器地址: --

+

当前时间: --

+

运行状态: 正常运行

+
+
+ + +
+
+
+

屏蔽设置

+
+
+
+ + + +
+ + NXDOMAIN: 返回域名不存在错误
+ refused: 返回查询拒绝错误
+ emptyIP: 返回0.0.0.0
+ customIP: 返回自定义IP地址 +
+
+
+
+
+

添加屏蔽规则

+
+
+
+ + + +
+ 支持AdGuardHome规则格式:域名规则(||example.com^)、排除规则(@@||example.com^)、正则规则(/regex/)、通配符规则(*example.com)等 +
+
+
+
+

规则列表

+
+
+
+
+ +

规则列表加载中...

+
+
+
+
+
+ + +
+
+
+

添加Hosts条目

+
+
+
+ + + +
+
+
+
+
+

当前Hosts条目

+
+
+
+
+ +

Hosts列表加载中...

+
+
+
+
+
+ + +
+
+
+

DNS查询

+
+
+
+ + +
+
+
+
+
+

查询结果

+
+
+
请输入域名并点击查询按钮
+
+
+
+
+
+ + + +