Compare commits
5 Commits
dnssever
...
qimeng-dns
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecbc20d89f | ||
|
|
b48dc4ed27 | ||
|
|
356310ae75 | ||
|
|
fe77539da1 | ||
|
|
1f8bd6b97f |
109
CHANGELOG.md
109
CHANGELOG.md
@@ -1,6 +1,113 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
所有对本项目的显著更改都将记录在此文件中。
|
所有对本项目的显著更改都将记录在此文件中。
|
||||||
|
## [1.2.5] - 2025-12-26
|
||||||
|
### 新增
|
||||||
|
- 增加了对IPv6的支持配置项,默认关闭;
|
||||||
|
- 增加跟踪器状态显示(匹配tracker/trackers.json数据库);
|
||||||
|
- 全局UX改进,包括但不限于:
|
||||||
|
- 增加了页面滚动时,菜单栏和顶部标题栏保持固定的功能;
|
||||||
|
- 优化了页面适应窗口大小的改变,确保在所有设备上都能正确显示;
|
||||||
|
- 增加点击解析记录后弹窗日志详情的UI/UX,使用现代化设计和动画效果;
|
||||||
|
- 增加了查询日志详情界面的滚动条,方便查看长日志。
|
||||||
|
### 改进
|
||||||
|
- 新增API接口,用于查询解析日志详情;
|
||||||
|
- 支持EDNS,在web界面查询日志详情的请求列表区域增加了EDNS标记显示。
|
||||||
|
### 修复
|
||||||
|
- 修复DNS服务器地址缺少端口号导致的Server Failed问题;
|
||||||
|
- 修复查询日志详情接口返回的日志格式错误问题,现在返回的日志包含完整的解析记录和解析时间;
|
||||||
|
- 修复查询日志详情接口返回的日志中,解析记录中缺少IP地址、类型、DNSSEC验证状态等信息的问题;
|
||||||
|
- web界面系统设置加载后不获取数据和保存配置不生效的问题;
|
||||||
|
- 修复了DNS查询超时设置过短导致的"Server failed"错误。
|
||||||
|
### 更新
|
||||||
|
- 更新Swagger API文档。
|
||||||
|
### 下一版本改进
|
||||||
|
- 增加了对DNSSEC的支持配置项,默认关闭;
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.4] - 2025-12-25
|
||||||
|
### 改进
|
||||||
|
- 修复DNS解析记录显示,现在显示完整格式:"A: 104.26.24.30 (ttl=193)" 而不仅仅是IP地址
|
||||||
|
- 移除了查询日志列表中的"屏蔽规则"列,但在详情弹窗中仍保留
|
||||||
|
- 在弹窗日志详情中,只有被屏蔽或者有自定义规则时才显示规则信息
|
||||||
|
- 改进了日志详情弹窗的UI/UX,使用现代化设计和动画效果
|
||||||
|
- 移除了右上角的服务器状态卡片(CPU、查询统计等)
|
||||||
|
- 实现了页面滚动时,菜单栏和顶部标题栏保持固定
|
||||||
|
- 优化了页面适应窗口大小的改变,确保在所有设备上都能正确显示
|
||||||
|
### 修复
|
||||||
|
- 修复了移动端侧边栏在打开时遮挡页面内容的问题
|
||||||
|
- 修复了侧边栏布局,分离了桌面端和移动端侧边栏,使用CSS媒体查询控制显示
|
||||||
|
|
||||||
|
## [1.2.3] - 2025-12-25
|
||||||
|
### 修复
|
||||||
|
- 修复DNS服务器地址缺少端口号导致的Server Failed问题
|
||||||
|
- 添加normalizeDNSServerAddress函数,确保DNS服务器地址始终包含端口号,默认添加53端口
|
||||||
|
- 修改所有resolver.Exchange()调用,确保传递的服务器地址包含端口号
|
||||||
|
- 优化DNSSEC服务器合并逻辑,确保DNSSEC服务器地址也包含端口号
|
||||||
|
|
||||||
|
## [1.2.2] - 2025-12-25
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- 增加查询日志详情界面点击域名列表,显示解析日志的详细信息。
|
||||||
|
- 增加DNSSEC上游服务器的配置项。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- web界面系统设置加载后不获取数据和保存配置不生效的问题。
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.1] - 2025-12-25
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
- 增加IPv6支持配置项,默认关闭
|
||||||
|
### 修复
|
||||||
|
- 修复了DNS查询超时设置过短导致的"Server failed"错误
|
||||||
|
- 将默认DNS请求超时时间从5毫秒调整为1000毫秒
|
||||||
|
|
||||||
|
## [1.2.0] - 2025-12-24
|
||||||
|
|
||||||
|
### 添加
|
||||||
|
- 在查询日志详情的域名左侧添加DNSSEC状态锁图标和跟踪器状态图标
|
||||||
|
- 实现跟踪器状态显示(匹配tracker/trackers.json数据库)
|
||||||
|
- 添加跟踪器详情浮窗(鼠标悬停在眼睛图标上时显示跟踪器名称、类别、URL、来源等信息)
|
||||||
|
- 实现日志页面页码跳转功能(输入框+"前往"按钮)
|
||||||
|
- 实现日志页面显示数量选择功能(下拉框)
|
||||||
|
|
||||||
|
### 修改
|
||||||
|
- 异步加载跟踪器数据库并缓存,优化性能
|
||||||
|
- 将日志渲染逻辑改为支持异步操作的for...of循环
|
||||||
|
- 修复跟踪器浮窗CSS样式语法错误
|
||||||
|
- 在后端添加/tracker目录静态文件服务路由
|
||||||
|
|
||||||
|
## [1.1.4] - 2025-12-21
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复规则优先级问题:确保自定义规则优先于远程规则
|
||||||
|
- 修复添加自定义规则后需要重启服务器的问题:通过在添加或删除规则后清空DNS缓存实现
|
||||||
|
|
||||||
|
## [1.1.3] - 2025-12-19
|
||||||
|
|
||||||
|
### 移除
|
||||||
|
- 移除search domain功能,不再支持自动添加域名前缀进行查询
|
||||||
|
- 移除DNSConfig结构体中的PrefixDomain字段
|
||||||
|
- 移除配置文件中的prefixDomain配置项
|
||||||
|
|
||||||
|
## [1.1.2] - 2025-12-19
|
||||||
|
|
||||||
|
### 添加
|
||||||
|
- 添加不验证DNSSEC的域名功能,支持通过配置文件指定需要跳过DNSSEC验证的域名模式
|
||||||
|
- 在DNSConfig结构体中增加NoDNSSECDomains字段,用于存储不验证DNSSEC的域名模式列表
|
||||||
|
|
||||||
|
### 修改
|
||||||
|
- 在forwardDNSRequestWithCache函数中添加域名匹配逻辑,检查域名是否包含不验证DNSSEC的模式
|
||||||
|
- 在所有查询模式(parallel、loadbalance、fastest-ip、default)中实现跳过DNSSEC验证的功能
|
||||||
|
|
||||||
|
## [1.1.1] - 2025-12-19
|
||||||
|
|
||||||
|
### 修改
|
||||||
|
- 修复NXDOMAIN响应传播逻辑,确保上游DNS服务器返回的NXDOMAIN响应能正确传递给客户端
|
||||||
|
- 优化loadbalance、fastest-ip和parallel查询模式下的NXDOMAIN响应选择机制
|
||||||
|
- 确保不存在的域名能被正确识别并返回NXDOMAIN状态码
|
||||||
|
- 修复服务器绑定地址配置,确保IPv4兼容性
|
||||||
|
|
||||||
## [1.0.0] - 2025-12-16
|
## [1.0.0] - 2025-12-16
|
||||||
|
|
||||||
@@ -18,4 +125,4 @@
|
|||||||
|
|
||||||
本CHANGELOG遵循[Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)格式。
|
本CHANGELOG遵循[Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)格式。
|
||||||
|
|
||||||
版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)规范。
|
版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)规范。
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
- 支持域名规则和正则表达式规则
|
- 支持域名规则和正则表达式规则
|
||||||
- 支持规则例外
|
- 支持规则例外
|
||||||
- 支持远程规则列表
|
- 支持远程规则列表
|
||||||
- 支持本地规则管理
|
- 支持自定义规则管理
|
||||||
|
|
||||||
### 3. 查询日志记录和统计
|
### 3. 查询日志记录和统计
|
||||||
- 实时记录DNS查询日志
|
- 实时记录DNS查询日志
|
||||||
@@ -116,7 +116,7 @@ http://localhost:8080
|
|||||||
|
|
||||||
1. 登录Web控制台
|
1. 登录Web控制台
|
||||||
2. 点击左侧菜单中的"屏蔽管理"
|
2. 点击左侧菜单中的"屏蔽管理"
|
||||||
3. 在"本地规则管理"中添加或删除规则
|
3. 在"自定义规则管理"中添加或删除规则
|
||||||
4. 在"远程黑名单管理"中添加或删除远程规则列表
|
4. 在"远程黑名单管理"中添加或删除远程规则列表
|
||||||
|
|
||||||
### 查看查询日志
|
### 查看查询日志
|
||||||
|
|||||||
5
build-windows.sh
Executable file
5
build-windows.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
CGO_ENABLED=1 \
|
||||||
|
GOOS=windows \
|
||||||
|
GOARCH=amd64 \
|
||||||
|
CC=gcc \
|
||||||
|
go build -o dns-server.exe main.go
|
||||||
5
build.sh
Executable file
5
build.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
CGO_ENABLED=1 \
|
||||||
|
GOOS=linux \
|
||||||
|
GOARCH=amd64 \
|
||||||
|
CC=gcc \
|
||||||
|
go build -ldflags "-linkmode external -extldflags '-static -pthread'" -o dns-server main.go
|
||||||
69
config.json
69
config.json
@@ -2,18 +2,50 @@
|
|||||||
"dns": {
|
"dns": {
|
||||||
"port": 53,
|
"port": 53,
|
||||||
"upstreamDNS": [
|
"upstreamDNS": [
|
||||||
"223.5.5.5:53",
|
"223.5.5.5:53"
|
||||||
"223.6.6.6:53",
|
|
||||||
"117.50.10.10:53"
|
|
||||||
],
|
],
|
||||||
"dnssecUpstreamDNS": [
|
"dnssecUpstreamDNS": [
|
||||||
"1.1.1.1:53"
|
"117.50.10.10",
|
||||||
|
"101.226.4.6",
|
||||||
|
"218.30.118.6",
|
||||||
|
"208.67.220.220",
|
||||||
|
"208.67.222.222"
|
||||||
],
|
],
|
||||||
"timeout": 5000,
|
"timeout": 5,
|
||||||
"statsFile": "data/stats.json",
|
"saveInterval": 30,
|
||||||
"saveInterval": 300,
|
"cacheTTL": 10,
|
||||||
"cacheTTL": 30,
|
"enableDNSSEC": true,
|
||||||
"enableDNSSEC": true
|
"queryMode": "parallel",
|
||||||
|
"domainSpecificDNS": {
|
||||||
|
"addr.arpa": [
|
||||||
|
"10.35.10.200:53"
|
||||||
|
],
|
||||||
|
"akadns": [
|
||||||
|
"4.2.2.1:53"
|
||||||
|
],
|
||||||
|
"akamai": [
|
||||||
|
"4.2.2.1:53"
|
||||||
|
],
|
||||||
|
"amazehome.cn": [
|
||||||
|
"10.35.10.200:53"
|
||||||
|
],
|
||||||
|
"amazehome.xyz": [
|
||||||
|
"10.35.10.200:53"
|
||||||
|
],
|
||||||
|
"microsoft.com": [
|
||||||
|
"4.2.2.1:53"
|
||||||
|
],
|
||||||
|
"steam": [
|
||||||
|
"4.2.2.1:53"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noDNSSECDomains": [
|
||||||
|
"amazehome.cn",
|
||||||
|
"addr.arpa",
|
||||||
|
"amazehome.xyz",
|
||||||
|
".cn"
|
||||||
|
],
|
||||||
|
"enableIPv6": false
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
@@ -23,13 +55,12 @@
|
|||||||
"password": "admin"
|
"password": "admin"
|
||||||
},
|
},
|
||||||
"shield": {
|
"shield": {
|
||||||
"localRulesFile": "data/rules.txt",
|
|
||||||
"blacklists": [
|
"blacklists": [
|
||||||
{
|
{
|
||||||
"name": "AdGuard DNS filter",
|
"name": "AdGuard DNS filter",
|
||||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"lastUpdateTime": "2025-11-28T16:13:03.564Z"
|
"lastUpdateTime": "2025-12-21T10:46:36.629Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Adaway Default Blocklist",
|
"name": "Adaway Default Blocklist",
|
||||||
@@ -47,7 +78,7 @@
|
|||||||
"name": "My GitHub Rules",
|
"name": "My GitHub Rules",
|
||||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"lastUpdateTime": "2025-11-29T17:05:40.283Z"
|
"lastUpdateTime": "2025-12-24T07:11:16.596Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CNList",
|
"name": "CNList",
|
||||||
@@ -62,13 +93,14 @@
|
|||||||
{
|
{
|
||||||
"name": "Hate \u0026 Junk",
|
"name": "Hate \u0026 Junk",
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt",
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt",
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"lastUpdateTime": "2025-12-21T10:46:43.522Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "My Gitlab Hosts",
|
"name": "My Gitlab Hosts",
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt",
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"lastUpdateTime": "2025-11-29T17:11:28.130Z"
|
"lastUpdateTime": "2025-12-18T10:39:39.333Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Anti Remote Requests",
|
"name": "Anti Remote Requests",
|
||||||
@@ -83,7 +115,8 @@
|
|||||||
{
|
{
|
||||||
"name": "My Gitlab A/T Rules",
|
"name": "My Gitlab A/T Rules",
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt",
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt",
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"lastUpdateTime": "2025-12-24T07:11:07.334Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "My Gitlab Malware List",
|
"name": "My Gitlab Malware List",
|
||||||
@@ -107,15 +140,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updateInterval": 3600,
|
"updateInterval": 3600,
|
||||||
"hostsFile": "data/hosts.txt",
|
|
||||||
"blockMethod": "NXDOMAIN",
|
"blockMethod": "NXDOMAIN",
|
||||||
"customBlockIP": "",
|
"customBlockIP": "",
|
||||||
"statsFile": "./data/shield_stats.json",
|
"statsSaveInterval": 60
|
||||||
"statsSaveInterval": 60,
|
|
||||||
"remoteRulesCacheDir": "data/remote_rules"
|
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"file": "logs/dns-server.log",
|
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
"maxSize": 100,
|
"maxSize": 100,
|
||||||
"maxBackups": 10,
|
"maxBackups": 10,
|
||||||
|
|||||||
@@ -5,16 +5,26 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DomainSpecificDNS 域名特定DNS服务器配置
|
||||||
|
// 格式:{"domainMatch": ["dns1", "dns2"]}
|
||||||
|
// domainMatch: 域名匹配字符串,当域名中包含该字符串时使用对应的DNS服务器
|
||||||
|
// dns1, dns2: 用于解析匹配域名的DNS服务器列表
|
||||||
|
|
||||||
|
type DomainSpecificDNS map[string][]string
|
||||||
|
|
||||||
// DNSConfig DNS配置
|
// DNSConfig DNS配置
|
||||||
type DNSConfig struct {
|
type DNSConfig struct {
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
UpstreamDNS []string `json:"upstreamDNS"`
|
UpstreamDNS []string `json:"upstreamDNS"`
|
||||||
DNSSECUpstreamDNS []string `json:"dnssecUpstreamDNS"` // 用于DNSSEC查询的专用服务器
|
DNSSECUpstreamDNS []string `json:"dnssecUpstreamDNS"` // 用于DNSSEC查询的专用服务器
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
StatsFile string `json:"statsFile"` // 统计数据持久化文件
|
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
||||||
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
||||||
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持
|
||||||
EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持
|
QueryMode string `json:"queryMode"` // 查询模式:"loadbalance"(负载均衡)、"parallel"(并行请求)、"fastest-ip"(最快的IP地址)
|
||||||
|
DomainSpecificDNS DomainSpecificDNS `json:"domainSpecificDNS"` // 域名特定DNS服务器配置
|
||||||
|
NoDNSSECDomains []string `json:"noDNSSECDomains"` // 不验证DNSSEC的域名模式列表
|
||||||
|
EnableIPv6 bool `json:"enableIPv6"` // 是否启用IPv6解析(AAAA记录)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPConfig HTTP控制台配置
|
// HTTPConfig HTTP控制台配置
|
||||||
@@ -37,20 +47,15 @@ type BlacklistEntry struct {
|
|||||||
|
|
||||||
// ShieldConfig 屏蔽规则配置
|
// ShieldConfig 屏蔽规则配置
|
||||||
type ShieldConfig struct {
|
type ShieldConfig struct {
|
||||||
LocalRulesFile string `json:"localRulesFile"`
|
Blacklists []BlacklistEntry `json:"blacklists"`
|
||||||
Blacklists []BlacklistEntry `json:"blacklists"`
|
UpdateInterval int `json:"updateInterval"`
|
||||||
UpdateInterval int `json:"updateInterval"`
|
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
|
||||||
HostsFile string `json:"hostsFile"`
|
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用
|
||||||
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
|
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
|
||||||
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用
|
|
||||||
StatsFile string `json:"statsFile"` // 计数数据持久化文件
|
|
||||||
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
|
|
||||||
RemoteRulesCacheDir string `json:"remoteRulesCacheDir"` // 远程规则缓存目录
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogConfig 日志配置
|
// LogConfig 日志配置
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
File string `json:"file"`
|
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
MaxSize int `json:"maxSize"`
|
MaxSize int `json:"maxSize"`
|
||||||
MaxBackups int `json:"maxBackups"`
|
MaxBackups int `json:"maxBackups"`
|
||||||
@@ -82,12 +87,12 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
if config.DNS.Port == 0 {
|
if config.DNS.Port == 0 {
|
||||||
config.DNS.Port = 53
|
config.DNS.Port = 53
|
||||||
}
|
}
|
||||||
|
if config.DNS.Timeout == 0 {
|
||||||
|
config.DNS.Timeout = 5000 // 默认超时时间为5000毫秒
|
||||||
|
}
|
||||||
if len(config.DNS.UpstreamDNS) == 0 {
|
if len(config.DNS.UpstreamDNS) == 0 {
|
||||||
config.DNS.UpstreamDNS = []string{"223.5.5.5:53", "223.6.6.6:53"}
|
config.DNS.UpstreamDNS = []string{"223.5.5.5:53", "223.6.6.6:53"}
|
||||||
}
|
}
|
||||||
if config.DNS.StatsFile == "" {
|
|
||||||
config.DNS.StatsFile = "./data/stats.json" // 默认统计数据文件路径
|
|
||||||
}
|
|
||||||
if config.DNS.SaveInterval == 0 {
|
if config.DNS.SaveInterval == 0 {
|
||||||
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
||||||
}
|
}
|
||||||
@@ -97,10 +102,21 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
// DNSSEC默认配置
|
// DNSSEC默认配置
|
||||||
config.DNS.EnableDNSSEC = true // 默认启用DNSSEC支持
|
config.DNS.EnableDNSSEC = true // 默认启用DNSSEC支持
|
||||||
|
// IPv6默认配置
|
||||||
|
config.DNS.EnableIPv6 = true // 默认启用IPv6解析
|
||||||
// DNSSEC专用服务器默认配置
|
// DNSSEC专用服务器默认配置
|
||||||
if len(config.DNS.DNSSECUpstreamDNS) == 0 {
|
if len(config.DNS.DNSSECUpstreamDNS) == 0 {
|
||||||
config.DNS.DNSSECUpstreamDNS = []string{"8.8.8.8:53", "1.1.1.1:53"}
|
config.DNS.DNSSECUpstreamDNS = []string{"8.8.8.8:53", "1.1.1.1:53"}
|
||||||
}
|
}
|
||||||
|
// 查询模式默认配置
|
||||||
|
if config.DNS.QueryMode == "" {
|
||||||
|
config.DNS.QueryMode = "parallel" // 默认使用并行请求模式
|
||||||
|
}
|
||||||
|
// 域名特定DNS服务器配置默认值
|
||||||
|
if config.DNS.DomainSpecificDNS == nil {
|
||||||
|
config.DNS.DomainSpecificDNS = make(DomainSpecificDNS) // 默认为空映射
|
||||||
|
}
|
||||||
|
|
||||||
if config.HTTP.Port == 0 {
|
if config.HTTP.Port == 0 {
|
||||||
config.HTTP.Port = 8080
|
config.HTTP.Port = 8080
|
||||||
}
|
}
|
||||||
@@ -120,15 +136,9 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
if config.Shield.BlockMethod == "" {
|
if config.Shield.BlockMethod == "" {
|
||||||
config.Shield.BlockMethod = "NXDOMAIN" // 默认屏蔽方法为NXDOMAIN
|
config.Shield.BlockMethod = "NXDOMAIN" // 默认屏蔽方法为NXDOMAIN
|
||||||
}
|
}
|
||||||
if config.Shield.StatsFile == "" {
|
|
||||||
config.Shield.StatsFile = "./data/shield_stats.json" // 默认Shield统计数据文件路径
|
|
||||||
}
|
|
||||||
if config.Shield.StatsSaveInterval == 0 {
|
if config.Shield.StatsSaveInterval == 0 {
|
||||||
config.Shield.StatsSaveInterval = 300 // 默认5分钟保存一次
|
config.Shield.StatsSaveInterval = 300 // 默认5分钟保存一次
|
||||||
}
|
}
|
||||||
if config.Shield.RemoteRulesCacheDir == "" {
|
|
||||||
config.Shield.RemoteRulesCacheDir = "./data/remote_rules" // 默认远程规则缓存目录
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果黑名单列表为空,添加一些默认的黑名单
|
// 如果黑名单列表为空,添加一些默认的黑名单
|
||||||
if len(config.Shield.Blacklists) == 0 {
|
if len(config.Shield.Blacklists) == 0 {
|
||||||
|
|||||||
1289
dns/server.go
1289
dns/server.go
File diff suppressed because it is too large
Load Diff
12
download.sh
Executable file
12
download.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e -f -u -x
|
||||||
|
|
||||||
|
# This script syncs companies DB that we bundle with AdGuard Home. The source
|
||||||
|
# for this database is https://github.com/AdguardTeam/companiesdb.
|
||||||
|
#
|
||||||
|
trackers_url='https://raw.githubusercontent.com/AdguardTeam/companiesdb/main/dist/trackers.json'
|
||||||
|
output='./trackers.json'
|
||||||
|
readonly trackers_url output
|
||||||
|
|
||||||
|
curl -o "$output" -v "$trackers_url"
|
||||||
@@ -154,6 +154,17 @@ func (s *Server) Start() error {
|
|||||||
http.ServeFile(w, r, "./static/login.html")
|
http.ServeFile(w, r, "./static/login.html")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Tracker目录静态文件服务
|
||||||
|
trackerFileServer := http.FileServer(http.Dir("./tracker"))
|
||||||
|
mux.HandleFunc("/tracker/", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 添加Cache-Control头,禁用浏览器缓存
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Pragma", "no-cache")
|
||||||
|
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
||||||
|
// 使用StripPrefix处理路径
|
||||||
|
http.StripPrefix("/tracker", trackerFileServer).ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
// 其他静态文件需要登录
|
// 其他静态文件需要登录
|
||||||
mux.HandleFunc("/", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// 添加Cache-Control头,禁用浏览器缓存
|
// 添加Cache-Control头,禁用浏览器缓存
|
||||||
@@ -740,7 +751,7 @@ func (s *Server) handleTopDomains(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 合并并去重域名统计
|
// 合并并去重域名统计
|
||||||
domainMap := make(map[string]int64)
|
domainMap := make(map[string]int64)
|
||||||
dnssecStatusMap := make(map[string]bool)
|
dnssecStatusMap := make(map[string]bool)
|
||||||
|
|
||||||
for _, domain := range blockedDomains {
|
for _, domain := range blockedDomains {
|
||||||
domainMap[domain.Domain] += domain.Count
|
domainMap[domain.Domain] += domain.Count
|
||||||
dnssecStatusMap[domain.Domain] = domain.DNSSEC
|
dnssecStatusMap[domain.Domain] = domain.DNSSEC
|
||||||
@@ -832,6 +843,9 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空DNS缓存,使新规则立即生效
|
||||||
|
s.dnsServer.DnsCache.Clear()
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||||
return
|
return
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
@@ -850,6 +864,9 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空DNS缓存,使规则变更立即生效
|
||||||
|
s.dnsServer.DnsCache.Clear()
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||||
return
|
return
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
@@ -1216,18 +1233,40 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// 返回当前配置(包括黑名单配置)
|
// 返回当前配置(包括黑名单配置)
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"shield": map[string]interface{}{
|
"Shield": map[string]interface{}{
|
||||||
"blockMethod": s.globalConfig.Shield.BlockMethod,
|
"blockMethod": s.globalConfig.Shield.BlockMethod,
|
||||||
"customBlockIP": s.globalConfig.Shield.CustomBlockIP,
|
"customBlockIP": s.globalConfig.Shield.CustomBlockIP,
|
||||||
"blacklists": s.globalConfig.Shield.Blacklists,
|
"blacklists": s.globalConfig.Shield.Blacklists,
|
||||||
"updateInterval": s.globalConfig.Shield.UpdateInterval,
|
"updateInterval": s.globalConfig.Shield.UpdateInterval,
|
||||||
},
|
},
|
||||||
|
"DNSServer": map[string]interface{}{
|
||||||
|
"port": s.globalConfig.DNS.Port,
|
||||||
|
"UpstreamServers": s.globalConfig.DNS.UpstreamDNS,
|
||||||
|
"DNSSECUpstreamServers": s.globalConfig.DNS.DNSSECUpstreamDNS,
|
||||||
|
"timeout": s.globalConfig.DNS.Timeout,
|
||||||
|
"saveInterval": s.globalConfig.DNS.SaveInterval,
|
||||||
|
"enableIPv6": s.globalConfig.DNS.EnableIPv6,
|
||||||
|
},
|
||||||
|
"HTTPServer": map[string]interface{}{
|
||||||
|
"port": s.globalConfig.HTTP.Port,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(config)
|
json.NewEncoder(w).Encode(config)
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
// 更新配置
|
// 更新配置
|
||||||
var req struct {
|
var req struct {
|
||||||
|
DNSServer struct {
|
||||||
|
Port int `json:"port"`
|
||||||
|
UpstreamServers []string `json:"upstreamServers"`
|
||||||
|
DnssecUpstreamServers []string `json:"dnssecUpstreamServers"`
|
||||||
|
Timeout int `json:"timeout"`
|
||||||
|
SaveInterval int `json:"saveInterval"`
|
||||||
|
EnableIPv6 bool `json:"enableIPv6"`
|
||||||
|
} `json:"dnsserver"`
|
||||||
|
HTTPServer struct {
|
||||||
|
Port int `json:"port"`
|
||||||
|
} `json:"httpserver"`
|
||||||
Shield struct {
|
Shield struct {
|
||||||
BlockMethod string `json:"blockMethod"`
|
BlockMethod string `json:"blockMethod"`
|
||||||
CustomBlockIP string `json:"customBlockIP"`
|
CustomBlockIP string `json:"customBlockIP"`
|
||||||
@@ -1241,6 +1280,29 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新DNS配置
|
||||||
|
if req.DNSServer.Port > 0 {
|
||||||
|
s.globalConfig.DNS.Port = req.DNSServer.Port
|
||||||
|
}
|
||||||
|
if len(req.DNSServer.UpstreamServers) > 0 {
|
||||||
|
s.globalConfig.DNS.UpstreamDNS = req.DNSServer.UpstreamServers
|
||||||
|
}
|
||||||
|
if len(req.DNSServer.DnssecUpstreamServers) > 0 {
|
||||||
|
s.globalConfig.DNS.DNSSECUpstreamDNS = req.DNSServer.DnssecUpstreamServers
|
||||||
|
}
|
||||||
|
if req.DNSServer.Timeout > 0 {
|
||||||
|
s.globalConfig.DNS.Timeout = req.DNSServer.Timeout
|
||||||
|
}
|
||||||
|
if req.DNSServer.SaveInterval > 0 {
|
||||||
|
s.globalConfig.DNS.SaveInterval = req.DNSServer.SaveInterval
|
||||||
|
}
|
||||||
|
s.globalConfig.DNS.EnableIPv6 = req.DNSServer.EnableIPv6
|
||||||
|
|
||||||
|
// 更新HTTP配置
|
||||||
|
if req.HTTPServer.Port > 0 {
|
||||||
|
s.globalConfig.HTTP.Port = req.HTTPServer.Port
|
||||||
|
}
|
||||||
|
|
||||||
// 更新屏蔽配置
|
// 更新屏蔽配置
|
||||||
if req.Shield.BlockMethod != "" {
|
if req.Shield.BlockMethod != "" {
|
||||||
// 验证屏蔽方法是否有效
|
// 验证屏蔽方法是否有效
|
||||||
|
|||||||
43
main.go
43
main.go
@@ -37,9 +37,15 @@ func createDefaultConfig(configFile string) error {
|
|||||||
"223.5.5.5:53",
|
"223.5.5.5:53",
|
||||||
"223.6.6.6:53"
|
"223.6.6.6:53"
|
||||||
],
|
],
|
||||||
|
"dnssecUpstreamDNS": [
|
||||||
|
"8.8.8.8:53",
|
||||||
|
"1.1.1.1:53"
|
||||||
|
],
|
||||||
"timeout": 5000,
|
"timeout": 5000,
|
||||||
"statsFile": "./data/stats.json",
|
"saveInterval": 300,
|
||||||
"saveInterval": 300
|
"cacheTTL": 30,
|
||||||
|
"enableDNSSEC": true,
|
||||||
|
"queryMode": "parallel"
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
@@ -49,7 +55,6 @@ func createDefaultConfig(configFile string) error {
|
|||||||
"password": "admin"
|
"password": "admin"
|
||||||
},
|
},
|
||||||
"shield": {
|
"shield": {
|
||||||
"localRulesFile": "data/rules.txt",
|
|
||||||
"blacklists": [
|
"blacklists": [
|
||||||
{
|
{
|
||||||
"name": "AdGuard DNS filter",
|
"name": "AdGuard DNS filter",
|
||||||
@@ -73,15 +78,11 @@ func createDefaultConfig(configFile string) error {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updateInterval": 3600,
|
"updateInterval": 3600,
|
||||||
"hostsFile": "data/hosts.txt",
|
|
||||||
"blockMethod": "NXDOMAIN",
|
"blockMethod": "NXDOMAIN",
|
||||||
"customBlockIP": "",
|
"customBlockIP": "",
|
||||||
"statsFile": "./data/shield_stats.json",
|
"statsSaveInterval": 60
|
||||||
"statsSaveInterval": 60,
|
|
||||||
"remoteRulesCacheDir": "./data/remote_rules"
|
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"file": "logs/dns-server.log",
|
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
"maxSize": 100,
|
"maxSize": 100,
|
||||||
"maxBackups": 10,
|
"maxBackups": 10,
|
||||||
@@ -102,42 +103,42 @@ func createRequiredFiles(cfg *config.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建远程规则缓存文件夹
|
// 创建远程规则缓存文件夹
|
||||||
if err := os.MkdirAll(cfg.Shield.RemoteRulesCacheDir, 0755); err != nil {
|
if err := os.MkdirAll("data/remote_rules", 0755); err != nil {
|
||||||
return fmt.Errorf("创建远程规则缓存文件夹失败: %w", err)
|
return fmt.Errorf("创建远程规则缓存文件夹失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建日志文件夹
|
// 创建日志文件夹
|
||||||
logDir := filepath.Dir(cfg.Log.File)
|
logDir := filepath.Dir("logs/dns-server.log")
|
||||||
if logDir != "." {
|
if logDir != "." {
|
||||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
return fmt.Errorf("创建日志文件夹失败: %w", err)
|
return fmt.Errorf("创建日志文件夹失败: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建本地规则文件
|
// 创建自定义规则文件
|
||||||
if _, err := os.Stat(cfg.Shield.LocalRulesFile); os.IsNotExist(err) {
|
if _, err := os.Stat("data/rules.txt"); os.IsNotExist(err) {
|
||||||
if err := os.WriteFile(cfg.Shield.LocalRulesFile, []byte("# 本地规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil {
|
if err := os.WriteFile("data/rules.txt", []byte("# 自定义规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil {
|
||||||
return fmt.Errorf("创建本地规则文件失败: %w", err)
|
return fmt.Errorf("创建自定义规则文件失败: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建Hosts文件
|
// 创建Hosts文件
|
||||||
if _, err := os.Stat(cfg.Shield.HostsFile); os.IsNotExist(err) {
|
if _, err := os.Stat("data/hosts.txt"); os.IsNotExist(err) {
|
||||||
if err := os.WriteFile(cfg.Shield.HostsFile, []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil {
|
if err := os.WriteFile("data/hosts.txt", []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil {
|
||||||
return fmt.Errorf("创建Hosts文件失败: %w", err)
|
return fmt.Errorf("创建Hosts文件失败: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建统计数据文件
|
// 创建统计数据文件
|
||||||
if _, err := os.Stat(cfg.DNS.StatsFile); os.IsNotExist(err) {
|
if _, err := os.Stat("data/stats.json"); os.IsNotExist(err) {
|
||||||
if err := os.WriteFile(cfg.DNS.StatsFile, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile("data/stats.json", []byte("{}"), 0644); err != nil {
|
||||||
return fmt.Errorf("创建统计数据文件失败: %w", err)
|
return fmt.Errorf("创建统计数据文件失败: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建Shield统计数据文件
|
// 创建Shield统计数据文件
|
||||||
if _, err := os.Stat(cfg.Shield.StatsFile); os.IsNotExist(err) {
|
if _, err := os.Stat("data/shield_stats.json"); os.IsNotExist(err) {
|
||||||
if err := os.WriteFile(cfg.Shield.StatsFile, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile("data/shield_stats.json", []byte("{}"), 0644); err != nil {
|
||||||
return fmt.Errorf("创建Shield统计数据文件失败: %w", err)
|
return fmt.Errorf("创建Shield统计数据文件失败: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +177,7 @@ func main() {
|
|||||||
log.Println("所需文件和文件夹创建成功")
|
log.Println("所需文件和文件夹创建成功")
|
||||||
|
|
||||||
// 初始化日志系统
|
// 初始化日志系统
|
||||||
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0, false); err != nil {
|
if err := logger.InitLogger("logs/dns-server.log", cfg.Log.Level, 0, 0, 0, false); err != nil {
|
||||||
log.Fatalf("初始化日志系统失败: %v", err)
|
log.Fatalf("初始化日志系统失败: %v", err)
|
||||||
}
|
}
|
||||||
defer logger.Close()
|
defer logger.Close()
|
||||||
|
|||||||
8670
server.log
8670
server.log
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
132708
|
|
||||||
@@ -30,7 +30,7 @@ type ShieldStatsData struct {
|
|||||||
type regexRule struct {
|
type regexRule struct {
|
||||||
pattern *regexp.Regexp
|
pattern *regexp.Regexp
|
||||||
original string
|
original string
|
||||||
isLocal bool // 是否为本地规则
|
isLocal bool // 是否为自定义规则
|
||||||
source string // 规则来源
|
source string // 规则来源
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +39,8 @@ type ShieldManager struct {
|
|||||||
config *config.ShieldConfig
|
config *config.ShieldConfig
|
||||||
domainRules map[string]bool
|
domainRules map[string]bool
|
||||||
domainExceptions map[string]bool
|
domainExceptions map[string]bool
|
||||||
domainRulesIsLocal map[string]bool // 标记域名规则是否为本地规则
|
domainRulesIsLocal map[string]bool // 标记域名规则是否为自定义规则
|
||||||
domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为本地规则
|
domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为自定义规则
|
||||||
domainRulesSource map[string]string // 标记域名规则来源
|
domainRulesSource map[string]string // 标记域名规则来源
|
||||||
domainExceptionsSource map[string]string // 标记域名排除规则来源
|
domainExceptionsSource map[string]string // 标记域名排除规则来源
|
||||||
domainRulesOriginal map[string]string // 存储域名规则的原始字符串
|
domainRulesOriginal map[string]string // 存储域名规则的原始字符串
|
||||||
@@ -54,7 +54,7 @@ type ShieldManager struct {
|
|||||||
updateCtx context.Context
|
updateCtx context.Context
|
||||||
updateCancel context.CancelFunc
|
updateCancel context.CancelFunc
|
||||||
updateRunning bool
|
updateRunning bool
|
||||||
localRulesCount int // 本地规则数量
|
localRulesCount int // 自定义规则数量
|
||||||
remoteRulesCount int // 远程规则数量
|
remoteRulesCount int // 远程规则数量
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,9 +109,9 @@ func (m *ShieldManager) LoadRules() error {
|
|||||||
m.remoteRulesCount = 0
|
m.remoteRulesCount = 0
|
||||||
// 保留计数数据,不随规则重新加载而清空
|
// 保留计数数据,不随规则重新加载而清空
|
||||||
|
|
||||||
// 加载本地规则文件
|
// 加载自定义规则文件
|
||||||
if err := m.loadLocalRules(); err != nil {
|
if err := m.loadLocalRules(); err != nil {
|
||||||
logger.Error("加载本地规则失败", "error", err)
|
logger.Error("加载自定义规则失败", "error", err)
|
||||||
// 继续执行,不返回错误
|
// 继续执行,不返回错误
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,19 +132,15 @@ func (m *ShieldManager) LoadRules() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadLocalRules 加载本地规则文件
|
// loadLocalRules 加载自定义规则文件
|
||||||
func (m *ShieldManager) loadLocalRules() error {
|
func (m *ShieldManager) loadLocalRules() error {
|
||||||
if m.config.LocalRulesFile == "" {
|
file, err := os.Open("data/rules.txt")
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Open(m.config.LocalRulesFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// 记录加载前的规则数量,用于计算本地规则数量
|
// 记录加载前的规则数量,用于计算自定义规则数量
|
||||||
beforeDomainRules := len(m.domainRules)
|
beforeDomainRules := len(m.domainRules)
|
||||||
beforeRegexRules := len(m.regexRules)
|
beforeRegexRules := len(m.regexRules)
|
||||||
|
|
||||||
@@ -154,10 +150,10 @@ func (m *ShieldManager) loadLocalRules() error {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.parseRule(line, true, "本地规则") // 本地规则,isLocal=true,来源为"本地规则"
|
m.parseRule(line, true, "自定义规则") // 自定义规则,isLocal=true,来源为"自定义规则"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新本地规则计数
|
// 更新自定义规则计数
|
||||||
m.localRulesCount = (len(m.domainRules) - beforeDomainRules) + (len(m.regexRules) - beforeRegexRules)
|
m.localRulesCount = (len(m.domainRules) - beforeDomainRules) + (len(m.regexRules) - beforeRegexRules)
|
||||||
|
|
||||||
return scanner.Err()
|
return scanner.Err()
|
||||||
@@ -183,7 +179,7 @@ func (m *ShieldManager) getCacheFilePath(url string) string {
|
|||||||
// 简单处理,移除特殊字符,确保文件名合法
|
// 简单处理,移除特殊字符,确保文件名合法
|
||||||
hash = strings.ReplaceAll(hash, "/", "_")
|
hash = strings.ReplaceAll(hash, "/", "_")
|
||||||
hash = strings.ReplaceAll(hash, "\\", "_")
|
hash = strings.ReplaceAll(hash, "\\", "_")
|
||||||
return filepath.Join(m.config.RemoteRulesCacheDir, hash+".rules")
|
return filepath.Join("data/remote_rules", hash+".rules")
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldUpdateCache 检查缓存是否需要更新
|
// shouldUpdateCache 检查缓存是否需要更新
|
||||||
@@ -298,7 +294,7 @@ func (m *ShieldManager) loadCachedRules(filePath string, source string) error {
|
|||||||
// saveRemoteRulesToCache 保存远程规则到缓存文件
|
// saveRemoteRulesToCache 保存远程规则到缓存文件
|
||||||
func (m *ShieldManager) saveRemoteRulesToCache(filePath string, data []byte) error {
|
func (m *ShieldManager) saveRemoteRulesToCache(filePath string, data []byte) error {
|
||||||
// 确保缓存目录存在
|
// 确保缓存目录存在
|
||||||
if err := os.MkdirAll(m.config.RemoteRulesCacheDir, 0755); err != nil {
|
if err := os.MkdirAll("data/remote_rules", 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,11 +304,7 @@ func (m *ShieldManager) saveRemoteRulesToCache(filePath string, data []byte) err
|
|||||||
|
|
||||||
// loadHosts 加载hosts文件
|
// loadHosts 加载hosts文件
|
||||||
func (m *ShieldManager) loadHosts() error {
|
func (m *ShieldManager) loadHosts() error {
|
||||||
if m.config.HostsFile == "" {
|
file, err := os.Open("data/hosts.txt")
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Open(m.config.HostsFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -445,10 +437,16 @@ func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string {
|
|||||||
// addDomainRule 添加域名规则,支持是否为阻止规则
|
// addDomainRule 添加域名规则,支持是否为阻止规则
|
||||||
func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) {
|
func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) {
|
||||||
if block {
|
if block {
|
||||||
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
// 如果是远程规则,检查是否已经存在自定义规则(阻止或排除),如果存在则不覆盖
|
||||||
if !isLocal {
|
if !isLocal {
|
||||||
|
// 检查是否存在自定义阻止规则
|
||||||
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
|
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
|
||||||
// 已经存在本地规则,不覆盖
|
// 已经存在自定义规则,不覆盖
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 检查是否存在自定义排除规则
|
||||||
|
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
|
||||||
|
// 已经存在自定义规则,不覆盖
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,10 +456,16 @@ func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, s
|
|||||||
m.domainRulesOriginal[domain] = original
|
m.domainRulesOriginal[domain] = original
|
||||||
} else {
|
} else {
|
||||||
// 添加到排除规则
|
// 添加到排除规则
|
||||||
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
// 如果是远程规则,检查是否已经存在自定义规则(阻止或排除),如果存在则不覆盖
|
||||||
if !isLocal {
|
if !isLocal {
|
||||||
|
// 检查是否存在自定义阻止规则
|
||||||
|
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
|
||||||
|
// 已经存在自定义规则,不覆盖
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 检查是否存在自定义排除规则
|
||||||
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
|
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
|
||||||
// 已经存在本地规则,不覆盖
|
// 已经存在自定义规则,不覆盖
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,11 +485,19 @@ func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block b
|
|||||||
source: source,
|
source: source,
|
||||||
}
|
}
|
||||||
if block {
|
if block {
|
||||||
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
|
// 如果是远程规则,检查是否已经存在任何自定义规则,如果存在则不添加
|
||||||
if !isLocal {
|
if !isLocal {
|
||||||
|
// 检查是否存在自定义阻止规则
|
||||||
for _, existingRule := range m.regexRules {
|
for _, existingRule := range m.regexRules {
|
||||||
if existingRule.original == original && existingRule.isLocal {
|
if existingRule.pattern.String() == re.String() && existingRule.isLocal {
|
||||||
// 已经存在相同的本地规则,不添加
|
// 已经存在相同的自定义阻止规则,不添加
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 检查是否存在自定义排除规则
|
||||||
|
for _, existingRule := range m.regexExceptions {
|
||||||
|
if existingRule.pattern.String() == re.String() && existingRule.isLocal {
|
||||||
|
// 已经存在相同的自定义排除规则,不添加
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,11 +505,19 @@ func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block b
|
|||||||
m.regexRules = append(m.regexRules, rule)
|
m.regexRules = append(m.regexRules, rule)
|
||||||
} else {
|
} else {
|
||||||
// 添加到排除规则
|
// 添加到排除规则
|
||||||
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
|
// 如果是远程规则,检查是否已经存在任何自定义规则,如果存在则不添加
|
||||||
if !isLocal {
|
if !isLocal {
|
||||||
|
// 检查是否存在自定义阻止规则
|
||||||
|
for _, existingRule := range m.regexRules {
|
||||||
|
if existingRule.pattern.String() == re.String() && existingRule.isLocal {
|
||||||
|
// 已经存在相同的自定义阻止规则,不添加
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 检查是否存在自定义排除规则
|
||||||
for _, existingRule := range m.regexExceptions {
|
for _, existingRule := range m.regexExceptions {
|
||||||
if existingRule.original == original && existingRule.isLocal {
|
if existingRule.pattern.String() == re.String() && existingRule.isLocal {
|
||||||
// 已经存在相同的本地规则,不添加
|
// 已经存在相同的自定义排除规则,不添加
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,8 +557,17 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["hostsIP"] = hostsIP
|
result["hostsIP"] = hostsIP
|
||||||
|
|
||||||
// 检查排除规则(优先级最高)
|
// 检查排除规则(优先级最高)
|
||||||
// 检查域名排除规则
|
// 1. 先检查本地域名排除规则
|
||||||
if m.domainExceptions[domain] {
|
if m.domainExceptions[domain] && m.domainExceptionsIsLocal[domain] {
|
||||||
|
result["excluded"] = true
|
||||||
|
result["excludeRule"] = m.domainExceptionsOriginal[domain]
|
||||||
|
result["excludeRuleType"] = "exact_domain"
|
||||||
|
result["blocksource"] = m.domainExceptionsSource[domain]
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 再检查远程域名排除规则
|
||||||
|
if m.domainExceptions[domain] && !m.domainExceptionsIsLocal[domain] {
|
||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = m.domainExceptionsOriginal[domain]
|
result["excludeRule"] = m.domainExceptionsOriginal[domain]
|
||||||
result["excludeRuleType"] = "exact_domain"
|
result["excludeRuleType"] = "exact_domain"
|
||||||
@@ -548,9 +577,11 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
|
|
||||||
// 检查子域名排除规则
|
// 检查子域名排除规则
|
||||||
parts := strings.Split(domain, ".")
|
parts := strings.Split(domain, ".")
|
||||||
|
|
||||||
|
// 3. 先检查本地子域名排除规则
|
||||||
for i := 0; i < len(parts)-1; i++ {
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
subdomain := strings.Join(parts[i:], ".")
|
subdomain := strings.Join(parts[i:], ".")
|
||||||
if m.domainExceptions[subdomain] {
|
if m.domainExceptions[subdomain] && m.domainExceptionsIsLocal[subdomain] {
|
||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
|
result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
|
||||||
result["excludeRuleType"] = "subdomain"
|
result["excludeRuleType"] = "subdomain"
|
||||||
@@ -559,9 +590,32 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查正则表达式排除规则
|
// 4. 再检查远程子域名排除规则
|
||||||
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
|
subdomain := strings.Join(parts[i:], ".")
|
||||||
|
if m.domainExceptions[subdomain] && !m.domainExceptionsIsLocal[subdomain] {
|
||||||
|
result["excluded"] = true
|
||||||
|
result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
|
||||||
|
result["excludeRuleType"] = "subdomain"
|
||||||
|
result["blocksource"] = m.domainExceptionsSource[subdomain]
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 先检查本地正则表达式排除规则
|
||||||
for _, re := range m.regexExceptions {
|
for _, re := range m.regexExceptions {
|
||||||
if re.pattern.MatchString(domain) {
|
if re.isLocal && re.pattern.MatchString(domain) {
|
||||||
|
result["excluded"] = true
|
||||||
|
result["excludeRule"] = re.original
|
||||||
|
result["excludeRuleType"] = "regex"
|
||||||
|
result["blocksource"] = re.source
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 再检查远程正则表达式排除规则
|
||||||
|
for _, re := range m.regexExceptions {
|
||||||
|
if !re.isLocal && re.pattern.MatchString(domain) {
|
||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = re.original
|
result["excludeRule"] = re.original
|
||||||
result["excludeRuleType"] = "regex"
|
result["excludeRuleType"] = "regex"
|
||||||
@@ -571,8 +625,17 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配
|
// 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配
|
||||||
// 检查精确域名匹配
|
// 7. 先检查本地域名阻止规则
|
||||||
if m.domainRules[domain] {
|
if m.domainRules[domain] && m.domainRulesIsLocal[domain] {
|
||||||
|
result["blocked"] = true
|
||||||
|
result["blockRule"] = m.domainRulesOriginal[domain]
|
||||||
|
result["blockRuleType"] = "exact_domain"
|
||||||
|
result["blocksource"] = m.domainRulesSource[domain]
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 再检查远程域名阻止规则
|
||||||
|
if m.domainRules[domain] && !m.domainRulesIsLocal[domain] {
|
||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = m.domainRulesOriginal[domain]
|
result["blockRule"] = m.domainRulesOriginal[domain]
|
||||||
result["blockRuleType"] = "exact_domain"
|
result["blockRuleType"] = "exact_domain"
|
||||||
@@ -582,9 +645,11 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
|
|
||||||
// 检查子域名匹配(AdGuardHome风格)
|
// 检查子域名匹配(AdGuardHome风格)
|
||||||
// 从最长的子域名开始匹配,确保优先级正确
|
// 从最长的子域名开始匹配,确保优先级正确
|
||||||
|
|
||||||
|
// 9. 先检查本地子域名阻止规则
|
||||||
for i := 0; i < len(parts)-1; i++ {
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
subdomain := strings.Join(parts[i:], ".")
|
subdomain := strings.Join(parts[i:], ".")
|
||||||
if m.domainRules[subdomain] {
|
if m.domainRules[subdomain] && m.domainRulesIsLocal[subdomain] {
|
||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = m.domainRulesOriginal[subdomain]
|
result["blockRule"] = m.domainRulesOriginal[subdomain]
|
||||||
result["blockRuleType"] = "subdomain"
|
result["blockRuleType"] = "subdomain"
|
||||||
@@ -593,9 +658,32 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查正则表达式匹配
|
// 10. 再检查远程子域名阻止规则
|
||||||
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
|
subdomain := strings.Join(parts[i:], ".")
|
||||||
|
if m.domainRules[subdomain] && !m.domainRulesIsLocal[subdomain] {
|
||||||
|
result["blocked"] = true
|
||||||
|
result["blockRule"] = m.domainRulesOriginal[subdomain]
|
||||||
|
result["blockRuleType"] = "subdomain"
|
||||||
|
result["blocksource"] = m.domainRulesSource[subdomain]
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. 先检查本地正则表达式阻止规则
|
||||||
for _, re := range m.regexRules {
|
for _, re := range m.regexRules {
|
||||||
if re.pattern.MatchString(domain) {
|
if re.isLocal && re.pattern.MatchString(domain) {
|
||||||
|
result["blocked"] = true
|
||||||
|
result["blockRule"] = re.original
|
||||||
|
result["blockRuleType"] = "regex"
|
||||||
|
result["blocksource"] = re.source
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. 再检查远程正则表达式阻止规则
|
||||||
|
for _, re := range m.regexRules {
|
||||||
|
if !re.isLocal && re.pattern.MatchString(domain) {
|
||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = re.original
|
result["blockRule"] = re.original
|
||||||
result["blockRuleType"] = "regex"
|
result["blockRuleType"] = "regex"
|
||||||
@@ -722,20 +810,18 @@ func (m *ShieldManager) GetHostsIP(domain string) (string, bool) {
|
|||||||
return ip, exists
|
return ip, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRule 添加屏蔽规则,用户添加的规则是本地规则
|
// AddRule 添加屏蔽规则,用户添加的规则是自定义规则
|
||||||
func (m *ShieldManager) AddRule(rule string) error {
|
func (m *ShieldManager) AddRule(rule string) error {
|
||||||
m.rulesMutex.Lock()
|
m.rulesMutex.Lock()
|
||||||
defer m.rulesMutex.Unlock()
|
defer m.rulesMutex.Unlock()
|
||||||
|
|
||||||
// 解析并添加规则到内存,isLocal=true表示本地规则,来源为"本地规则"
|
// 解析并添加规则到内存,isLocal=true表示自定义规则,来源为"自定义规则"
|
||||||
m.parseRule(rule, true, "本地规则")
|
m.parseRule(rule, true, "自定义规则")
|
||||||
|
|
||||||
// 持久化保存规则到文件
|
// 持久化保存规则到文件
|
||||||
if m.config.LocalRulesFile != "" {
|
if err := m.saveRulesToFile(); err != nil {
|
||||||
if err := m.saveRulesToFile(); err != nil {
|
logger.Error("保存规则到文件失败", "error", err)
|
||||||
logger.Error("保存规则到文件失败", "error", err)
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -900,7 +986,7 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果有规则被删除,持久化保存更改
|
// 如果有规则被删除,持久化保存更改
|
||||||
if removed && m.config.LocalRulesFile != "" {
|
if removed {
|
||||||
if err := m.saveRulesToFile(); err != nil {
|
if err := m.saveRulesToFile(); err != nil {
|
||||||
logger.Error("保存规则到文件失败", "error", err)
|
logger.Error("保存规则到文件失败", "error", err)
|
||||||
return err
|
return err
|
||||||
@@ -957,7 +1043,7 @@ func (m *ShieldManager) StopAutoUpdate() {
|
|||||||
logger.Info("规则自动更新已停止")
|
logger.Info("规则自动更新已停止")
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveRulesToFile 保存规则到文件,只保存本地规则
|
// saveRulesToFile 保存规则到文件,只保存自定义规则
|
||||||
func (m *ShieldManager) saveRulesToFile() error {
|
func (m *ShieldManager) saveRulesToFile() error {
|
||||||
var rules []string
|
var rules []string
|
||||||
|
|
||||||
@@ -991,7 +1077,7 @@ func (m *ShieldManager) saveRulesToFile() error {
|
|||||||
|
|
||||||
// 写入文件
|
// 写入文件
|
||||||
content := strings.Join(rules, "\n")
|
content := strings.Join(rules, "\n")
|
||||||
return ioutil.WriteFile(m.config.LocalRulesFile, []byte(content), 0644)
|
return ioutil.WriteFile("data/rules.txt", []byte(content), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddHostsEntry 添加hosts条目
|
// AddHostsEntry 添加hosts条目
|
||||||
@@ -1002,11 +1088,9 @@ func (m *ShieldManager) AddHostsEntry(ip, domain string) error {
|
|||||||
m.hostsMap[domain] = ip
|
m.hostsMap[domain] = ip
|
||||||
|
|
||||||
// 持久化保存到hosts文件
|
// 持久化保存到hosts文件
|
||||||
if m.config.HostsFile != "" {
|
if err := m.saveHostsToFile(); err != nil {
|
||||||
if err := m.saveHostsToFile(); err != nil {
|
logger.Error("保存hosts到文件失败", "error", err)
|
||||||
logger.Error("保存hosts到文件失败", "error", err)
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -1021,11 +1105,9 @@ func (m *ShieldManager) RemoveHostsEntry(domain string) error {
|
|||||||
delete(m.hostsMap, domain)
|
delete(m.hostsMap, domain)
|
||||||
|
|
||||||
// 持久化保存到hosts文件
|
// 持久化保存到hosts文件
|
||||||
if m.config.HostsFile != "" {
|
if err := m.saveHostsToFile(); err != nil {
|
||||||
if err := m.saveHostsToFile(); err != nil {
|
logger.Error("保存hosts到文件失败", "error", err)
|
||||||
logger.Error("保存hosts到文件失败", "error", err)
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,7 +1137,7 @@ func (m *ShieldManager) saveHostsToFile() error {
|
|||||||
|
|
||||||
// 写入文件
|
// 写入文件
|
||||||
content := strings.Join(lines, "\n")
|
content := strings.Join(lines, "\n")
|
||||||
return ioutil.WriteFile(m.config.HostsFile, []byte(content), 0644)
|
return ioutil.WriteFile("data/hosts.txt", []byte(content), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats 获取规则统计信息
|
// GetStats 获取规则统计信息
|
||||||
@@ -1075,15 +1157,10 @@ func (m *ShieldManager) GetStats() map[string]interface{} {
|
|||||||
|
|
||||||
// loadStatsData 从文件加载计数数据
|
// loadStatsData 从文件加载计数数据
|
||||||
func (m *ShieldManager) loadStatsData() {
|
func (m *ShieldManager) loadStatsData() {
|
||||||
if m.config.StatsFile == "" {
|
|
||||||
logger.Info("Shield统计文件路径未配置,跳过加载")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取绝对路径以避免工作目录问题
|
// 获取绝对路径以避免工作目录问题
|
||||||
statsFilePath, err := filepath.Abs(m.config.StatsFile)
|
statsFilePath, err := filepath.Abs("data/shield_stats.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
|
logger.Error("获取Shield统计文件绝对路径失败", "path", "data/shield_stats.json", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Debug("尝试加载Shield统计数据", "file", statsFilePath)
|
logger.Debug("尝试加载Shield统计数据", "file", statsFilePath)
|
||||||
@@ -1175,15 +1252,10 @@ func (m *ShieldManager) loadStatsData() {
|
|||||||
|
|
||||||
// saveStatsData 保存计数数据到文件
|
// saveStatsData 保存计数数据到文件
|
||||||
func (m *ShieldManager) saveStatsData() {
|
func (m *ShieldManager) saveStatsData() {
|
||||||
if m.config.StatsFile == "" {
|
|
||||||
logger.Debug("Shield统计文件路径未配置,跳过保存")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取绝对路径以避免工作目录问题
|
// 获取绝对路径以避免工作目录问题
|
||||||
statsFilePath, err := filepath.Abs(m.config.StatsFile)
|
statsFilePath, err := filepath.Abs("data/shield_stats.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
|
logger.Error("获取Shield统计文件绝对路径失败", "path", "data/shield_stats.json", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1241,14 +1313,14 @@ func (m *ShieldManager) saveStatsData() {
|
|||||||
|
|
||||||
// startAutoSaveStats 启动计数数据自动保存功能
|
// startAutoSaveStats 启动计数数据自动保存功能
|
||||||
func (m *ShieldManager) startAutoSaveStats() {
|
func (m *ShieldManager) startAutoSaveStats() {
|
||||||
if m.config.StatsFile == "" || m.config.StatsSaveInterval <= 0 {
|
if m.config.StatsSaveInterval <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Duration(m.config.StatsSaveInterval) * time.Second)
|
ticker := time.NewTicker(time.Duration(m.config.StatsSaveInterval) * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
logger.Info("启动Shield计数数据自动保存功能", "interval", m.config.StatsSaveInterval, "file", m.config.StatsFile)
|
logger.Info("启动Shield计数数据自动保存功能", "interval", m.config.StatsSaveInterval, "file", "data/shield_stats.json")
|
||||||
|
|
||||||
// 定期保存数据
|
// 定期保存数据
|
||||||
for {
|
for {
|
||||||
@@ -1293,12 +1365,12 @@ func (m *ShieldManager) GetHostsCount() int {
|
|||||||
return len(m.hostsMap)
|
return len(m.hostsMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLocalRules 获取仅本地规则
|
// GetLocalRules 获取仅自定义规则
|
||||||
func (m *ShieldManager) GetLocalRules() map[string]interface{} {
|
func (m *ShieldManager) GetLocalRules() map[string]interface{} {
|
||||||
m.rulesMutex.RLock()
|
m.rulesMutex.RLock()
|
||||||
defer m.rulesMutex.RUnlock()
|
defer m.rulesMutex.RUnlock()
|
||||||
|
|
||||||
// 转换map和slice为字符串列表,只包含本地规则
|
// 转换map和slice为字符串列表,只包含自定义规则
|
||||||
domainRulesList := make([]string, 0)
|
domainRulesList := make([]string, 0)
|
||||||
for domain, isLocal := range m.domainRulesIsLocal {
|
for domain, isLocal := range m.domainRulesIsLocal {
|
||||||
if isLocal && m.domainRules[domain] {
|
if isLocal && m.domainRules[domain] {
|
||||||
@@ -1329,7 +1401,7 @@ func (m *ShieldManager) GetLocalRules() map[string]interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算本地规则数量
|
// 计算自定义规则数量
|
||||||
localDomainRulesCount := 0
|
localDomainRulesCount := 0
|
||||||
for _, isLocal := range m.domainRulesIsLocal {
|
for _, isLocal := range m.domainRulesIsLocal {
|
||||||
if isLocal {
|
if isLocal {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "DNS Server API",
|
"title": "DNS Server API",
|
||||||
"description": "DNS服务器完整API文档,包括统计信息、Shield管理、主机管理等功能。",
|
"description": "DNS服务器完整API文档,包括统计信息、Shield管理、主机管理等功能。",
|
||||||
"version": "1.1.0",
|
"version": "1.2.5",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "DNS Server 支持",
|
"name": "DNS Server 支持",
|
||||||
"email": "support@dnsserver.com"
|
"email": "support@dnsserver.com"
|
||||||
@@ -713,6 +713,7 @@
|
|||||||
"get": {
|
"get": {
|
||||||
"summary": "获取Shield配置和统计信息",
|
"summary": "获取Shield配置和统计信息",
|
||||||
"description": "获取Shield的配置信息和规则统计,包括更新间隔、屏蔽方法、黑名单数量等。",
|
"description": "获取Shield的配置信息和规则统计,包括更新间隔、屏蔽方法、黑名单数量等。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -764,6 +765,7 @@
|
|||||||
"post": {
|
"post": {
|
||||||
"summary": "添加屏蔽规则",
|
"summary": "添加屏蔽规则",
|
||||||
"description": "添加新的屏蔽规则到Shield。",
|
"description": "添加新的屏蔽规则到Shield。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -829,6 +831,7 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"summary": "删除屏蔽规则",
|
"summary": "删除屏蔽规则",
|
||||||
"description": "从Shield中删除指定的屏蔽规则。",
|
"description": "从Shield中删除指定的屏蔽规则。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -894,6 +897,7 @@
|
|||||||
"put": {
|
"put": {
|
||||||
"summary": "重新加载规则",
|
"summary": "重新加载规则",
|
||||||
"description": "重新加载和应用Shield规则。",
|
"description": "重新加载和应用Shield规则。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -934,7 +938,8 @@
|
|||||||
"/shield/blacklists": {
|
"/shield/blacklists": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "获取黑名单列表",
|
"summary": "获取黑名单列表",
|
||||||
"description": "获取所有远程黑名单的列表及详细信息。",
|
"description": "获取Shield的黑名单列表,包括内置黑名单和自定义黑名单。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -986,7 +991,8 @@
|
|||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "添加黑名单",
|
"summary": "添加黑名单",
|
||||||
"description": "添加新的远程黑名单URL。",
|
"description": "添加新的黑名单URL到Shield。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -1057,6 +1063,7 @@
|
|||||||
"put": {
|
"put": {
|
||||||
"summary": "更新黑名单列表(包括启用/禁用状态)",
|
"summary": "更新黑名单列表(包括启用/禁用状态)",
|
||||||
"description": "更新黑名单列表(包括启用/禁用状态)。",
|
"description": "更新黑名单列表(包括启用/禁用状态)。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -1138,12 +1145,13 @@
|
|||||||
},
|
},
|
||||||
"/shield/localrules": {
|
"/shield/localrules": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "获取本地规则",
|
"summary": "获取自定义规则",
|
||||||
"description": "获取Shield的本地规则列表。",
|
"description": "获取Shield的自定义规则列表。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "成功获取本地规则",
|
"description": "成功获取自定义规则",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -1166,8 +1174,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "添加本地规则",
|
"summary": "添加自定义规则",
|
||||||
"description": "添加新的本地规则。",
|
"description": "添加新的自定义规则。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -1205,8 +1214,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"summary": "删除本地规则",
|
"summary": "删除自定义规则",
|
||||||
"description": "删除指定ID的本地规则。",
|
"description": "删除指定的自定义规则。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -1239,8 +1249,9 @@
|
|||||||
},
|
},
|
||||||
"/shield/remoterules": {
|
"/shield/remoterules": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "获取远程规则",
|
"summary": "获取远程规则状态",
|
||||||
"description": "获取Shield的远程规则列表。",
|
"description": "获取远程规则的更新状态和版本信息。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -1269,8 +1280,9 @@
|
|||||||
},
|
},
|
||||||
"/shield/hosts": {
|
"/shield/hosts": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "获取hosts列表",
|
"summary": "获取hosts内容",
|
||||||
"description": "获取所有hosts记录。",
|
"description": "获取当前的hosts文件内容。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -1298,6 +1310,7 @@
|
|||||||
"post": {
|
"post": {
|
||||||
"summary": "添加hosts记录",
|
"summary": "添加hosts记录",
|
||||||
"description": "添加新的hosts记录。",
|
"description": "添加新的hosts记录。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -1366,6 +1379,7 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"summary": "删除hosts记录",
|
"summary": "删除hosts记录",
|
||||||
"description": "删除指定域名的hosts记录。",
|
"description": "删除指定域名的hosts记录。",
|
||||||
|
"deprecated": false,
|
||||||
"tags": ["shield"],
|
"tags": ["shield"],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -1424,90 +1438,69 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/query": {
|
"/dns/query": {
|
||||||
"get": {
|
"post": {
|
||||||
"summary": "检查域名是否被屏蔽",
|
"summary": "查询DNS记录",
|
||||||
"description": "检查指定域名是否被Shield屏蔽,并返回详细的屏蔽信息,包括屏蔽规则、规则类型、来源等。",
|
"description": "查询指定域名的DNS记录,可以指定记录类型。",
|
||||||
"tags": ["shield"],
|
"tags": ["dns"],
|
||||||
"parameters": [
|
"requestBody": {
|
||||||
{
|
"required": true,
|
||||||
"name": "domain",
|
"content": {
|
||||||
"in": "query",
|
"application/json": {
|
||||||
"required": true,
|
"schema": {
|
||||||
"schema": {
|
"type": "object",
|
||||||
"type": "string"
|
"properties": {
|
||||||
},
|
"domain": {
|
||||||
"description": "要检查的域名"
|
"type": "string",
|
||||||
|
"description": "要查询的域名"
|
||||||
|
},
|
||||||
|
"recordType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "DNS记录类型(如A、AAAA、MX、NS等)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"domain": "example.com",
|
||||||
|
"recordType": "A"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "成功获取域名屏蔽信息",
|
"description": "成功获取DNS记录",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "array",
|
||||||
"properties": {
|
"items": {
|
||||||
"domain": {
|
"type": "object",
|
||||||
"type": "string",
|
"properties": {
|
||||||
"description": "检查的域名"
|
"Type": {
|
||||||
},
|
"type": "string",
|
||||||
"blocked": {
|
"description": "DNS记录类型"
|
||||||
"type": "boolean",
|
},
|
||||||
"description": "是否被屏蔽"
|
"Value": {
|
||||||
},
|
"type": "string",
|
||||||
"blockRule": {
|
"description": "DNS记录值"
|
||||||
"type": "string",
|
},
|
||||||
"description": "屏蔽规则"
|
"TTL": {
|
||||||
},
|
"type": "integer",
|
||||||
"blockRuleType": {
|
"description": "生存时间"
|
||||||
"type": "string",
|
},
|
||||||
"description": "屏蔽规则类型"
|
"Preference": {
|
||||||
},
|
"type": "integer",
|
||||||
"blocksource": {
|
"description": "MX记录优先级"
|
||||||
"type": "string",
|
}
|
||||||
"description": "屏蔽规则来源"
|
|
||||||
},
|
|
||||||
"excluded": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "是否被排除"
|
|
||||||
},
|
|
||||||
"excludeRule": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "排除规则"
|
|
||||||
},
|
|
||||||
"excludeRuleType": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "排除规则类型"
|
|
||||||
},
|
|
||||||
"hasHosts": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "是否有hosts记录"
|
|
||||||
},
|
|
||||||
"hostsIP": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "hosts记录中的IP"
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "查询时间戳"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"example": {
|
"example": [
|
||||||
"domain": "example.com",
|
{"Type": "A", "Value": "93.184.216.34", "TTL": 172800},
|
||||||
"blocked": true,
|
{"Type": "A", "Value": "93.184.216.35", "TTL": 172800}
|
||||||
"blockRule": "example.com",
|
]
|
||||||
"blockRuleType": "exact_domain",
|
|
||||||
"blocksource": "本地规则",
|
|
||||||
"excluded": false,
|
|
||||||
"excludeRule": "",
|
|
||||||
"excludeRuleType": "",
|
|
||||||
"hasHosts": false,
|
|
||||||
"hostsIP": "",
|
|
||||||
"timestamp": "2023-07-15T14:30:45Z"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1063,4 +1063,95 @@ tr:hover {
|
|||||||
.actions-cell {
|
.actions-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 跟踪器状态图标容器 */
|
||||||
|
.tracker-icon-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 跟踪器浮窗样式 */
|
||||||
|
.tracker-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: 100%;
|
||||||
|
margin-left: 10px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 12px;
|
||||||
|
min-width: 250px;
|
||||||
|
z-index: 50;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
/* 添加箭头 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮窗箭头 */
|
||||||
|
.tracker-tooltip::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: -10px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid transparent;
|
||||||
|
border-right: 10px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracker-tooltip::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: -11px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid transparent;
|
||||||
|
border-right: 10px solid #e2e8f0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮窗标题 */
|
||||||
|
.tracker-tooltip .font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d3748;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索框样式优化 */
|
||||||
|
#logs-search {
|
||||||
|
/* 确保搜索框在所有设备上都有合适的宽度 */
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在移动设备上进一步优化搜索框 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 确保搜索框在移动设备上占满宽度 */
|
||||||
|
#logs-search {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮窗内容项 */
|
||||||
|
.tracker-tooltip > div {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮窗链接样式 */
|
||||||
|
.tracker-tooltip a {
|
||||||
|
color: #3182ce;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracker-tooltip a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
@@ -18,9 +18,67 @@
|
|||||||
<body class="bg-gray-50 text-dark font-sans">
|
<body class="bg-gray-50 text-dark font-sans">
|
||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 md:relative md:translate-x-0 -translate-x-full shadow-lg">
|
<aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 hidden md:flex">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="flex items-center justify-center h-16 border-b border-gray-200">
|
||||||
|
<i class="fa fa-server text-3xl text-primary mr-3"></i>
|
||||||
|
<h1 class="text-xl font-bold text-primary">DNS 控制台</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 菜单 -->
|
||||||
|
<nav class="flex-1 overflow-y-auto p-4">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li>
|
||||||
|
<a href="#dashboard" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all sidebar-item-active">
|
||||||
|
<i class="fa fa-tachometer mr-3 text-lg"></i>
|
||||||
|
<span>仪表盘</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#shield" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||||
|
<i class="fa fa-shield mr-3 text-lg"></i>
|
||||||
|
<span>屏蔽管理</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#hosts" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||||
|
<i class="fa fa-file-text mr-3 text-lg"></i>
|
||||||
|
<span>Hosts管理</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="#query" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||||
|
<i class="fa fa-search mr-3 text-lg"></i>
|
||||||
|
<span>DNS屏蔽查询</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#logs" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||||
|
<i class="fa fa-file-text-o mr-3 text-lg"></i>
|
||||||
|
<span>查询日志</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#config" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||||
|
<i class="fa fa-cog mr-3 text-lg"></i>
|
||||||
|
<span>系统设置</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 底部信息 -->
|
||||||
|
<div class="p-4 border-t border-gray-200 text-center text-gray-500 text-sm">
|
||||||
|
<p>DNS服务器 v1.2.0</p>
|
||||||
|
<p class="mt-1" id="uptime">正常运行中</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 移动端侧边栏 -->
|
||||||
|
<aside id="mobile-sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 -translate-x-full md:hidden">
|
||||||
<!-- 移动端关闭按钮 -->
|
<!-- 移动端关闭按钮 -->
|
||||||
<div class="absolute top-4 right-4 md:hidden">
|
<div class="absolute top-4 right-4">
|
||||||
<button id="close-sidebar" class="p-2 text-gray-500 hover:text-gray-700 focus:outline-none">
|
<button id="close-sidebar" class="p-2 text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||||
<i class="fa fa-times text-xl"></i>
|
<i class="fa fa-times text-xl"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -76,7 +134,7 @@
|
|||||||
|
|
||||||
<!-- 底部信息 -->
|
<!-- 底部信息 -->
|
||||||
<div class="p-4 border-t border-gray-200 text-center text-gray-500 text-sm">
|
<div class="p-4 border-t border-gray-200 text-center text-gray-500 text-sm">
|
||||||
<p>DNS服务器 v1.0.0</p>
|
<p>DNS服务器 v1.2.0</p>
|
||||||
<p class="mt-1" id="uptime">正常运行中</p>
|
<p class="mt-1" id="uptime">正常运行中</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -85,9 +143,9 @@
|
|||||||
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden md:hidden"></div>
|
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden md:hidden"></div>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<main class="flex-1 overflow-y-auto">
|
<main class="flex-1 flex flex-col md:ml-64">
|
||||||
<!-- 顶部导航栏 -->
|
<!-- 顶部导航栏 -->
|
||||||
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6">
|
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 sticky top-0 z-30">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button id="toggle-sidebar" class="block md:hidden text-gray-500 hover:text-gray-700 focus:outline-none">
|
<button id="toggle-sidebar" class="block md:hidden text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||||
<i class="fa fa-bars text-xl"></i>
|
<i class="fa fa-bars text-xl"></i>
|
||||||
@@ -96,55 +154,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<!-- 服务器状态组件 -->
|
|
||||||
<div class="relative bg-white rounded-lg shadow-md px-3 py-2 flex items-center space-x-2 server-status-widget md:min-w-[300px] sm:min-w-[250px] min-w-[180px]" id="server-status-widget">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-xs font-medium text-gray-500">CPU</span>
|
|
||||||
<span id="server-cpu-value" class="ml-2 text-sm font-semibold">0%</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-16 h-1 bg-gray-100 rounded-full mt-1">
|
|
||||||
<div id="server-cpu-bar" class="h-full bg-warning rounded-full" style="width: 0%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-xs font-medium text-gray-500">查询</span>
|
|
||||||
<span id="server-queries-value" class="ml-2 text-sm font-semibold">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-16 h-1 bg-gray-100 rounded-full mt-1">
|
|
||||||
<div id="server-queries-bar" class="h-full bg-primary rounded-full" style="width: 0%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 额外指标区域 - 初始隐藏,只在非首页显示 -->
|
|
||||||
<div id="server-additional-stats" class="hidden md:flex items-center">
|
|
||||||
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-xs font-medium text-gray-500">总量</span>
|
|
||||||
<span id="server-total-queries" class="ml-2 text-sm font-semibold">0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-xs font-medium text-gray-500">屏蔽</span>
|
|
||||||
<span id="server-blocked-queries" class="ml-2 text-sm font-semibold">0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-xs font-medium text-gray-500">正常</span>
|
|
||||||
<span id="server-allowed-queries" class="ml-2 text-sm font-semibold">0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="absolute top-1 right-1">
|
|
||||||
<span id="server-status-indicator" class="inline-block w-2 h-2 bg-success rounded-full"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
|
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
|
||||||
<i class="fa fa-bell text-lg"></i>
|
<i class="fa fa-bell text-lg"></i>
|
||||||
@@ -170,7 +179,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 页面内容 -->
|
<!-- 页面内容 -->
|
||||||
<div class="p-6">
|
<div class="p-6 overflow-y-auto flex-1">
|
||||||
<!-- 仪表盘部分 -->
|
<!-- 仪表盘部分 -->
|
||||||
<div id="dashboard-content" class="space-y-6">
|
<div id="dashboard-content" class="space-y-6">
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
@@ -630,9 +639,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 本地规则管理 -->
|
<!-- 自定义规则管理 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||||
<h3 class="text-lg font-semibold mb-6">本地规则管理</h3>
|
<h3 class="text-lg font-semibold mb-6">自定义规则管理</h3>
|
||||||
|
|
||||||
<!-- 添加规则表单 -->
|
<!-- 添加规则表单 -->
|
||||||
<div id="add-rule-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
|
<div id="add-rule-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
|
||||||
@@ -975,7 +984,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">响应时间</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">响应时间</th>
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">屏蔽规则</th>
|
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="logs-table-body">
|
<tbody id="logs-table-body">
|
||||||
@@ -994,13 +1003,32 @@
|
|||||||
<div class="text-sm text-gray-500">
|
<div class="text-sm text-gray-500">
|
||||||
显示 <span id="logs-current-page">1</span> / <span id="logs-total-pages">1</span> 页
|
显示 <span id="logs-current-page">1</span> / <span id="logs-total-pages">1</span> 页
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex items-center space-x-4">
|
||||||
<button id="logs-prev-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
|
<div class="flex items-center space-x-2">
|
||||||
<i class="fa fa-chevron-left"></i>
|
<span class="text-sm text-gray-500">每页显示:</span>
|
||||||
</button>
|
<select id="logs-per-page" class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||||
<button id="logs-next-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
|
<option value="10">10条</option>
|
||||||
<i class="fa fa-chevron-right"></i>
|
<option value="20">20条</option>
|
||||||
</button>
|
<option value="30" selected>30条</option>
|
||||||
|
<option value="50">50条</option>
|
||||||
|
<option value="100">100条</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-sm text-gray-500">页码:</span>
|
||||||
|
<input type="number" id="logs-page-input" min="1" max="1" value="1" class="w-16 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-center">
|
||||||
|
<button id="logs-go-page" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
|
||||||
|
前往
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button id="logs-prev-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
|
||||||
|
<i class="fa fa-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
<button id="logs-next-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
|
||||||
|
<i class="fa fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1030,13 +1058,19 @@
|
|||||||
<input type="text" id="dns-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
|
<input type="text" id="dns-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="dns-stats-file" class="block text-sm font-medium text-gray-700 mb-1">统计文件路径</label>
|
<label for="dns-dnssec-upstream-servers" class="block text-sm font-medium text-gray-700 mb-1">DNSSEC上游DNS服务器 (逗号分隔)</label>
|
||||||
<input type="text" id="dns-stats-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./stats.json">
|
<input type="text" id="dns-dnssec-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="dns-save-interval" class="block text-sm font-medium text-gray-700 mb-1">保存间隔 (秒)</label>
|
<label for="dns-save-interval" class="block text-sm font-medium text-gray-700 mb-1">保存间隔 (秒)</label>
|
||||||
<input type="number" id="dns-save-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="300">
|
<input type="number" id="dns-save-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="300">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="dns-enable-ipv6" class="rounded text-primary focus:ring-primary">
|
||||||
|
<span class="text-sm font-medium text-gray-700">启用IPv6解析(AAAA记录)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1054,15 +1088,6 @@
|
|||||||
<!-- 屏蔽配置 -->
|
<!-- 屏蔽配置 -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h4 class="text-md font-medium mb-4">屏蔽配置</h4>
|
<h4 class="text-md font-medium mb-4">屏蔽配置</h4>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label for="shield-local-rules-file" class="block text-sm font-medium text-gray-700 mb-1">本地规则文件</label>
|
|
||||||
<input type="text" id="shield-local-rules-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./rules.txt">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="shield-hosts-file" class="block text-sm font-medium text-gray-700 mb-1">Hosts文件</label>
|
|
||||||
<input type="text" id="shield-hosts-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="/etc/hosts">
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label for="shield-update-interval" class="block text-sm font-medium text-gray-700 mb-1">更新间隔 (秒)</label>
|
<label for="shield-update-interval" class="block text-sm font-medium text-gray-700 mb-1">更新间隔 (秒)</label>
|
||||||
<input type="number" id="shield-update-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="3600">
|
<input type="number" id="shield-update-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="3600">
|
||||||
@@ -1097,15 +1122,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改密码模态框 -->
|
<!-- 修改密码模态框 -->
|
||||||
|
<!-- 日志详情模态框 -->
|
||||||
|
<div id="log-detail-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-xl font-semibold">日志详情</h3>
|
||||||
|
<button id="close-log-modal-btn" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||||
|
<i class="fa fa-times text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="log-detail-content">
|
||||||
|
<!-- 日志详情内容将通过JS动态填充 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="change-password-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
<div id="change-password-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
||||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h3 class="text-xl font-semibold">修改密码</h3>
|
<h3 class="text-xl font-semibold">修改密码</h3>
|
||||||
<button id="close-modal-btn" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
<button id="close-modal-btn" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||||
<i class="fa fa-times text-xl"></i>
|
<i class="fa fa-times text-xl"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="change-password-form">
|
<form id="change-password-form">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="current-password" class="block text-sm font-medium text-gray-700 mb-1">当前密码</label>
|
<label for="current-password" class="block text-sm font-medium text-gray-700 mb-1">当前密码</label>
|
||||||
@@ -1135,7 +1174,6 @@
|
|||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/dashboard.js"></script>
|
<script src="js/dashboard.js"></script>
|
||||||
<script src="js/server-status.js"></script>
|
|
||||||
<script src="js/shield.js"></script>
|
<script src="js/shield.js"></script>
|
||||||
<script src="js/hosts.js"></script>
|
<script src="js/hosts.js"></script>
|
||||||
<script src="js/query.js"></script>
|
<script src="js/query.js"></script>
|
||||||
|
|||||||
@@ -61,18 +61,20 @@ function populateConfigForm(config) {
|
|||||||
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
|
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
|
||||||
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
|
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
|
||||||
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
|
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
|
||||||
|
setElementValue('dns-dnssec-upstream-servers', getSafeArray(dnsServerConfig.DNSSECUpstreamServers).join(', '));
|
||||||
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
|
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
|
||||||
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
//setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
||||||
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
|
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 30));
|
||||||
|
//setElementValue('dns-cache-ttl', getSafeValue(dnsServerConfig.CacheTTL, 10));
|
||||||
|
setElementValue('dns-enable-ipv6', getSafeValue(dnsServerConfig.EnableIPv6, false));
|
||||||
// HTTP配置
|
// HTTP配置
|
||||||
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
|
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
|
||||||
|
|
||||||
// 屏蔽配置
|
// 屏蔽配置
|
||||||
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
//setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
||||||
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
|
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
|
||||||
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
//setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
||||||
// 使用服务器端接受的屏蔽方法值,默认使用NXDOMAIN
|
// 使用服务器端接受的屏蔽方法值,默认使用NXDOMAIN, 可选值: NXDOMAIN, NULL, REFUSED
|
||||||
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
|
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +82,11 @@ function populateConfigForm(config) {
|
|||||||
function setElementValue(elementId, value) {
|
function setElementValue(elementId, value) {
|
||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
if (element && element.tagName === 'INPUT') {
|
if (element && element.tagName === 'INPUT') {
|
||||||
element.value = value;
|
if (element.type === 'checkbox') {
|
||||||
|
element.checked = value;
|
||||||
|
} else {
|
||||||
|
element.value = value;
|
||||||
|
}
|
||||||
} else if (!element) {
|
} else if (!element) {
|
||||||
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
|
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
|
||||||
}
|
}
|
||||||
@@ -163,6 +169,12 @@ function collectFormData() {
|
|||||||
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||||
[];
|
[];
|
||||||
|
|
||||||
|
// 安全获取DNSSEC上游服务器列表
|
||||||
|
const dnssecUpstreamServersText = getElementValue('dns-dnssec-upstream-servers');
|
||||||
|
const dnssecUpstreamServers = dnssecUpstreamServersText ?
|
||||||
|
dnssecUpstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||||
|
[];
|
||||||
|
|
||||||
// 安全获取并转换整数值
|
// 安全获取并转换整数值
|
||||||
const timeoutValue = getElementValue('dns-timeout');
|
const timeoutValue = getElementValue('dns-timeout');
|
||||||
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
|
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
|
||||||
@@ -174,21 +186,20 @@ function collectFormData() {
|
|||||||
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
|
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
DNSServer: {
|
dnsserver: {
|
||||||
Port: dnsPort,
|
port: dnsPort,
|
||||||
UpstreamServers: upstreamServers,
|
upstreamServers: upstreamServers,
|
||||||
Timeout: timeout,
|
dnssecUpstreamServers: dnssecUpstreamServers,
|
||||||
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
|
timeout: timeout,
|
||||||
SaveInterval: saveInterval
|
saveInterval: saveInterval,
|
||||||
|
enableIPv6: getElementValue('dns-enable-ipv6')
|
||||||
},
|
},
|
||||||
HTTPServer: {
|
httpserver: {
|
||||||
Port: httpPort
|
port: httpPort
|
||||||
},
|
},
|
||||||
Shield: {
|
shield: {
|
||||||
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
|
updateInterval: updateInterval,
|
||||||
UpdateInterval: updateInterval,
|
blockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
|
||||||
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
|
|
||||||
BlockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -197,6 +208,9 @@ function collectFormData() {
|
|||||||
function getElementValue(elementId) {
|
function getElementValue(elementId) {
|
||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
if (element && element.tagName === 'INPUT') {
|
if (element && element.tagName === 'INPUT') {
|
||||||
|
if (element.type === 'checkbox') {
|
||||||
|
return element.checked;
|
||||||
|
}
|
||||||
return element.value;
|
return element.value;
|
||||||
}
|
}
|
||||||
return ''; // 默认返回空字符串
|
return ''; // 默认返回空字符串
|
||||||
|
|||||||
@@ -2964,33 +2964,10 @@ window.addEventListener('hashchange', handleHashChange);
|
|||||||
// 初始化hash路由 - 确保在页面加载时就能被调用
|
// 初始化hash路由 - 确保在页面加载时就能被调用
|
||||||
initHashRoute();
|
initHashRoute();
|
||||||
|
|
||||||
// 侧边栏切换
|
|
||||||
function toggleSidebar() {
|
|
||||||
const sidebar = document.getElementById('sidebar');
|
|
||||||
sidebar.classList.toggle('-translate-x-full');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式处理
|
// 响应式处理
|
||||||
function handleResponsive() {
|
function handleResponsive() {
|
||||||
const toggleBtn = document.getElementById('toggle-sidebar');
|
|
||||||
const sidebar = document.getElementById('sidebar');
|
|
||||||
|
|
||||||
toggleBtn.addEventListener('click', toggleSidebar);
|
|
||||||
|
|
||||||
// 初始状态处理
|
|
||||||
function updateSidebarState() {
|
|
||||||
if (window.innerWidth < 1024) {
|
|
||||||
sidebar.classList.add('-translate-x-full');
|
|
||||||
} else {
|
|
||||||
sidebar.classList.remove('-translate-x-full');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSidebarState();
|
|
||||||
|
|
||||||
// 窗口大小改变时处理
|
// 窗口大小改变时处理
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
updateSidebarState();
|
|
||||||
|
|
||||||
// 更新所有图表大小
|
// 更新所有图表大小
|
||||||
if (dnsRequestsChart) {
|
if (dnsRequestsChart) {
|
||||||
|
|||||||
0
static/js/guide.js
Normal file
0
static/js/guide.js
Normal file
@@ -14,10 +14,83 @@ let currentSortDirection = 'desc'; // 默认降序
|
|||||||
let ipGeolocationCache = {};
|
let ipGeolocationCache = {};
|
||||||
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||||||
|
|
||||||
|
// 跟踪器数据库缓存
|
||||||
|
let trackersDatabase = null;
|
||||||
|
let trackersLoaded = false;
|
||||||
|
let trackersLoading = false;
|
||||||
|
|
||||||
// WebSocket连接和重连计时器
|
// WebSocket连接和重连计时器
|
||||||
let logsWsConnection = null;
|
let logsWsConnection = null;
|
||||||
let logsWsReconnectTimer = null;
|
let logsWsReconnectTimer = null;
|
||||||
|
|
||||||
|
// 加载跟踪器数据库
|
||||||
|
async function loadTrackersDatabase() {
|
||||||
|
if (trackersLoaded) return trackersDatabase;
|
||||||
|
if (trackersLoading) {
|
||||||
|
// 等待正在进行的加载完成
|
||||||
|
while (trackersLoading) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
return trackersDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackersLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/tracker/trackers.json');
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('加载跟踪器数据库失败:', response.statusText);
|
||||||
|
trackersDatabase = { trackers: {} };
|
||||||
|
return trackersDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackersDatabase = await response.json();
|
||||||
|
trackersLoaded = true;
|
||||||
|
return trackersDatabase;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载跟踪器数据库失败:', error);
|
||||||
|
trackersDatabase = { trackers: {} };
|
||||||
|
return trackersDatabase;
|
||||||
|
} finally {
|
||||||
|
trackersLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查域名是否在跟踪器数据库中,并返回跟踪器信息
|
||||||
|
async function isDomainInTrackerDatabase(domain) {
|
||||||
|
if (!trackersDatabase || !trackersLoaded) {
|
||||||
|
await loadTrackersDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trackersDatabase || !trackersDatabase.trackers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查域名是否直接作为跟踪器键存在
|
||||||
|
if (trackersDatabase.trackers.hasOwnProperty(domain)) {
|
||||||
|
return trackersDatabase.trackers[domain];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查域名是否在跟踪器URL中
|
||||||
|
for (const trackerKey in trackersDatabase.trackers) {
|
||||||
|
if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) {
|
||||||
|
const tracker = trackersDatabase.trackers[trackerKey];
|
||||||
|
if (tracker && tracker.url) {
|
||||||
|
try {
|
||||||
|
const trackerUrl = new URL(tracker.url);
|
||||||
|
if (trackerUrl.hostname === domain || trackerUrl.hostname.includes(domain)) {
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略无效URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化查询日志页面
|
// 初始化查询日志页面
|
||||||
function initLogsPage() {
|
function initLogsPage() {
|
||||||
console.log('初始化查询日志页面');
|
console.log('初始化查询日志页面');
|
||||||
@@ -34,13 +107,25 @@ function initLogsPage() {
|
|||||||
// 绑定事件
|
// 绑定事件
|
||||||
bindLogsEvents();
|
bindLogsEvents();
|
||||||
|
|
||||||
|
// 初始化日志详情弹窗
|
||||||
|
initLogDetailModal();
|
||||||
|
|
||||||
// 建立WebSocket连接,用于实时更新统计数据和图表
|
// 建立WebSocket连接,用于实时更新统计数据和图表
|
||||||
connectLogsWebSocket();
|
connectLogsWebSocket();
|
||||||
|
|
||||||
|
// 窗口大小改变时重新加载日志表格
|
||||||
|
window.addEventListener('resize', handleWindowResize);
|
||||||
|
|
||||||
// 在页面卸载时清理资源
|
// 在页面卸载时清理资源
|
||||||
window.addEventListener('beforeunload', cleanupLogsResources);
|
window.addEventListener('beforeunload', cleanupLogsResources);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理窗口大小改变
|
||||||
|
function handleWindowResize() {
|
||||||
|
// 重新加载日志表格,以适应新的屏幕尺寸
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
|
||||||
// 清理资源
|
// 清理资源
|
||||||
function cleanupLogsResources() {
|
function cleanupLogsResources() {
|
||||||
// 清除WebSocket连接
|
// 清除WebSocket连接
|
||||||
@@ -54,6 +139,9 @@ function cleanupLogsResources() {
|
|||||||
clearTimeout(logsWsReconnectTimer);
|
clearTimeout(logsWsReconnectTimer);
|
||||||
logsWsReconnectTimer = null;
|
logsWsReconnectTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清除窗口大小改变事件监听器
|
||||||
|
window.removeEventListener('resize', handleWindowResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
@@ -122,6 +210,34 @@ function bindLogsEvents() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页码跳转
|
||||||
|
const pageInput = document.getElementById('logs-page-input');
|
||||||
|
const goBtn = document.getElementById('logs-go-page');
|
||||||
|
|
||||||
|
if (pageInput) {
|
||||||
|
// 页码输入框回车事件
|
||||||
|
pageInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const page = parseInt(pageInput.value);
|
||||||
|
if (page >= 1 && page <= totalPages) {
|
||||||
|
currentPage = page;
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goBtn) {
|
||||||
|
// 前往按钮点击事件
|
||||||
|
goBtn.addEventListener('click', () => {
|
||||||
|
const page = parseInt(pageInput.value);
|
||||||
|
if (page >= 1 && page <= totalPages) {
|
||||||
|
currentPage = page;
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 时间范围切换
|
// 时间范围切换
|
||||||
const timeRangeBtns = document.querySelectorAll('.time-range-btn');
|
const timeRangeBtns = document.querySelectorAll('.time-range-btn');
|
||||||
timeRangeBtns.forEach(btn => {
|
timeRangeBtns.forEach(btn => {
|
||||||
@@ -282,6 +398,9 @@ function loadLogs() {
|
|||||||
// 更新日志表格
|
// 更新日志表格
|
||||||
updateLogsTable(logs);
|
updateLogsTable(logs);
|
||||||
|
|
||||||
|
// 绑定操作按钮事件
|
||||||
|
bindActionButtonsEvents();
|
||||||
|
|
||||||
// 更新分页信息
|
// 更新分页信息
|
||||||
updateLogsPagination();
|
updateLogsPagination();
|
||||||
|
|
||||||
@@ -301,7 +420,7 @@ function loadLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新日志表格
|
// 更新日志表格
|
||||||
function updateLogsTable(logs) {
|
async function updateLogsTable(logs) {
|
||||||
const tableBody = document.getElementById('logs-table-body');
|
const tableBody = document.getElementById('logs-table-body');
|
||||||
if (!tableBody) return;
|
if (!tableBody) return;
|
||||||
|
|
||||||
@@ -321,13 +440,16 @@ function updateLogsTable(logs) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测是否为移动设备
|
||||||
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
|
||||||
// 填充表格
|
// 填充表格
|
||||||
logs.forEach(log => {
|
for (const log of logs) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
|
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
|
||||||
|
|
||||||
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
|
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
|
||||||
const time = new Date(log.Timestamp);
|
const time = new Date(log.timestamp);
|
||||||
const formattedDate = time.toLocaleDateString('zh-CN', {
|
const formattedDate = time.toLocaleDateString('zh-CN', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
@@ -341,13 +463,13 @@ function updateLogsTable(logs) {
|
|||||||
|
|
||||||
// 根据结果添加不同的背景色
|
// 根据结果添加不同的背景色
|
||||||
let rowClass = '';
|
let rowClass = '';
|
||||||
switch (log.Result) {
|
switch (log.result) {
|
||||||
case 'blocked':
|
case 'blocked':
|
||||||
rowClass = 'bg-red-50'; // 淡红色填充
|
rowClass = 'bg-red-50'; // 淡红色填充
|
||||||
break;
|
break;
|
||||||
case 'allowed':
|
case 'allowed':
|
||||||
// 检查是否是规则允许项目
|
// 检查是否是规则允许项目
|
||||||
if (log.BlockRule && log.BlockRule.includes('allow')) {
|
if (log.blockRule && log.blockRule.includes('allow')) {
|
||||||
rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
|
rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
|
||||||
} else {
|
} else {
|
||||||
rowClass = ''; // 允许的不填充
|
rowClass = ''; // 允许的不填充
|
||||||
@@ -365,7 +487,7 @@ function updateLogsTable(logs) {
|
|||||||
// 添加被屏蔽或允许显示,并增加颜色
|
// 添加被屏蔽或允许显示,并增加颜色
|
||||||
let statusText = '';
|
let statusText = '';
|
||||||
let statusClass = '';
|
let statusClass = '';
|
||||||
switch (log.Result) {
|
switch (log.result) {
|
||||||
case 'blocked':
|
case 'blocked':
|
||||||
statusText = '被屏蔽';
|
statusText = '被屏蔽';
|
||||||
statusClass = 'text-danger';
|
statusClass = 'text-danger';
|
||||||
@@ -383,30 +505,130 @@ function updateLogsTable(logs) {
|
|||||||
statusClass = '';
|
statusClass = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建行内容 - 两行显示,时间列显示时间和日期,请求列显示域名和类型状态
|
// 检查域名是否在跟踪器数据库中
|
||||||
// 添加缓存状态显示
|
const trackerInfo = await isDomainInTrackerDatabase(log.domain);
|
||||||
const cacheStatusClass = log.FromCache ? 'text-primary' : 'text-gray-500';
|
const isTracker = trackerInfo !== null;
|
||||||
const cacheStatusText = log.FromCache ? '缓存' : '非缓存';
|
|
||||||
|
|
||||||
row.innerHTML = `
|
// 构建行内容 - 根据设备类型决定显示内容
|
||||||
<td class="py-3 px-4">
|
// 添加缓存状态显示
|
||||||
<div class="text-sm font-medium">${formattedTime}</div>
|
const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500';
|
||||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
const cacheStatusText = log.fromCache ? '缓存' : '非缓存';
|
||||||
</td>
|
|
||||||
<td class="py-3 px-4 text-sm">
|
// 检查域名是否被拦截
|
||||||
<div class="font-medium">${log.ClientIP}</div>
|
const isBlocked = log.result === 'blocked';
|
||||||
<div class="text-xs text-gray-500 mt-1">${log.Location || '未知 未知'}</div>
|
|
||||||
</td>
|
// 构建跟踪器浮窗内容
|
||||||
<td class="py-3 px-4 text-sm">
|
const trackerTooltip = isTracker ? `
|
||||||
<div class="font-medium">${log.Domain}</div>
|
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
|
||||||
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.FromCache ? '缓存' : '实时'}</span>${log.DNSSEC ? ', <span class="text-green-500"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.EDNS ? ', <span class="text-blue-500"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
|
<div class="font-semibold mb-1">已知跟踪器</div>
|
||||||
</td>
|
<div class="mb-1">名称: ${trackerInfo.name}</div>
|
||||||
<td class="py-3 px-4 text-sm">${log.ResponseTime}ms</td>
|
<div class="mb-1">类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}</div>
|
||||||
<td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td>
|
${trackerInfo.url ? `<div class="mb-1">URL: <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
|
||||||
`;
|
${trackerInfo.source ? `<div class="mb-1">源: ${trackerInfo.source}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
// 移动设备只显示时间和请求信息
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<div class="text-sm font-medium">${formattedTime}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-sm" colspan="4">
|
||||||
|
<div class="font-medium flex items-center relative">
|
||||||
|
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||||
|
<div class="tracker-icon-container relative">
|
||||||
|
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||||
|
${trackerTooltip}
|
||||||
|
</div>
|
||||||
|
${log.domain}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span></div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">客户端: ${log.clientIP}</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// 桌面设备显示完整信息
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<div class="text-sm font-medium">${formattedTime}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-sm">
|
||||||
|
<div class="font-medium">${log.clientIP}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${log.location || '未知 未知'}</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-sm">
|
||||||
|
<div class="font-medium flex items-center relative">
|
||||||
|
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||||
|
<div class="tracker-icon-container relative">
|
||||||
|
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||||
|
${trackerTooltip}
|
||||||
|
</div>
|
||||||
|
${log.domain}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.fromCache ? '缓存' : '非缓存'}</span>${log.dnssec ? ', <span class="text-green-500"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.edns ? ', <span class="text-blue-500"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">DNS 服务器: ${log.dnsServer || '无'}, DNSSEC专用: ${log.dnssecServer || '无'}</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-sm">${log.responseTime}ms</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-center">
|
||||||
|
${isBlocked ?
|
||||||
|
`<button class="unblock-btn px-3 py-1 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-xs" data-domain="${log.domain}">放行</button>` :
|
||||||
|
`<button class="block-btn px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-xs" data-domain="${log.domain}">拦截</button>`
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加跟踪器图标悬停事件
|
||||||
|
if (isTracker) {
|
||||||
|
const iconContainer = row.querySelector('.tracker-icon-container');
|
||||||
|
const tooltip = iconContainer.querySelector('.tracker-tooltip');
|
||||||
|
if (iconContainer && tooltip) {
|
||||||
|
tooltip.style.display = 'none';
|
||||||
|
|
||||||
|
iconContainer.addEventListener('mouseenter', () => {
|
||||||
|
tooltip.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
iconContainer.addEventListener('mouseleave', () => {
|
||||||
|
tooltip.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定按钮事件
|
||||||
|
const blockBtn = row.querySelector('.block-btn');
|
||||||
|
if (blockBtn) {
|
||||||
|
blockBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const domain = e.currentTarget.dataset.domain;
|
||||||
|
blockDomain(domain);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const unblockBtn = row.querySelector('.unblock-btn');
|
||||||
|
if (unblockBtn) {
|
||||||
|
unblockBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const domain = e.currentTarget.dataset.domain;
|
||||||
|
unblockDomain(domain);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定日志详情点击事件
|
||||||
|
row.addEventListener('click', (e) => {
|
||||||
|
// 如果点击的是按钮,不触发详情弹窗
|
||||||
|
if (e.target.closest('button')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Row clicked, log object:', log);
|
||||||
|
showLogDetailModal(log);
|
||||||
|
});
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新分页信息
|
// 更新分页信息
|
||||||
@@ -415,6 +637,13 @@ function updateLogsPagination() {
|
|||||||
document.getElementById('logs-current-page').textContent = currentPage;
|
document.getElementById('logs-current-page').textContent = currentPage;
|
||||||
document.getElementById('logs-total-pages').textContent = totalPages;
|
document.getElementById('logs-total-pages').textContent = totalPages;
|
||||||
|
|
||||||
|
// 更新页码输入框
|
||||||
|
const pageInput = document.getElementById('logs-page-input');
|
||||||
|
if (pageInput) {
|
||||||
|
pageInput.max = totalPages;
|
||||||
|
pageInput.value = currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
// 更新按钮状态
|
// 更新按钮状态
|
||||||
const prevBtn = document.getElementById('logs-prev-page');
|
const prevBtn = document.getElementById('logs-prev-page');
|
||||||
const nextBtn = document.getElementById('logs-next-page');
|
const nextBtn = document.getElementById('logs-next-page');
|
||||||
@@ -613,6 +842,607 @@ function updateLogsStatsFromWebSocket(stats) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 拦截域名
|
||||||
|
async function blockDomain(domain) {
|
||||||
|
try {
|
||||||
|
console.log(`开始拦截域名: ${domain}`);
|
||||||
|
|
||||||
|
// 创建拦截规则,使用AdBlock Plus格式
|
||||||
|
const blockRule = `||${domain}^`;
|
||||||
|
console.log(`创建的拦截规则: ${blockRule}`);
|
||||||
|
|
||||||
|
// 调用API添加拦截规则
|
||||||
|
console.log(`调用API添加拦截规则,路径: /shield, 方法: POST`);
|
||||||
|
const response = await apiRequest('/shield', 'POST', { rule: blockRule });
|
||||||
|
|
||||||
|
console.log(`API响应:`, response);
|
||||||
|
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (response && (response.success || response.status === 'success')) {
|
||||||
|
// 重新加载日志,显示更新后的状态
|
||||||
|
loadLogs();
|
||||||
|
|
||||||
|
// 刷新规则列表
|
||||||
|
refreshRulesList();
|
||||||
|
|
||||||
|
// 显示成功通知
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(`已成功拦截域名: ${domain}`, 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMsg = response ? (response.message || '添加拦截规则失败') : '添加拦截规则失败: 无效的API响应';
|
||||||
|
console.error(`拦截域名失败: ${errorMsg}`);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('拦截域名失败:', error);
|
||||||
|
|
||||||
|
// 显示错误通知
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(`拦截域名失败: ${error.message}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定操作按钮事件
|
||||||
|
function bindActionButtonsEvents() {
|
||||||
|
// 绑定拦截按钮事件
|
||||||
|
const blockBtns = document.querySelectorAll('.block-btn');
|
||||||
|
blockBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const domain = e.currentTarget.dataset.domain;
|
||||||
|
await blockDomain(domain);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绑定放行按钮事件
|
||||||
|
const unblockBtns = document.querySelectorAll('.unblock-btn');
|
||||||
|
unblockBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const domain = e.currentTarget.dataset.domain;
|
||||||
|
await unblockDomain(domain);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新规则列表
|
||||||
|
async function refreshRulesList() {
|
||||||
|
try {
|
||||||
|
// 调用API重新加载规则
|
||||||
|
const response = await apiRequest('/shield', 'GET');
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
// 处理规则列表响应
|
||||||
|
let allRules = [];
|
||||||
|
if (response && typeof response === 'object') {
|
||||||
|
// 合并所有类型的规则到一个数组
|
||||||
|
if (Array.isArray(response.domainRules)) allRules = allRules.concat(response.domainRules);
|
||||||
|
if (Array.isArray(response.domainExceptions)) allRules = allRules.concat(response.domainExceptions);
|
||||||
|
if (Array.isArray(response.regexRules)) allRules = allRules.concat(response.regexRules);
|
||||||
|
if (Array.isArray(response.regexExceptions)) allRules = allRules.concat(response.regexExceptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新规则列表
|
||||||
|
if (window.rules) {
|
||||||
|
rules = allRules;
|
||||||
|
filteredRules = [...rules];
|
||||||
|
|
||||||
|
// 更新规则数量统计
|
||||||
|
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||||
|
window.updateRulesCount(rules.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新规则列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 放行域名
|
||||||
|
async function unblockDomain(domain) {
|
||||||
|
try {
|
||||||
|
console.log(`开始放行域名: ${domain}`);
|
||||||
|
|
||||||
|
// 创建放行规则,使用AdBlock Plus格式
|
||||||
|
const allowRule = `@@||${domain}^`;
|
||||||
|
console.log(`创建的放行规则: ${allowRule}`);
|
||||||
|
|
||||||
|
// 调用API添加放行规则
|
||||||
|
console.log(`调用API添加放行规则,路径: /shield, 方法: POST`);
|
||||||
|
const response = await apiRequest('/shield', 'POST', { rule: allowRule });
|
||||||
|
|
||||||
|
console.log(`API响应:`, response);
|
||||||
|
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (response && (response.success || response.status === 'success')) {
|
||||||
|
// 重新加载日志,显示更新后的状态
|
||||||
|
loadLogs();
|
||||||
|
|
||||||
|
// 刷新规则列表
|
||||||
|
refreshRulesList();
|
||||||
|
|
||||||
|
// 显示成功通知
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(`已成功放行域名: ${domain}`, 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMsg = response ? (response.message || '添加放行规则失败') : '添加放行规则失败: 无效的API响应';
|
||||||
|
console.error(`放行域名失败: ${errorMsg}`);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('放行域名失败:', error);
|
||||||
|
|
||||||
|
// 显示错误通知
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(`放行域名失败: ${error.message}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 独立的DNS记录格式化函数
|
||||||
|
function formatDNSRecords(log, result) {
|
||||||
|
if (result === 'blocked') return '无';
|
||||||
|
|
||||||
|
let records = '';
|
||||||
|
const sources = [
|
||||||
|
log.answers,
|
||||||
|
log.answer,
|
||||||
|
log.Records,
|
||||||
|
log.records,
|
||||||
|
log.response
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const source of sources) {
|
||||||
|
if (records) break;
|
||||||
|
if (!source || source === '无') continue;
|
||||||
|
|
||||||
|
// 处理数组类型
|
||||||
|
if (Array.isArray(source)) {
|
||||||
|
records = source.map(answer => {
|
||||||
|
const type = answer.type || answer.Type || '未知';
|
||||||
|
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
|
||||||
|
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
|
||||||
|
|
||||||
|
// 增强的记录值提取逻辑
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.trim();
|
||||||
|
// 处理制表符分隔的格式
|
||||||
|
if (value.includes('\t') || value.includes('\\t')) {
|
||||||
|
const parts = value.replace(/\\t/g, '\t').split('\t');
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
value = parts[parts.length - 1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理JSON格式
|
||||||
|
else if (value.startsWith('{') && value.endsWith('}')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
value = parsed.data || parsed.value || value;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${type}: ${value} (ttl=${ttl})`;
|
||||||
|
}).join('\n').trim();
|
||||||
|
}
|
||||||
|
// 处理字符串类型
|
||||||
|
else if (typeof source === 'string') {
|
||||||
|
// 尝试解析为JSON数组
|
||||||
|
if (source.startsWith('[') && source.endsWith(']')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(source);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
records = parsed.map(answer => {
|
||||||
|
const type = answer.type || answer.Type || '未知';
|
||||||
|
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
|
||||||
|
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${type}: ${value} (ttl=${ttl})`;
|
||||||
|
}).join('\n').trim();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败,尝试直接格式化
|
||||||
|
records = formatDNSString(source);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 直接格式化字符串
|
||||||
|
records = formatDNSString(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return records || '无解析记录';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化DNS字符串记录
|
||||||
|
function formatDNSString(str) {
|
||||||
|
// 处理可能的转义字符并分割行
|
||||||
|
const recordLines = str.split(/\r?\n/).map(line => line.replace(/^\s+/, '')).filter(line => line.trim() !== '');
|
||||||
|
|
||||||
|
return recordLines.map(line => {
|
||||||
|
// 检查是否已经是标准格式
|
||||||
|
if (line.includes(':') && line.includes('(')) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
// 尝试解析为标准DNS格式
|
||||||
|
const parts = line.split(/\s+/);
|
||||||
|
if (parts.length >= 5) {
|
||||||
|
const type = parts[3];
|
||||||
|
const value = parts.slice(4).join(' ');
|
||||||
|
const ttl = parts[1];
|
||||||
|
return `${type}: ${value} (ttl=${ttl})`;
|
||||||
|
}
|
||||||
|
// 无法解析,返回原始行但移除前导空格
|
||||||
|
return line.replace(/^\s+/, '');
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示日志详情弹窗
|
||||||
|
async function showLogDetailModal(log) {
|
||||||
|
console.log('showLogDetailModal called with log:', JSON.stringify(log, null, 2)); // 输出完整的log对象
|
||||||
|
|
||||||
|
if (!log) {
|
||||||
|
console.error('No log data provided!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 安全获取log属性,提供默认值
|
||||||
|
const timestamp = log.timestamp ? new Date(log.timestamp) : null;
|
||||||
|
const dateStr = timestamp ? timestamp.toLocaleDateString() : '未知';
|
||||||
|
const timeStr = timestamp ? timestamp.toLocaleTimeString() : '未知';
|
||||||
|
const domain = log.domain || '未知';
|
||||||
|
const queryType = log.queryType || '未知';
|
||||||
|
const result = log.result || '未知';
|
||||||
|
const responseTime = log.responseTime || '未知';
|
||||||
|
const clientIP = log.clientIP || '未知';
|
||||||
|
const location = log.location || '未知';
|
||||||
|
const fromCache = log.fromCache || false;
|
||||||
|
const dnssec = log.dnssec || false;
|
||||||
|
const edns = log.edns || false;
|
||||||
|
const dnsServer = log.dnsServer || '无';
|
||||||
|
const dnssecServer = log.dnssecServer || '无';
|
||||||
|
const blockRule = log.blockRule || '无';
|
||||||
|
|
||||||
|
// 检查域名是否在跟踪器数据库中
|
||||||
|
const trackerInfo = await isDomainInTrackerDatabase(log.domain);
|
||||||
|
const isTracker = trackerInfo !== null;
|
||||||
|
|
||||||
|
// 格式化DNS解析记录
|
||||||
|
const dnsRecords = formatDNSRecords(log, result);
|
||||||
|
|
||||||
|
// 创建模态框容器
|
||||||
|
const modalContainer = document.createElement('div');
|
||||||
|
modalContainer.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 animate-fade-in';
|
||||||
|
modalContainer.style.zIndex = '9999';
|
||||||
|
|
||||||
|
// 创建模态框内容
|
||||||
|
const modalContent = document.createElement('div');
|
||||||
|
modalContent.className = 'bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto animate-slide-in';
|
||||||
|
|
||||||
|
// 创建标题栏
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center';
|
||||||
|
|
||||||
|
const title = document.createElement('h3');
|
||||||
|
title.className = 'text-xl font-semibold text-gray-900';
|
||||||
|
title.textContent = '日志详情';
|
||||||
|
|
||||||
|
const closeButton = document.createElement('button');
|
||||||
|
closeButton.innerHTML = '<i class="fa fa-times text-xl"></i>';
|
||||||
|
closeButton.className = 'text-gray-500 hover:text-gray-700 focus:outline-none transition-colors';
|
||||||
|
closeButton.onclick = () => closeModal();
|
||||||
|
|
||||||
|
header.appendChild(title);
|
||||||
|
header.appendChild(closeButton);
|
||||||
|
|
||||||
|
// 创建内容区域
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'p-6 space-y-6';
|
||||||
|
|
||||||
|
// 基本信息部分
|
||||||
|
const basicInfo = document.createElement('div');
|
||||||
|
basicInfo.className = 'space-y-4';
|
||||||
|
|
||||||
|
const basicInfoTitle = document.createElement('h4');
|
||||||
|
basicInfoTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
|
||||||
|
basicInfoTitle.textContent = '基本信息';
|
||||||
|
|
||||||
|
const basicInfoGrid = document.createElement('div');
|
||||||
|
basicInfoGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
|
||||||
|
|
||||||
|
// 添加基本信息项
|
||||||
|
basicInfoGrid.innerHTML = `
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">日期</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${dateStr}</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">时间</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${timeStr}</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">状态</div>
|
||||||
|
<div class="text-sm font-medium ${result === 'blocked' ? 'text-red-600' : result === 'allowed' ? 'text-green-600' : 'text-gray-500'}">
|
||||||
|
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">域名</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900 break-all">${domain}</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">类型</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${queryType}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// DNS特性
|
||||||
|
const dnsFeatures = document.createElement('div');
|
||||||
|
dnsFeatures.className = 'col-span-1 md:col-span-2 space-y-1';
|
||||||
|
dnsFeatures.innerHTML = `
|
||||||
|
<div class="text-xs text-gray-500">DNS特性</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">
|
||||||
|
${dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>DNSSEC ' : ''}
|
||||||
|
${edns ? '<i class="fa fa-exchange text-blue-500 mr-1" title="EDNS已启用"></i>EDNS' : ''}
|
||||||
|
${!dnssec && !edns ? '无' : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 跟踪器信息
|
||||||
|
const trackerDiv = document.createElement('div');
|
||||||
|
trackerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
||||||
|
trackerDiv.innerHTML = `
|
||||||
|
<div class="text-xs text-gray-500">跟踪器信息</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">
|
||||||
|
${isTracker ? `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fa fa-eye text-red-500 mr-1"></i>
|
||||||
|
<span>${trackerInfo.name} (${trackersDatabase.categories[trackerInfo.categoryId] || '未知'})</span>
|
||||||
|
</div>
|
||||||
|
` : '无'}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 解析记录
|
||||||
|
const recordsDiv = document.createElement('div');
|
||||||
|
recordsDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
||||||
|
recordsDiv.innerHTML = `
|
||||||
|
<div class="text-xs text-gray-500">解析记录</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900 whitespace-pre-wrap break-all bg-gray-50 p-3 rounded-md border border-gray-200">${dnsRecords}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// DNS服务器
|
||||||
|
const dnsServerDiv = document.createElement('div');
|
||||||
|
dnsServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
||||||
|
dnsServerDiv.innerHTML = `
|
||||||
|
<div class="text-xs text-gray-500">DNS服务器</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${dnsServer}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// DNSSEC专用服务器
|
||||||
|
const dnssecServerDiv = document.createElement('div');
|
||||||
|
dnssecServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
||||||
|
dnssecServerDiv.innerHTML = `
|
||||||
|
<div class="text-xs text-gray-500">DNSSEC专用服务器</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${dnssecServer}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
basicInfoGrid.appendChild(dnsFeatures);
|
||||||
|
basicInfoGrid.appendChild(trackerDiv);
|
||||||
|
basicInfoGrid.appendChild(recordsDiv);
|
||||||
|
basicInfoGrid.appendChild(dnsServerDiv);
|
||||||
|
basicInfoGrid.appendChild(dnssecServerDiv);
|
||||||
|
|
||||||
|
basicInfo.appendChild(basicInfoTitle);
|
||||||
|
basicInfo.appendChild(basicInfoGrid);
|
||||||
|
|
||||||
|
// 响应细节部分
|
||||||
|
const responseDetails = document.createElement('div');
|
||||||
|
responseDetails.className = 'space-y-4 pt-4 border-t border-gray-200';
|
||||||
|
|
||||||
|
const responseDetailsTitle = document.createElement('h4');
|
||||||
|
responseDetailsTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
|
||||||
|
responseDetailsTitle.textContent = '响应细节';
|
||||||
|
|
||||||
|
// 准备响应细节内容,根据条件添加规则信息
|
||||||
|
let responseDetailsHTML = `
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">响应时间</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${responseTime}毫秒</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">响应代码</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${getResponseCodeText(log.responseCode)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">缓存状态</div>
|
||||||
|
<div class="text-sm font-medium ${fromCache ? 'text-primary' : 'text-gray-500'}">
|
||||||
|
${fromCache ? '缓存' : '非缓存'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 只有被屏蔽或者有自定义规则时才显示规则信息
|
||||||
|
if (result === 'blocked' || (blockRule && blockRule !== '无' && blockRule !== '-')) {
|
||||||
|
responseDetailsHTML += `
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs text-gray-500">规则</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${blockRule || '-'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseGrid = document.createElement('div');
|
||||||
|
responseGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
|
||||||
|
responseGrid.innerHTML = responseDetailsHTML;
|
||||||
|
|
||||||
|
responseDetails.appendChild(responseDetailsTitle);
|
||||||
|
responseDetails.appendChild(responseGrid);
|
||||||
|
|
||||||
|
// 客户端详情部分
|
||||||
|
const clientDetails = document.createElement('div');
|
||||||
|
clientDetails.className = 'space-y-4 pt-4 border-t border-gray-200';
|
||||||
|
|
||||||
|
const clientDetailsTitle = document.createElement('h4');
|
||||||
|
clientDetailsTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
|
||||||
|
clientDetailsTitle.textContent = '客户端详情';
|
||||||
|
|
||||||
|
const clientIPDiv = document.createElement('div');
|
||||||
|
clientIPDiv.className = 'space-y-1';
|
||||||
|
clientIPDiv.innerHTML = `
|
||||||
|
<div class="text-xs text-gray-500">IP地址</div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">${clientIP} (${location})</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
clientDetails.appendChild(clientDetailsTitle);
|
||||||
|
clientDetails.appendChild(clientIPDiv);
|
||||||
|
|
||||||
|
// 组装内容
|
||||||
|
content.appendChild(basicInfo);
|
||||||
|
content.appendChild(responseDetails);
|
||||||
|
content.appendChild(clientDetails);
|
||||||
|
|
||||||
|
// 组装模态框
|
||||||
|
modalContent.appendChild(header);
|
||||||
|
modalContent.appendChild(content);
|
||||||
|
modalContainer.appendChild(modalContent);
|
||||||
|
|
||||||
|
// 添加到页面
|
||||||
|
document.body.appendChild(modalContainer);
|
||||||
|
|
||||||
|
// 关闭模态框函数
|
||||||
|
function closeModal() {
|
||||||
|
modalContainer.classList.add('animate-fade-out');
|
||||||
|
modalContent.classList.add('animate-slide-out');
|
||||||
|
|
||||||
|
// 等待动画结束后移除元素
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(modalContainer);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击外部关闭
|
||||||
|
modalContainer.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modalContainer) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ESC键关闭
|
||||||
|
const handleEsc = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeModal();
|
||||||
|
document.removeEventListener('keydown', handleEsc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEsc);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in showLogDetailModal:', error);
|
||||||
|
|
||||||
|
// 显示错误提示
|
||||||
|
const errorModal = document.createElement('div');
|
||||||
|
errorModal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 animate-fade-in';
|
||||||
|
errorModal.style.zIndex = '9999';
|
||||||
|
|
||||||
|
const errorContent = document.createElement('div');
|
||||||
|
errorContent.className = 'bg-white rounded-xl shadow-2xl p-6 w-full max-w-md animate-slide-in';
|
||||||
|
|
||||||
|
errorContent.innerHTML = `
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">错误</h3>
|
||||||
|
<button onclick="closeErrorModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none transition-colors">
|
||||||
|
<i class="fa fa-times text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-red-600 text-sm">
|
||||||
|
加载日志详情失败: ${error.message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
errorModal.appendChild(errorContent);
|
||||||
|
document.body.appendChild(errorModal);
|
||||||
|
|
||||||
|
// 关闭错误模态框函数
|
||||||
|
function closeErrorModal() {
|
||||||
|
errorModal.classList.add('animate-fade-out');
|
||||||
|
errorContent.classList.add('animate-slide-out');
|
||||||
|
|
||||||
|
// 等待动画结束后移除元素
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(errorModal);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC键关闭错误模态框
|
||||||
|
const handleErrorEsc = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeErrorModal();
|
||||||
|
document.removeEventListener('keydown', handleErrorEsc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleErrorEsc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭日志详情弹窗
|
||||||
|
// 获取响应代码文本
|
||||||
|
function getResponseCodeText(rcode) {
|
||||||
|
const rcodeMap = {
|
||||||
|
0: 'NOERROR',
|
||||||
|
1: 'FORMERR',
|
||||||
|
2: 'SERVFAIL',
|
||||||
|
3: 'NXDOMAIN',
|
||||||
|
4: 'NOTIMP',
|
||||||
|
5: 'REFUSED',
|
||||||
|
6: 'YXDOMAIN',
|
||||||
|
7: 'YXRRSET',
|
||||||
|
8: 'NXRRSET',
|
||||||
|
9: 'NOTAUTH',
|
||||||
|
10: 'NOTZONE'
|
||||||
|
};
|
||||||
|
return rcodeMap[rcode] || `UNKNOWN(${rcode})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLogDetailModal() {
|
||||||
|
const modal = document.getElementById('log-detail-modal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化日志详情弹窗事件
|
||||||
|
function initLogDetailModal() {
|
||||||
|
// 关闭按钮事件
|
||||||
|
const closeBtn = document.getElementById('close-log-modal-btn');
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener('click', closeLogDetailModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击模态框外部关闭
|
||||||
|
const modal = document.getElementById('log-detail-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
closeLogDetailModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC键关闭
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeLogDetailModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 定期更新日志统计数据(备用方案)
|
// 定期更新日志统计数据(备用方案)
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
// 只有在查询日志页面时才更新
|
// 只有在查询日志页面时才更新
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function setupNavigation() {
|
|||||||
// 移动端侧边栏切换
|
// 移动端侧边栏切换
|
||||||
const toggleSidebar = document.getElementById('toggle-sidebar');
|
const toggleSidebar = document.getElementById('toggle-sidebar');
|
||||||
const closeSidebarBtn = document.getElementById('close-sidebar');
|
const closeSidebarBtn = document.getElementById('close-sidebar');
|
||||||
const sidebar = document.getElementById('sidebar');
|
const sidebar = document.getElementById('mobile-sidebar');
|
||||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||||
|
|
||||||
// 打开侧边栏函数
|
// 打开侧边栏函数
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ async function loadShieldStats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载本地规则
|
// 加载自定义规则
|
||||||
async function loadLocalRules() {
|
async function loadLocalRules() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/shield/localrules');
|
const response = await fetch('/api/shield/localrules');
|
||||||
@@ -242,7 +242,7 @@ async function loadLocalRules() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// 更新本地规则数量显示
|
// 更新自定义规则数量显示
|
||||||
if (document.getElementById('local-rules-count')) {
|
if (document.getElementById('local-rules-count')) {
|
||||||
document.getElementById('local-rules-count').textContent = data.localRulesCount || 0;
|
document.getElementById('local-rules-count').textContent = data.localRulesCount || 0;
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@ async function loadLocalRules() {
|
|||||||
// 设置当前规则类型
|
// 设置当前规则类型
|
||||||
currentRulesType = 'local';
|
currentRulesType = 'local';
|
||||||
|
|
||||||
// 合并所有本地规则
|
// 合并所有自定义规则
|
||||||
let rules = [];
|
let rules = [];
|
||||||
// 添加域名规则
|
// 添加域名规则
|
||||||
if (Array.isArray(data.domainRules)) {
|
if (Array.isArray(data.domainRules)) {
|
||||||
@@ -271,8 +271,8 @@ async function loadLocalRules() {
|
|||||||
|
|
||||||
updateRulesTable(rules);
|
updateRulesTable(rules);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载本地规则失败:', error);
|
console.error('加载自定义规则失败:', error);
|
||||||
showNotification('加载本地规则失败', 'error');
|
showNotification('加载自定义规则失败', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,7 +1202,7 @@ let currentRulesType = 'local';
|
|||||||
|
|
||||||
// 设置事件监听器
|
// 设置事件监听器
|
||||||
function setupShieldEventListeners() {
|
function setupShieldEventListeners() {
|
||||||
// 本地规则管理事件
|
// 自定义规则管理事件
|
||||||
const saveRuleBtn = document.getElementById('save-rule-btn');
|
const saveRuleBtn = document.getElementById('save-rule-btn');
|
||||||
if (saveRuleBtn) {
|
if (saveRuleBtn) {
|
||||||
saveRuleBtn.addEventListener('click', handleAddRule);
|
saveRuleBtn.addEventListener('click', handleAddRule);
|
||||||
@@ -1214,7 +1214,7 @@ function setupShieldEventListeners() {
|
|||||||
saveBlacklistBtn.addEventListener('click', handleAddBlacklist);
|
saveBlacklistBtn.addEventListener('click', handleAddBlacklist);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加切换查看本地规则和远程规则的事件监听
|
// 添加切换查看自定义规则和远程规则的事件监听
|
||||||
const viewLocalRulesBtn = document.getElementById('view-local-rules-btn');
|
const viewLocalRulesBtn = document.getElementById('view-local-rules-btn');
|
||||||
if (viewLocalRulesBtn) {
|
if (viewLocalRulesBtn) {
|
||||||
viewLocalRulesBtn.addEventListener('click', loadLocalRules);
|
viewLocalRulesBtn.addEventListener('click', loadLocalRules);
|
||||||
|
|||||||
52
temp_config.json
Normal file
52
temp_config.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"dns": {
|
||||||
|
"port": 5353,
|
||||||
|
"upstreamDNS": [
|
||||||
|
"223.5.5.5:53",
|
||||||
|
"223.6.6.6:53",
|
||||||
|
"117.50.10.10:53",
|
||||||
|
"10.35.10.200:53"
|
||||||
|
],
|
||||||
|
"dnssecUpstreamDNS": [
|
||||||
|
"117.50.10.10:53",
|
||||||
|
"101.226.4.6:53",
|
||||||
|
"218.30.118.6:53",
|
||||||
|
"208.67.220.220:53",
|
||||||
|
"208.67.222.222:53"
|
||||||
|
],
|
||||||
|
"timeout": 5000,
|
||||||
|
"statsFile": "data/stats.json",
|
||||||
|
"saveInterval": 300,
|
||||||
|
"cacheTTL": 30,
|
||||||
|
"enableDNSSEC": true,
|
||||||
|
"queryMode": "parallel",
|
||||||
|
"domainSpecificDNS": {
|
||||||
|
"amazehome.xyz": ["10.35.10.200:53"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"port": 8081,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"enableAPI": true,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin"
|
||||||
|
},
|
||||||
|
"shield": {
|
||||||
|
"localRulesFile": "data/rules.txt",
|
||||||
|
"blacklists": [],
|
||||||
|
"updateInterval": 3600,
|
||||||
|
"hostsFile": "data/hosts.txt",
|
||||||
|
"blockMethod": "NXDOMAIN",
|
||||||
|
"customBlockIP": "",
|
||||||
|
"statsFile": "./data/shield_stats.json",
|
||||||
|
"statsSaveInterval": 60,
|
||||||
|
"remoteRulesCacheDir": "data/remote_rules"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"file": "logs/dns-server-5353.log",
|
||||||
|
"level": "debug",
|
||||||
|
"maxSize": 100,
|
||||||
|
"maxBackups": 10,
|
||||||
|
"maxAge": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
25333
tracker/trackers.json
Normal file
25333
tracker/trackers.json
Normal file
File diff suppressed because it is too large
Load Diff
25333
tracker/trackers.json.bak
Normal file
25333
tracker/trackers.json.bak
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user