增加更多匹配的域名信息

This commit is contained in:
Alex Yang
2026-01-14 23:08:46 +08:00
parent f247eaeaa8
commit 8159577be0
60 changed files with 11716 additions and 1022 deletions

View File

@@ -0,0 +1,79 @@
## DNS服务器性能优化方案
### 问题分析
1. **并行查询模式**:当前配置使用`parallel`模式,会等待所有上游服务器响应后才返回,受最慢服务器影响
2. **DNSSEC验证开销**启用了DNSSEC验证增加了额外的计算和网络请求
3. **过多上游服务器**DNSSEC上游服务器多达5个响应时间差异大
4. **调试级别日志**`debug`级别日志记录大量信息占用CPU和I/O资源
5. **缓存TTL过短**10秒的缓存TTL导致频繁向上游请求
6. **黑名单规则过多**14个启用的黑名单每次请求都需要检查
### 优化方案
#### 1. 修改查询模式为快速返回
*`queryMode``parallel`改为`fastest-ip`或优化默认模式
* 快速返回模式会返回第一个有效响应,而不是等待所有响应
#### 2. 优化DNSSEC配置
* 减少DNSSEC上游服务器数量只保留2-3个可靠的
* 对国内域名禁用DNSSEC验证已配置部分可扩展
#### 3. 调整缓存策略
* 增加`cacheTTL`到60秒或更高减少上游请求频率
* 优化缓存实现,减少锁竞争
#### 4. 降低日志级别
* 将日志级别从`debug`改为`info``warn`,减少日志写入开销
#### 5. 优化黑名单处理
* 合并重复的黑名单规则
* 考虑使用更高效的域名匹配算法
#### 6. 代码优化
* 减少DNSSEC验证的重复调用
* 优化响应合并逻辑,避免不必要的计算
* 调整超时设置,避免过长等待
### 具体修改点
1. **config.json**
* 修改`queryMode``fastest-ip`
* 减少`dnssecUpstreamDNS`数量
* 增加`cacheTTL`到60
* 将日志级别改为`info`
2. **dns/server.go**
* 优化`forwardDNSRequestWithCache`函数减少DNSSEC重复验证
* 优化响应合并逻辑,避免不必要的计算
* 调整并行模式的超时处理
### 预期效果
* 减少响应时间,从当前的秒级降低到毫秒级
* 减少CPU和I/O资源占用
* 提高并发处理能力
* 保持DNS解析的准确性和可靠性

View File

@@ -1,35 +1,49 @@
# DNSSEC状态显示问题修复计划
## 问题分析
用户报告已在配置中启用DNSSEC`enableDNSSEC: true`但界面显示DNSSEC为禁用状态且使用率为0%。经过代码检查,发现问题出在`GetStats`函数中,该函数返回的`Stats`结构体缺少DNSSEC相关字段导致前端无法获取正确的DNSSEC状态和统计信息。
## 修复方案
### 1. 修复`GetStats`函数
**修改文件:** `dns/server.go`
**修改函数:** `GetStats`
**问题:** 当前`GetStats`函数返回的`Stats`结构体缺少DNSSEC相关字段包括
- `DNSSECEnabled`
- `DNSSECQueries`
- `DNSSECSuccess`
- `DNSSECFailed`
* `DNSSECEnabled`
* `DNSSECQueries`
* `DNSSECSuccess`
* `DNSSECFailed`
**解决方案:**`GetStats`函数返回的`Stats`结构体中添加所有DNSSEC相关字段确保前端能获取到正确的DNSSEC状态和统计数据。
## 具体实现步骤
1. **修改`GetStats`函数**
- 在返回的`Stats`结构体中添加`DNSSECEnabled`字段
- 添加`DNSSECQueries`字段
- 添加`DNSSECSuccess`字段
- 添加`DNSSECFailed`字段
* 在返回的`Stats`结构体中添加`DNSSECEnabled`字段
* 添加`DNSSECQueries`字段
* 添加`DNSSECSuccess`字段
* 添加`DNSSECFailed`字段
2. **测试修复效果**
- 重新编译DNS服务器
- 启动服务器
- 使用API查询统计信息确认DNSSEC状态和统计数据正确返回
- 检查前端界面是否显示正确的DNSSEC状态
* 重新编译DNS服务器
* 启动服务器
* 使用API查询统计信息确认DNSSEC状态和统计数据正确返回
* 检查前端界面是否显示正确的DNSSEC状态
## 预期效果
@@ -40,4 +54,5 @@
## 代码修改范围
- `dns/server.go`:修复`GetStats`函数添加缺失的DNSSEC字段
* `dns/server.go`:修复`GetStats`函数添加缺失的DNSSEC字段

View File

@@ -0,0 +1,52 @@
1. **修改QueryLog结构体**
*`dns/server.go`中的`QueryLog`结构体添加`ResponseCode`字段
2. **修改addQueryLog函数**
*`dns/server.go`中的`addQueryLog`函数添加`responseCode`参数
* 将响应代码记录到QueryLog结构体中
3. **修改DNS请求处理逻辑**
*`handleDNSRequest`函数中,获取实际的响应代码
* 将响应代码传递给`addQueryLog`函数
4. **修改前端模板**
*`static/js/logs.js`中,将响应代码的硬编码值"无"替换为从日志数据中获取的实际响应代码
* 添加响应代码映射,将数字响应代码转换为可读的字符串
5. **编译和测试**
* 重新编译项目
* 测试DNS查询详情中响应代码是否正确显示
**DNS响应代码映射**
* 0: NOERROR
* 1: FORMERR
* 2: SERVFAIL
* 3: NXDOMAIN
* 4: NOTIMP
* 5: REFUSED
* 6: YXDOMAIN
* 7: YXRRSET
* 8: NXRRSET
* 9: NOTAUTH
* 10: NOTZONE

View File

@@ -0,0 +1,42 @@
1. **调整DNS超时时间**
* 将配置文件中的`timeout`值从5毫秒增加到5000毫秒5秒
* 5毫秒的超时时间对于DNS查询来说太短导致大部分查询都超时失败
2. **优化查询模式**
* 将查询模式从`parallel`(并行)改为`loadbalance`(负载均衡)
* 并行模式在短超时时间下会导致大量超时,负载均衡模式更可靠
3. **检查上游DNS服务器配置**
* 确保所有配置的上游DNS服务器都能正常工作
* 移除或调整可能不可达的DNS服务器
4. **调整DNSSEC配置**
* 检查DNSSEC专用服务器的可达性
* 考虑暂时禁用DNSSEC验证观察是否能改善性能
5. **增强错误处理**
* 优化`forwardDNSRequestWithCache`函数的错误处理逻辑
* 确保在所有服务器都超时的情况下有合理的回退机制
6. **监控和日志优化**
* 添加更详细的日志记录每个DNS服务器的响应情况
* 增加监控指标追踪DNS查询成功率和响应时间
7. **测试验证**
* 在修改后进行DNS查询测试确保服务器能正常响应
* 监控日志确认不再出现大量DNS查询失败的情况

View File

@@ -0,0 +1,53 @@
## 问题分析
DNS服务器出现"Server Failed"的根本原因是当用户配置的DNS服务器地址没有包含端口号时代码直接将其传递给`dns.Client.Exchange()`方法,而该方法需要完整的"IP:端口"格式地址。
## 解决方案
### 1. 创建DNS服务器地址处理函数
- **功能**确保DNS服务器地址始终包含端口号默认添加53端口
- **实现**:创建`normalizeDNSServerAddress`函数,检查并添加端口号
### 2. 应用地址处理函数到所有DNS服务器配置
**需要修改的位置**
- **主DNS服务器**`s.config.UpstreamDNS`
- **DNSSEC专用服务器**`s.config.DNSSECUpstreamDNS`
- **域名特定DNS服务器**`s.config.DomainSpecificDNS`
- **所有调用`resolver.Exchange()`的地方**:确保传递的服务器地址包含端口号
### 3. 修改具体代码位置
**文件**`dns/server.go`
**修改点**
1. **添加地址处理函数**:在文件中添加`normalizeDNSServerAddress`函数
2. **在parallel模式中使用**修改第865行附近的代码
3. **在loadbalance模式中使用**修改第1063行附近的代码
4. **在fastest-ip模式中使用**修改第1189行附近的代码
5. **在default模式中使用**修改第1311行附近的代码
6. **在DNSSEC专用服务器请求中使用**修改第1452行附近的代码
7. **在本地解析中使用**修改第1550行附近的代码
### 4. 确保配置文件加载时也处理地址
- 检查配置文件加载代码确保在加载配置时就处理DNS服务器地址
- 或者在每次使用DNS服务器地址时动态处理
## 修复步骤
1. **创建地址处理函数**:实现`normalizeDNSServerAddress`函数
2. **修改所有DNS查询点**:在所有调用`resolver.Exchange()`的地方使用该函数
3. **测试修复效果**重启DNS服务器并测试查询功能
4. **验证各种配置场景**测试带端口和不带端口的DNS服务器配置
## 预期效果
- 当用户配置DNS服务器为`223.5.5.5`时,自动添加端口变为`223.5.5.5:53`
- 当用户配置DNS服务器为`8.8.8.8:53`时,保持不变
- DNS查询成功率显著提高不再出现"Server Failed"错误
- 支持各种DNS服务器配置格式提高系统兼容性
## 关键文件修改
- `/root/dns/dns/server.go`添加地址处理函数并应用到所有DNS查询点

View File

@@ -0,0 +1,52 @@
## 问题分析
当前DNS服务器的解析记录显示存在以下问题
1. 前端`logs.js`中使用了`console.log`来调试和显示解析记录
2. 需要确保API返回的解析记录是正确的JSON格式
3. 前端需要正确解析API返回的JSON数据来显示解析记录
## 解决方案
### 1. 优化API返回格式
**文件**`dns/server.go`
**修改内容**
- 确保`QueryLog`结构体的`Answers`字段正确序列化为JSON
- 检查`DNSAnswer`结构体的JSON标签是否正确
### 2. 清理前端console.log代码
**文件**`static/js/logs.js`
**修改内容**
- 删除或注释掉第1047、1054行等console.log调试代码
- 优化解析记录提取逻辑确保正确处理API返回的JSON数据
### 3. 优化解析记录显示逻辑
**文件**`static/js/logs.js`
**修改内容**
- 完善`extractDNSRecords`函数,确保正确处理各种格式的解析记录
- 优化解析记录的HTML渲染逻辑确保显示格式清晰
- 确保支持`log.answers`字段(小写)和`log.Answers`字段(大写)
### 4. 测试验证
**步骤**
- 重启DNS服务器
- 使用API测试工具验证`/api/logs/query`返回的解析记录格式正确
- 测试前端页面解析记录显示正常
## 预期效果
- API返回的解析记录格式为标准JSON
- 前端页面不再使用console.log显示解析记录
- 解析记录显示清晰、格式统一
- 支持各种情况下的解析记录提取
## 关键文件修改
1. **`dns/server.go`**:确保解析记录正确序列化
2. **`static/js/logs.js`**优化解析记录显示逻辑移除console.log代码

View File

@@ -0,0 +1,43 @@
## 问题分析
当前服务器代码存在以下问题:
1. `QueryLog`结构体中只有部分字段有JSON标签
2. 缺少完整的JSON序列化支持导致API返回的JSON格式不完整
3. 需要确保所有字段都能正确序列化为JSON
## 解决方案
### 1. 完善QueryLog结构体的JSON标签
**文件**`dns/server.go`
**修改内容**
-`QueryLog`结构体的所有字段添加正确的JSON标签
- 确保`Timestamp`字段正确序列化为ISO格式时间
- 确保`Answers`字段序列化为`"answers"`(小写)
### 2. 确保API返回完整的JSON数据
**文件**`http/server.go`
**修改内容**
- 检查`handleLogsQuery`函数,确保返回完整的日志数据
- 确保日志查询API返回包含所有必要字段的JSON数据
### 3. 测试验证
**步骤**
- 重启DNS服务器
- 使用API测试工具验证`/api/logs/query`返回的JSON格式正确
- 确保所有字段都正确序列化
## 预期效果
- API返回的JSON数据包含所有日志字段
- 前端能够正确解析API返回的JSON数据
- 解析记录通过API查询方式显示不再使用console.log
## 关键文件修改
1. **`dns/server.go`**:完善`QueryLog`结构体的JSON标签
2. **`http/server.go`**确保API返回完整的JSON数据

View File

@@ -0,0 +1,31 @@
## 问题分析
从用户提供的截图可以看到解析记录显示存在问题只显示了IP地址"104.26.24.30",而没有完整的解析记录格式,如"A: 104.26.24.30 (ttl=193)"。
## 根本原因
通过分析代码,发现问题可能出在以下几个方面:
1. 解析记录的显示样式可能存在问题
2. 或者在生成解析记录字符串时出现了问题
3. 或者是在处理`dnsAnswers`数组时出现了问题
## 修复方案
1. 修改解析记录的生成逻辑确保完整显示记录类型、值和TTL
2. 检查并调整HTML元素的样式确保多行文本正确显示
## 具体修改点
1. **修改解析记录的生成逻辑**
-`showLogDetailModal`函数中修改解析记录的生成逻辑确保即使记录类型或TTL为空也能正确显示
- 确保每个解析记录都按照"类型: 值 (ttl=TTL)"的格式显示
2. **调整HTML元素的样式**
- 检查并调整解析记录显示容器的样式,确保多行文本正确显示
- 确保`whitespace-pre-wrap`样式正确应用
## 修复原则
- 确保解析记录完整显示包括记录类型、值和TTL
- 保持良好的可读性
- 确保样式兼容各种浏览器
## 验证方法
1. 修复代码后,重新加载页面
2. 查看解析记录是否完整显示包括记录类型、值和TTL
3. 测试不同类型的解析记录,确保都能正确显示

View File

@@ -0,0 +1,34 @@
## 移除查询日志详情中的屏蔽规则列
### 1. 问题分析
- 用户要求移除查询日志详情弹窗中的屏蔽规则列
- 屏蔽规则列位于响应细节部分,显示在响应时间和响应代码之间
- 该列显示了DNS查询被屏蔽时的规则信息
### 2. 实现方案
- 编辑`showLogDetailModal`函数找到响应细节部分的HTML模板
- 移除其中包含"规则"标题和`${blockRule}`变量的整个div元素
- 保持其他响应细节(响应时间、响应代码、缓存状态)不变
### 3. 代码修改
- 修改文件:`/root/dns/static/js/logs.js`
- 修改函数:`showLogDetailModal`
- 移除位置:响应细节部分的`responseGrid` HTML模板
- 移除内容:包含"规则"标题和`${blockRule}`变量的div元素
### 4. 预期效果
- 查询日志详情弹窗中将不再显示屏蔽规则列
- 响应细节部分将只显示:响应时间、响应代码、缓存状态
- 保持弹窗的整体布局和样式不变
- 不影响其他功能的正常运行
### 5. 技术细节
- 使用HTML模板字符串修改DOM结构
- 移除不必要的DOM元素简化UI
- 保持代码的可读性和可维护性
### 6. 测试验证
- 验证修改后的代码是否有语法错误
- 验证查询日志详情弹窗是否正常显示
- 验证屏蔽规则列已被成功移除
- 验证其他功能是否正常工作

View File

@@ -0,0 +1,56 @@
## 移除查询日志详情中的屏蔽规则列
### 1. 问题分析
* 用户要求移除查询日志详情弹窗中的屏蔽规则列
* 屏蔽规则列位于响应细节部分,显示在响应时间和响应代码之间
* 该列显示了DNS查询被屏蔽时的规则信息
### 2. 实现方案
* 编辑`showLogDetailModal`函数找到响应细节部分的HTML模板
* 移除其中包含"规则"标题和`${blockRule}`变量的整个div元素
* 保持其他响应细节(响应时间、响应代码、缓存状态)不变
### 3. 代码修改
* 修改文件:`/root/dns/static/js/logs.js`
* 修改函数:`showLogDetailModal`
* 移除位置:响应细节部分的`responseGrid` HTML模板
* 移除内容:包含"规则"标题和`${blockRule}`变量的div元素
### 4. 预期效果
* 查询日志详情弹窗中将不再显示屏蔽规则列
* 响应细节部分将只显示:响应时间、响应代码、缓存状态
* 保持弹窗的整体布局和样式不变
* 不影响其他功能的正常运行
### 5. 技术细节
* 使用HTML模板字符串修改DOM结构
* 移除不必要的DOM元素简化UI
* 保持代码的可读性和可维护性
### 6. 测试验证
* 验证修改后的代码是否有语法错误
* 验证查询日志详情弹窗是否正常显示
* 验证屏蔽规则列已被成功移除
* 验证其他功能是否正常工作

View File

@@ -0,0 +1,51 @@
## 优化DNS请求处理逻辑减少返回客户端超时
### 1. 问题分析
* 服务器请求上游解析成功,但返回给客户端超时
* 主要原因是`forwardDNSRequestWithCache`函数等待所有上游服务器响应,导致某个慢服务器拖慢整体响应
* 虽然是并行查询,但没有实现快速响应返回机制
* 阻塞式等待所有响应完成,而不是优先返回最快的成功响应
### 2. 实现方案
* 优化`forwardDNSRequestWithCache`函数,实现快速响应返回机制
* 当收到第一个成功响应时,立即返回给客户端,不再等待其他服务器响应
* 保持后台继续接收其他响应,更新最佳响应和服务器状态
* 优化并行查询逻辑,提高响应速度
* 保持代码的可读性和可维护性
### 3. 代码修改
* 修改文件:`/root/dns/dns/server.go`
* 修改函数:`forwardDNSRequestWithCache`
* 优化位置:`parallel`模式和`default`模式下的响应处理逻辑
* 优化内容:
- 实现快速响应返回机制
- 当收到第一个成功响应时,立即返回给客户端
- 保持后台处理其他响应
- 优化并行查询逻辑
### 4. 预期效果
* 减少DNS查询的平均响应时间
* 避免因某个上游服务器响应慢而导致整体超时
* 提高DNS服务器的吞吐量和并发处理能力
* 保持对上游服务器状态的准确跟踪
* 不影响现有功能的正常运行
### 5. 技术细节
* 使用通道和goroutine实现非阻塞响应处理
* 当收到第一个成功响应时,立即返回给客户端
* 保持后台继续接收其他响应,更新最佳响应和服务器状态
* 优化并行查询逻辑,提高响应速度
* 保持代码的可读性和可维护性
### 6. 测试验证
* 验证修改后的代码是否有语法错误
* 验证DNS查询响应时间是否明显减少
* 验证是否解决了返回客户端超时的问题
* 验证其他功能是否正常工作
* 验证上游服务器状态跟踪是否准确

View File

@@ -0,0 +1,78 @@
## 优化DNS请求处理逻辑减少返回客户端超时
### 1. 问题分析
* 服务器请求上游解析成功,但返回给客户端超时
* 主要原因是`forwardDNSRequestWithCache`函数等待所有上游服务器响应,导致某个慢服务器拖慢整体响应
* 虽然是并行查询,但没有实现快速响应返回机制
* 阻塞式等待所有响应完成,而不是优先返回最快的成功响应
### 2. 实现方案
* 优化`forwardDNSRequestWithCache`函数,实现快速响应返回机制
* 当收到第一个成功响应时,立即返回给客户端,不再等待其他服务器响应
* 保持后台继续接收其他响应,更新最佳响应和服务器状态
* 优化并行查询逻辑,提高响应速度
* 保持代码的可读性和可维护性
### 3. 代码修改
* 修改文件:`/root/dns/dns/server.go`
* 修改函数:`forwardDNSRequestWithCache`
* 优化位置:`parallel`模式和`default`模式下的响应处理逻辑
* 优化内容:
* 实现快速响应返回机制
* 当收到第一个成功响应时,立即返回给客户端
* 保持后台处理其他响应
* 优化并行查询逻辑
### 4. 预期效果
* 减少DNS查询的平均响应时间
* 避免因某个上游服务器响应慢而导致整体超时
* 提高DNS服务器的吞吐量和并发处理能力
* 保持对上游服务器状态的准确跟踪
* 不影响现有功能的正常运行
### 5. 技术细节
* 使用通道和goroutine实现非阻塞响应处理
* 当收到第一个成功响应时,立即返回给客户端
* 保持后台继续接收其他响应,更新最佳响应和服务器状态
* 优化并行查询逻辑,提高响应速度
* 保持代码的可读性和可维护性
### 6. 测试验证
* 验证修改后的代码是否有语法错误
* 验证DNS查询响应时间是否明显减少
* 验证是否解决了返回客户端超时的问题
* 验证其他功能是否正常工作
* 验证上游服务器状态跟踪是否准确

View File

@@ -0,0 +1,20 @@
# 修复域名匹配机制问题
## 问题分析
通过详细检查domain-info.json文件我发现了问题的根本原因
- JSON文件的结构存在错误`domains`对象只包含了网易公司
- 其他公司(如阿里云、百度等)被错误地放在了`domains`对象之外
- 这导致域名匹配机制只能找到网易公司的信息,而无法匹配其他公司
## 修复计划
1. **修复JSON文件结构**:将所有公司信息正确地包含在`domains`对象中
2. **优化域名匹配逻辑**根据用户需求实现优先匹配完整URL域名再匹配主域名的逻辑
3. **测试修复效果**:验证所有公司的域名都能被正确匹配
## 预期效果
- 修复后的JSON文件结构正确所有公司都包含在`domains`对象中
- 域名匹配机制能够正确识别所有公司的域名
- 匹配规则:
- 优先匹配完整URL域名如baike.baidu.com
- 如果没有匹配上则匹配主域名如baidu.com
- 输出对应的公司名称(如北京百度网讯科技有限公司)

View File

@@ -0,0 +1,33 @@
## 问题分析
通过测试脚本的输出我发现了kdocs.cn无法匹配到域名信息的原因
1. **JSON结构错误**domain-info.json文件中字节跳动公司的结构存在错误导致解析后的对象结构不正确。
2. **遍历顺序问题**由于JSON结构错误当遍历到字节跳动公司时脚本没有正确跳过company属性而是继续处理字节跳动对象内部的属性然后遇到了一个名为"company"的公司这个公司的属性值是categories对象的值。
3. **遍历不完整**由于结构错误脚本在遍历到字节跳动公司后就无法继续遍历到金山办公公司而金山办公公司正是kdocs.cn所属的公司。
## 解决方案
### 1. 修复domain-info.json文件的结构
修复字节跳动公司的结构错误,确保其包含正确的闭合括号和逗号:
- 修复抖音视频对象的闭合括号
- 确保今日头条API服务和豆包对象是字节跳动公司的直接子对象
- 确保company属性是字节跳动公司的直接子属性
### 2. 测试修复效果
修复后重新运行测试脚本验证kdocs.cn和www.kdocs.cn能够正确匹配到金山办公公司的金山文档。
## 实施步骤
1. 修改domain-info.json文件修复字节跳动公司的结构错误
2. 运行test-fetch.js测试脚本验证修复效果
3. 确认kdocs.cn和www.kdocs.cn能够正确匹配到域名信息
## 预期效果
修复后kdocs.cn和www.kdocs.cn将能够正确匹配到金山办公公司的金山文档显示网站名称、图标、类别和所属公司。

View File

@@ -0,0 +1,32 @@
# 移除负载均衡查询模式
## 问题分析
用户要求移除负载均衡查询模式,目前代码中支持多种查询模式,包括 "loadbalance"(负载均衡)、"parallel"(并行请求)、"fastest-ip"最快的IP地址。负载均衡模式使用加权随机选择算法来选择上游服务器。
## 解决方案
1. 从配置中移除 "loadbalance" 作为可用选项
2. 从代码中移除 "loadbalance" 分支
3. 确保其他使用 `selectWeightedRandomServer` 函数的地方不受影响
## 实施步骤
1. 修改 `config/config.go` 文件,更新 `QueryMode` 字段的注释,移除 "loadbalance" 选项
2. 修改 `dns/server.go` 文件,移除 switch 语句中的 "loadbalance" 分支
3. 确保所有使用 `selectWeightedRandomServer` 函数的地方仍然正常工作
4. 测试修改后的代码,确保 DNS 服务器仍然正常运行
## 修改内容
- 文件:`/root/dns/config/config.go`
- 修改点:更新 `QueryMode` 字段的注释,移除 "loadbalance" 选项
- 文件:`/root/dns/dns/server.go`
- 修改点:移除 switch 语句中的 "loadbalance" 分支第1139-1260行
## 预期效果
- 负载均衡查询模式将不再可用
- DNS 服务器仍然支持其他查询模式parallel、fastest-ip
- 其他功能不受影响
- 代码更加简洁,减少了维护成本
## 注意事项
- `selectWeightedRandomServer` 函数不仅用于负载均衡模式,还用于选择 DNSSEC 服务器和本地服务器,所以不能删除这个函数
- 确保修改后所有其他功能仍然正常工作

View File

@@ -0,0 +1,32 @@
# 清理无关代码
## 问题分析
我已经成功移除了负载均衡查询模式的主要代码,但还有一些相关的残留代码需要清理,包括:
1. CHANGELOG.md文件中提到了loadbalance模式
2. 可能还有其他未使用的代码或配置
## 解决方案
1. 更新CHANGELOG.md文件移除或修改与loadbalance模式相关的条目
2. 检查是否有其他未使用的代码或配置
3. 确保所有修改都不会影响现有功能
## 实施步骤
1. 修改CHANGELOG.md文件移除或修改与loadbalance模式相关的条目
2. 检查是否有其他未使用的代码或配置
3. 编译并测试修改后的代码,确保功能正常
## 修改内容
- 文件:`/root/dns/CHANGELOG.md`
- 修改点1第106行移除或修改与loadbalance模式相关的条目
- 修改点2第112行移除或修改与loadbalance模式相关的条目
## 预期效果
- 代码库中不再有与已移除功能相关的残留代码
- 文档与实际代码保持一致
- 现有功能不受影响
- 代码库更加整洁,易于维护
## 注意事项
- 不要修改数据文件中的loadbalance相关规则这些是广告过滤规则不是代码
- 确保所有修改都不会影响现有功能
- 编译并测试修改后的代码,确保功能正常

View File

@@ -0,0 +1,113 @@
## 支持Base64编码GFWList格式的实现计划
### 问题分析
当前的GFWList加载代码直接按行解析文件内容无法处理Base64编码的GFWList格式。Base64编码的GFWList文件包含
1. 头部注释行(以!开头)
2. Base64编码的规则内容整个文件或主要部分
3. 可能的尾部注释
### 解决方案
修改`gfw/manager.go`中的`LoadRules`函数添加Base64解码支持
1. **检测Base64编码**
- 读取文件内容
- 跳过注释行
- 检查剩余内容是否为Base64编码
2. **解码Base64内容**
- 如果是Base64编码使用`encoding/base64`包解码
- 解码后得到原始规则文本
3. **解析规则**
- 按行分割解码后的内容
- 调用现有的`parseRule`函数处理每一行
### 具体修改点
1. **添加Base64依赖**
-`gfw/manager.go`中添加`encoding/base64`导入
2. **修改LoadRules函数**
- 读取文件内容
- 过滤注释行收集可能的Base64内容
- 尝试Base64解码
- 解析解码后的规则
3. **测试**
- 使用现有的Base64编码GFWList文件测试
- 确保解码和规则解析正常工作
### 预期效果
- 服务器能够正确加载Base64编码的GFWList文件
- 兼容现有的纯文本GFWList格式
- 规则解析结果与原格式一致
### 代码修改示例
```go
// 在import中添加encoding/base64
import (
// 现有导入
"encoding/base64"
// 现有导入
)
// 修改LoadRules函数
func (m *GFWListManager) LoadRules() error {
// 现有代码...
// 从文件路径读取GFWList内容
content, err := os.ReadFile(m.config.Content)
if err != nil {
return fmt.Errorf("读取GFWList文件失败: %w", err)
}
// 处理Base64编码
rawContent := string(content)
// 过滤注释行收集Base64内容
var base64Content strings.Builder
lines := strings.Split(rawContent, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "!") || strings.HasPrefix(line, "[") {
// 跳过注释行和头信息
continue
}
base64Content.WriteString(line)
}
// 尝试Base64解码
decoded, err := base64.StdEncoding.DecodeString(base64Content.String())
if err == nil {
// 解码成功,使用解码后的内容
rawContent = string(decoded)
} else {
// 解码失败,使用原始内容(可能是纯文本格式)
}
// 按行解析规则
ruleLines := strings.Split(rawContent, "\n")
for _, line := range ruleLines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "!") || strings.HasPrefix(line, "[") {
// 跳过空行、注释行和头信息行
continue
}
m.parseRule(line)
}
// 现有代码...
}
```
### 实施步骤
1. 修改`gfw/manager.go`添加Base64支持
2. 编译并测试
3. 验证规则加载是否正常
这个修改将使服务器能够同时支持纯文本和Base64编码的GFWList格式提高了兼容性和灵活性。

View File

@@ -0,0 +1,196 @@
# 平均响应时间计算错误修复方案
## 问题分析
通过对代码的分析,我发现平均响应时间计算错误的根本原因是:
1. **统计数据持久化问题**:当服务器重启时,`loadStatsData` 函数直接覆盖 `s.stats` 对象,导致 `TotalResponseTime``Queries` 之间的关系可能被破坏
2. **异常响应时间累计**:在某些情况下,`responseTime` 可能被错误计算为非常大的值,导致 `TotalResponseTime` 异常增长
3. **计算逻辑不健壮**:平均响应时间计算没有考虑异常情况,如 `Queries` 为 0 或 `TotalResponseTime` 溢出
4. **统计数据一致性问题**:并发访问时可能导致统计数据不一致
## 修复方案
### 1. 修复 `loadStatsData` 函数
修改统计数据加载逻辑,确保 `TotalResponseTime``Queries` 之间的关系正确:
```go
// 恢复统计数据
s.statsMutex.Lock()
if statsData.Stats != nil {
// 只恢复有效数据,避免破坏统计关系
s.stats.Queries += statsData.Stats.Queries
s.stats.Blocked += statsData.Stats.Blocked
s.stats.Allowed += statsData.Stats.Allowed
s.stats.Errors += statsData.Stats.Errors
s.stats.TotalResponseTime += statsData.Stats.TotalResponseTime
s.stats.DNSSECQueries += statsData.Stats.DNSSECQueries
s.stats.DNSSECSuccess += statsData.Stats.DNSSECSuccess
s.stats.DNSSECFailed += statsData.Stats.DNSSECFailed
// 重新计算平均响应时间,确保一致性
if s.stats.Queries > 0 {
s.stats.AvgResponseTime = float64(s.stats.TotalResponseTime) / float64(s.stats.Queries)
}
// 合并查询类型统计
for k, v := range statsData.Stats.QueryTypes {
s.stats.QueryTypes[k] += v
}
// 合并来源IP统计
for ip := range statsData.Stats.SourceIPs {
s.stats.SourceIPs[ip] = true
}
// 确保使用当前配置中的EnableDNSSEC值
s.stats.DNSSECEnabled = s.config.EnableDNSSEC
}
s.statsMutex.Unlock()
```
### 2. 修复响应时间计算逻辑
`handleDNSRequest` 函数中,添加响应时间合理性检查:
```go
// 使用上游服务器的实际响应时间(转换为毫秒)
responseTime := int64(rtt.Milliseconds())
// 如果rtt为0查询失败则使用本地计算的时间
if responseTime == 0 {
responseTime = time.Since(startTime).Milliseconds()
}
// 添加合理性检查,避免异常大的响应时间影响统计
if responseTime > 60000 { // 超过60秒的响应时间视为异常
responseTime = 60000
}
```
### 3. 优化平均响应时间计算
修改 `updateStats` 函数,确保平均响应时间计算的健壮性:
```go
s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime
// 添加防御性编程确保Queries大于0
if stats.Queries > 0 {
// 使用更精确的计算方式,避免浮点数精度问题
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
// 限制平均响应时间的范围,避免显示异常大的值
if stats.AvgResponseTime > 60000 {
stats.AvgResponseTime = 60000
}
}
})
```
### 4. 添加统计数据重置机制
添加定期重置统计数据的功能,避免 `TotalResponseTime` 无限增长:
```go
// Start 启动DNS服务器
func (s *Server) Start() error {
// 现有代码...
// 启动统计数据定期重置功能每24小时
go func() {
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.resetStats()
case <-s.ctx.Done():
return
}
}
}()
// 现有代码...
}
// resetStats 重置统计数据
func (s *Server) resetStats() {
s.statsMutex.Lock()
defer s.statsMutex.Unlock()
// 只重置累计值,保留配置相关值
s.stats.TotalResponseTime = 0
s.stats.AvgResponseTime = 0
s.stats.Queries = 0
s.stats.Blocked = 0
s.stats.Allowed = 0
s.stats.Errors = 0
s.stats.DNSSECQueries = 0
s.stats.DNSSECSuccess = 0
s.stats.DNSSECFailed = 0
s.stats.QueryTypes = make(map[string]int64)
s.stats.SourceIPs = make(map[string]bool)
logger.Info("统计数据已重置")
}
```
### 5. 修复 GetStats 函数
确保 `GetStats` 函数正确复制所有统计数据:
```go// GetStats 获取DNS服务器统计信息
func (s *Server) GetStats() *Stats {
s.statsMutex.Lock()
defer s.statsMutex.Unlock()
// 创建完整的统计数据副本
statsCopy := &Stats{
Queries: s.stats.Queries,
Blocked: s.stats.Blocked,
Allowed: s.stats.Allowed,
Errors: s.stats.Errors,
LastQuery: s.stats.LastQuery,
AvgResponseTime: s.stats.AvgResponseTime,
TotalResponseTime: s.stats.TotalResponseTime,
QueryTypes: make(map[string]int64),
SourceIPs: make(map[string]bool),
CpuUsage: s.stats.CpuUsage,
DNSSECQueries: s.stats.DNSSECQueries,
DNSSECSuccess: s.stats.DNSSECSuccess,
DNSSECFailed: s.stats.DNSSECFailed,
DNSSECEnabled: s.stats.DNSSECEnabled,
}
// 复制查询类型统计
for k, v := range s.stats.QueryTypes {
statsCopy.QueryTypes[k] = v
}
// 复制来源IP统计
for ip := range s.stats.SourceIPs {
statsCopy.SourceIPs[ip] = true
}
return statsCopy
}
```
## 修复效果
1. **数据一致性**:修复后,`TotalResponseTime` 和 `Queries` 之间的关系将保持正确,避免因服务器重启导致的统计数据不一致
2. **异常值处理**:添加响应时间合理性检查,避免异常大的响应时间影响平均响应时间计算
3. **计算健壮性**:优化平均响应时间计算逻辑,添加防御性编程,确保计算结果合理
4. **统计数据管理**:添加定期重置统计数据的功能,避免 `TotalResponseTime` 无限增长导致的溢出问题
5. **并发安全**:确保所有统计数据操作都是线程安全的,避免并发访问导致的数据不一致
## 实现步骤
1. 修改 `loadStatsData` 函数,修复统计数据加载逻辑
2. 修改 `handleDNSRequest` 函数,添加响应时间合理性检查
3. 修改 `updateStats` 函数,优化平均响应时间计算
4. 添加 `resetStats` 函数,实现统计数据重置功能
5. 修改 `Start` 函数,启动定期重置统计数据的协程
6. 修复 `GetStats` 函数,确保正确复制所有统计数据
7. 测试修复效果,验证平均响应时间计算是否正确

View File

@@ -0,0 +1,46 @@
## GFWList管理页面实现计划
### 1. 修改 `index.html`
- 在侧边栏菜单(两个位置:桌面端和移动端)添加新的菜单项"GFWList管理"
- 创建新的页面内容区域 `#gfwlist-content`
- 添加配置选项:
- GFWList总开关checkbox
- GFWList解析目标IP输入框
- 通行网站开关组谷歌、YouTube、Facebook、X各checkbox
- 添加保存和重启服务按钮
### 2. 修改 `main.js`
- 在页面标题映射中添加 `'gfwlist': 'GFWList管理'`
-`contentSections` 数组中添加 `gfwlist-content`
- 添加hash为'gfwlist'时的页面初始化逻辑
### 3. 修改 `config.js`
- 添加GFWList页面初始化函数 `initGFWListPage()`
- 添加GFWList配置加载函数 `loadGFWListConfig()`
- 添加GFWList配置保存函数 `saveGFWListConfig()`
- 添加GFWList配置收集函数 `collectGFWListFormData()`
- 更新 `collectFormData()` 以包含新的GFWList配置字段
- 更新 `populateConfigForm()` 移除原有的GFWList配置已迁移到独立页面
### 4. 修改 `api.js`
- 添加GFWList专用的API方法如需要
### 配置数据结构
```json
{
"gfwlist": {
"enabled": true,
"targetIP": "127.0.0.1",
"allowGoogle": true,
"allowYouTube": true,
"allowFacebook": true,
"allowTwitter": true
}
}
```
### 实现顺序
1. 先修改HTML添加页面结构和菜单
2. 修改main.js添加导航支持
3. 修改config.js添加前端逻辑
4. 测试验证

View File

@@ -0,0 +1,86 @@
# 优化DNS服务器parallel模式响应时间
## 问题分析
经过对代码的分析我发现parallel模式下响应时间过高的主要原因包括
1. **缺少超时机制**:当前实现会等待所有上游服务器响应,单个慢服务器会拖慢整个查询
2. **响应时间计算不合理**:使用所有响应的平均时间,而不是最快响应时间
3. **合并响应开销大**需要合并所有响应增加CPU和内存开销
4. **等待所有响应**:没有实现快速返回机制,即使收到第一个有效响应也会等待所有响应
5. **DNSSEC验证开销**每个响应都需要进行DNSSEC验证增加额外开销
## 优化方案
### 1. 添加超时机制
- 为每个上游服务器请求添加超时设置
- 超时时间可配置建议默认500ms
- 超时的请求不会影响整体响应时间
### 2. 实现快速返回机制
- 当收到第一个有效响应成功或NXDOMAIN立即返回给客户端
- 继续处理其他响应用于合并和缓存,但不影响当前查询的响应时间
- 优先返回带DNSSEC的响应
### 3. 优化响应时间计算
- 使用最快的响应时间作为查询的响应时间
- 保留平均响应时间用于统计,但不影响客户端感知的响应时间
### 4. 优化响应合并逻辑
- 只合并成功响应,忽略错误响应
- 合并时优先保留TTL较长的记录
- 减少不必要的内存分配和拷贝
### 5. 优化DNSSEC验证
- 只对需要返回的响应进行DNSSEC验证
- 缓存DNSSEC验证结果减少重复验证
### 6. 增加服务器健康检查
- 定期检查上游服务器的响应时间和可用性
- 只向健康的服务器发送请求
- 根据历史响应时间动态调整服务器权重
## 实现步骤
1. **修改forwardDNSRequestWithCache函数**
- 添加超时设置
- 实现快速返回逻辑
- 优化响应时间计算
2. **修改mergeResponses函数**
- 优化合并逻辑,减少开销
- 优先保留TTL较长的记录
3. **修改DNSSEC验证逻辑**
- 只对需要返回的响应进行验证
- 添加DNSSEC验证结果缓存
4. **添加服务器健康检查机制**
- 定期检查上游服务器
- 动态调整服务器列表
5. **添加配置选项**
- 超时时间配置
- 快速返回开关
- 健康检查配置
## 预期效果
- **响应时间显著降低**:客户端感知的响应时间将接近最快的上游服务器响应时间
- **资源利用率提高**:减少不必要的等待和计算
- **鲁棒性增强**:单个慢服务器不会影响整体性能
- **用户体验改善**更快的DNS解析速度
## 文件修改
- `/root/dns/dns/server.go`主要修改文件包含parallel模式的实现逻辑
- `/root/dns/config/config.go`:添加新的配置选项
- `/root/dns/dns/cache.go`如果需要添加DNSSEC验证结果缓存
## 测试计划
1. **性能测试**:比较优化前后的响应时间
2. **压力测试**:在高并发情况下测试性能
3. **可靠性测试**:测试单个服务器故障时的表现
4. **DNSSEC测试**确保DNSSEC验证仍然正常工作
5. **不同配置测试**:测试不同超时时间和服务器数量的影响

View File

@@ -0,0 +1,182 @@
## 问题分析
通过深入分析代码,我找到了导致所有查询都显示同一个 NXDOMAIN 错误的根本原因:
**核心问题**`mergeResponses` 函数在合并多个 DNS 响应时,**没有正确处理 Rcode 字段**
**具体原因**
1. 当使用并行查询模式时DNS 服务器会向多个上游服务器发送请求
2. 函数使用第一个响应作为基础来合并其他响应
3. 它清空了 `Answer``Ns``Extra` 字段,但**保留了第一个响应的 Rcode**
4. 如果第一个响应返回 NXDOMAIN比如对于恶意域名 www.evilsnssdk.com那么**合并后的响应也会保持 NXDOMAIN 状态**,即使其他响应返回成功
5. 这导致所有查询都显示同一个 NXDOMAIN 错误
## 修复方案
### 修复 `mergeResponses` 函数
**关键修改点**
1. **重置 Rcode**:在合并响应前,将 Rcode 重置为成功状态
2. **处理 NXDOMAIN**:只有当所有响应都是 NXDOMAIN 时,才返回 NXDOMAIN
3. **优先使用成功响应**:如果有任何响应返回成功,就使用成功的 Rcode
### 修复步骤
1. **修改 `mergeResponses` 函数** (`/root/dns/dns/server.go:842-933`)
- 在合并记录前,将 `mergedResponse.Rcode` 设置为 `dns.RcodeSuccess`
- 添加变量 `allNXDOMAIN` 来跟踪是否所有响应都是 NXDOMAIN
- 遍历所有响应,检查是否有成功响应
- 如果所有响应都是 NXDOMAIN才将 `mergedResponse.Rcode` 设置为 `dns.RcodeNameError`
2. **优化合并逻辑**
- 确保优先使用成功响应中的记录
- 避免将 NXDOMAIN 响应的记录合并到成功响应中
- 保持响应的一致性Rcode 与记录内容匹配
## 修复代码
```go
// mergeResponses 合并多个DNS响应
func mergeResponses(responses []*dns.Msg) *dns.Msg {
if len(responses) == 0 {
return nil
}
// 如果只有一个响应,直接返回,避免不必要的合并操作
if len(responses) == 1 {
return responses[0].Copy()
}
// 使用第一个响应作为基础
mergedResponse := responses[0].Copy()
mergedResponse.Answer = []dns.RR{}
mergedResponse.Ns = []dns.RR{}
mergedResponse.Extra = []dns.RR{}
// 重置Rcode为成功除非所有响应都是NXDOMAIN
mergedResponse.Rcode = dns.RcodeSuccess
// 检查是否所有响应都是NXDOMAIN
allNXDOMAIN := true
// 收集所有成功响应的记录
for _, resp := range responses {
if resp == nil {
continue
}
// 如果有任何响应是成功的就不是allNXDOMAIN
if resp.Rcode == dns.RcodeSuccess {
allNXDOMAIN = false
}
}
// 如果所有响应都是NXDOMAIN设置合并响应为NXDOMAIN
if allNXDOMAIN {
mergedResponse.Rcode = dns.RcodeNameError
}
// 使用map存储唯一记录选择最长TTL
// 预分配map容量减少扩容开销
answerMap := make(map[recordKey]dns.RR, len(responses[0].Answer)*len(responses))
nsMap := make(map[recordKey]dns.RR, len(responses[0].Ns)*len(responses))
extraMap := make(map[recordKey]dns.RR, len(responses[0].Extra)*len(responses))
for _, resp := range responses {
if resp == nil {
continue
}
// 只合并与最终Rcode匹配的响应记录
if (mergedResponse.Rcode == dns.RcodeSuccess && resp.Rcode == dns.RcodeSuccess) ||
(mergedResponse.Rcode == dns.RcodeNameError && resp.Rcode == dns.RcodeNameError) {
// 合并Answer部分
for _, rr := range resp.Answer {
key := getRecordKey(rr)
if existing, exists := answerMap[key]; exists {
// 如果存在相同记录选择TTL更长的
if rr.Header().Ttl > existing.Header().Ttl {
answerMap[key] = rr
}
} else {
answerMap[key] = rr
}
}
// 合并Ns部分
for _, rr := range resp.Ns {
key := getRecordKey(rr)
if existing, exists := nsMap[key]; exists {
// 如果存在相同记录选择TTL更长的
if rr.Header().Ttl > existing.Header().Ttl {
nsMap[key] = rr
}
} else {
nsMap[key] = rr
}
}
// 合并Extra部分
for _, rr := range resp.Extra {
// 跳过OPT记录避免重复
if rr.Header().Rrtype == dns.TypeOPT {
continue
}
key := getRecordKey(rr)
if existing, exists := extraMap[key]; exists {
// 如果存在相同记录选择TTL更长的
if rr.Header().Ttl > existing.Header().Ttl {
extraMap[key] = rr
}
} else {
extraMap[key] = rr
}
}
}
}
// 预分配切片容量,减少扩容开销
mergedResponse.Answer = make([]dns.RR, 0, len(answerMap))
mergedResponse.Ns = make([]dns.RR, 0, len(nsMap))
mergedResponse.Extra = make([]dns.RR, 0, len(extraMap))
// 将map转换回切片
for _, rr := range answerMap {
mergedResponse.Answer = append(mergedResponse.Answer, rr)
}
for _, rr := range nsMap {
mergedResponse.Ns = append(mergedResponse.Ns, rr)
}
for _, rr := range extraMap {
mergedResponse.Extra = append(mergedResponse.Extra, rr)
}
return mergedResponse
}```
## 预期效果
修复后DNS服务器将能够
- 正确合并多个 DNS 响应
- 确保 Rcode 与实际记录内容匹配
- 只有当所有响应都是 NXDOMAIN 时才返回 NXDOMAIN
- 避免单个恶意域名影响所有查询结果
- 正确显示各个域名的查询结果
## 修复代码位置
- **核心修改文件**`/root/dns/dns/server.go`
- **关键函数**`mergeResponses`
- **修改内容**:修复响应合并逻辑,正确处理 Rcode 字段
## 测试方法
1. 使用 nslookup 测试不同域名
2. 检查是否每个域名都显示正确的查询结果
3. 验证 www.evilsnssdk.com 返回 NXDOMAIN而其他域名返回成功
4. 检查日志中是否还有大量错误信息
这个修复将彻底解决所有查询都显示同一个 NXDOMAIN 错误的问题!

View File

@@ -0,0 +1,58 @@
## 实现计划
### 1. 配置文件修改
* **修改`/root/dns/config/config.go`**
*`DNSConfig`结构体中添加`PrefixDomain []string`字段用于支持search domain功能
*`LoadConfig`函数中添加`prefixDomain`的默认值处理
### 2. DNS请求处理逻辑修改
* **修改`/root/dns/dns/server.go`中的`forwardDNSRequestWithCache`函数**
* 强化domainSpecificDNS逻辑确保当域名匹配时只使用指定的DNS服务器
* 移除向DNSSEC专用服务器发送请求的逻辑当域名匹配domainSpecificDNS时
* 确保匹配域名的DNS查询结果不会被其他DNS服务器的响应覆盖
* **修改`/root/dns/dns/server.go`中的`handleDNSRequest`函数**
* 实现search domain功能当直接查询失败时尝试添加prefixDomain中指定的域名前缀
* 按照/etc/resolv.conf中的search domain逻辑处理查询请求
### 3. 配置文件示例更新
* **更新配置文件示例**
* 添加`prefixDomain`配置项示例
* 说明search domain功能的使用方法
### 4. 测试验证
* 测试domainSpecificDNS强制使用功能确保匹配的域名只使用指定的DNS服务器
* 测试search domain功能确保能够正确处理带前缀的域名查询
* 测试不同配置组合下的功能正确性
## 预期效果
1. 当域名匹配domainSpecificDNS配置时无论DNSSEC是否启用只使用指定的DNS服务器
2. 支持search domain功能能够自动尝试添加配置的域名前缀
3. 配置简单直观,与/etc/resolv.conf的search domain行兼容
## 实现要点
* 确保domainSpecificDNS配置的优先级最高
* 实现高效的search domain查询逻辑避免不必要的网络请求
* 保持代码的可读性和可维护性
* 确保与现有功能的兼容性

View File

@@ -0,0 +1,20 @@
# 修复picsum.photos域名匹配问题
## 问题分析
当DNS查询picsum.photos时实际传递给`getDomainInfo()`函数的域名可能带有末尾点(如"picsum.photos."而domain-info.json中存储的是没有末尾点的域名如"picsum.photos")。在原始的`isDomainMatch()`函数中,直接比较两个域名,导致匹配失败。
## 解决方案
修改`isDomainMatch()`函数,在域名比较前对两个域名进行规范化处理,去除末尾的点,确保匹配准确性。
## 实施步骤
1. 修改`isDomainMatch()`函数,在域名比较前对`urlDomain``targetDomain`进行规范化处理,去除末尾的点
2. 确保无论是完整URL还是纯域名都能正确匹配
3. 测试修改后的代码确保picsum.photos域名能被正确匹配
## 修改内容
- 文件:`/root/dns/static/js/logs.js`
- 函数:`isDomainMatch()`
- 修改点:在域名比较前,去除`urlDomain``targetDomain`末尾的点
## 预期效果
修改后picsum.photos域名无论是否带有末尾点都能被正确匹配到domain-info.json中的条目。

View File

@@ -0,0 +1,56 @@
## 问题分析
域名信息显示为空,特别是对于`wx.qq.com`这样的域名,虽然在`domain-info.json`中已配置,但日志详情中显示为空。
### 可能的原因
1. **文件路径问题**`loadDomainInfoDatabase`函数使用`fetch('/domain-info/domains/domain-info.json')`加载文件,但服务器可能没有正确配置该路径,导致加载失败。
2. **代码逻辑问题**:虽然代码逻辑看起来正确,但可能存在一些边缘情况没有处理好。
3. **调试信息不足**:代码中缺少调试信息,难以定位具体问题。
## 解决方案
### 1. 添加调试日志
`loadDomainInfoDatabase``getDomainInfo`函数中添加调试日志,以便定位问题:
-`loadDomainInfoDatabase`函数中,添加日志记录文件加载状态和结果
-`getDomainInfo`函数中,添加日志记录域名匹配过程
-`isDomainMatch`函数中添加日志记录URL匹配细节
### 2. 检查并修复文件路径
确保`domain-info.json`文件能被正确访问:
- 检查服务器配置,确保`/domain-info/domains/domain-info.json`路径指向正确的文件
- 或者修改代码中的路径,使用绝对路径或正确的相对路径
### 3. 增强错误处理
`loadDomainInfoDatabase`函数中增强错误处理,提供更详细的错误信息:
- 记录完整的错误信息
- 在控制台显示友好的错误提示
- 考虑添加重试机制
### 4. 优化域名匹配逻辑
虽然代码逻辑看起来正确,但可以进一步优化:
- 确保所有URL都被正确处理无论是否包含协议
- 增强主域名提取逻辑,处理更多特殊情况
- 考虑添加缓存机制,提高匹配效率
## 实施步骤
1. 修改`loadDomainInfoDatabase`函数,添加调试日志和增强错误处理
2. 修改`getDomainInfo`函数,添加调试日志
3. 修改`isDomainMatch`函数,添加调试日志
4. 测试修复效果
5. 根据测试结果进一步优化
## 预期效果
修复后,域名信息将能正确显示在日志详情中,特别是对于`wx.qq.com`等已配置的域名。

View File

@@ -0,0 +1,44 @@
1. **修复规则添加逻辑**
* 修改`blockDomain`函数,使用正确的规则格式(如 `||domain^`
* 修改`unblockDomain`函数,使用正确的放行规则格式(如 `@@||domain^`
* 确保规则经过预处理后再发送到API
2. **更新本地规则列表**
* 添加规则成功后,更新`rules``filteredRules`数组
* 调用`renderRulesList`函数重新渲染规则列表
* 更新规则数量统计
3. **确保规则同步**
* 验证规则是否正确添加到本地规则列表
* 确保日志页面和规则页面的数据一致性
4. **优化用户体验**
* 改进操作反馈,显示更详细的成功/失败信息
* 确保规则添加后立即在日志中反映状态变化
5. **测试功能**
* 测试拦截域名功能
* 测试放行域名功能
* 验证规则是否正确添加到本地规则列表
* 验证日志状态是否正确更新
**修改文件**
* `/root/dns/static/js/logs.js`:修复`blockDomain``unblockDomain`函数
* 确保与`/root/dns/static/js/modules/rules.js`的规则处理逻辑保持一致

View File

@@ -0,0 +1,56 @@
1. **确认apiRequest函数的使用**
* 检查logs.js中apiRequest函数的来源和调用方式
* 确保使用正确的API\_BASE\_URL和异步apiRequest函数
2. **修复API调用路径**
* 确保拦截和放行功能使用正确的API路径
* 验证与规则管理模块的API调用一致性
3. **改进规则格式**
* 确保拦截和放行规则使用正确的格式
* 与规则管理模块的规则处理逻辑保持一致
4. **添加详细的错误处理**
* 在apiRequest调用中添加详细的日志
* 改进错误处理,显示更详细的错误信息
* 添加调试信息,以便跟踪问题
5. **确保跨文件函数调用正确**
* 确保logs.js能正确访问apiRequest函数
* 验证全局变量和函数的可用性
6. **测试功能**
* 测试拦截域名功能
* 测试放行域名功能
* 验证规则是否正确添加到本地规则列表
* 验证日志状态是否正确更新
**修改文件**
* `/root/dns/static/js/logs.js`修复apiRequest函数调用和API路径
**预期效果**
* 拦截功能正常工作,规则正确添加到本地规则列表
* 放行功能正常工作,规则正确添加到本地规则列表
* 日志状态立即更新,显示正确的拦截/放行状态
* 显示详细的操作反馈和错误信息

View File

@@ -0,0 +1,67 @@
## 问题分析
从截图中可以看到,日志表格显示了"Invalid Date"、"undefined"等错误值这表明前端代码在解析API返回的JSON数据时出现了问题。
## 根本原因
1. 后端`QueryLog`结构体的JSON标签使用了小写字段名`json:"timestamp"``json:"clientIP"`等)
2. 前端代码使用了大写的字段名(如`log.Timestamp``log.ClientIP`等)来访问数据
3. 字段名大小写不匹配导致前端无法正确解析API返回的数据
## 修复方案
修改前端代码使用正确的小写字段名来访问API返回的数据与后端返回的JSON格式匹配。
## 具体修改点
1. **`updateLogsTable`函数**约435-621行
*`log.Timestamp`改为`log.timestamp`
*`log.ClientIP`改为`log.clientIP`
*`log.Domain`改为`log.domain`
*`log.QueryType`改为`log.queryType`
*`log.ResponseTime`改为`log.responseTime`
*`log.Result`改为`log.result`
*`log.BlockRule`改为`log.blockRule`
*`log.FromCache`改为`log.fromCache`
*`log.DNSSEC`改为`log.dnssec`
*`log.EDNS`改为`log.edns`
*`log.DNSServer`改为`log.dnsServer`
*`log.DNSSECServer`改为`log.dnssecServer`
*`log.ResponseCode`改为`log.responseCode`
2. **`showLogDetailModal`函数**约1450-1597行
* 同样修改所有字段名的大小写
3. **其他可能的访问点**
* 检查并修改任何其他访问日志数据的地方
## 修复原则
* 保持前端代码与后端API返回的JSON格式一致
* 遵循REST API的最佳实践使用小写字段名
* 确保所有日志数据访问点都得到修复
## 验证方法
1. 修复代码后,重新编译并运行服务器
2. 访问日志页面,检查日志数据是否正确显示
3. 测试不同类型的日志(允许、屏蔽、错误),确保都能正确显示
4. 测试日志详情模态框,确保所有字段都能正确显示

View File

@@ -0,0 +1,21 @@
1. **问题分析**
- 当添加自定义规则时,规则被添加到内存中并保存到文件
- 但由于DNS缓存的存在如果该域名的DNS响应已经被缓存那么在缓存过期之前DNS服务器会直接返回缓存的响应而不会重新检查规则
- 这就导致了添加规则后需要重启服务器才能生效的问题
2. **修复方案**
- 当添加或删除自定义规则时清空DNS缓存这样新的规则会立即生效
- 这样当客户端再次请求该域名时DNS服务器会重新检查规则而不是直接返回缓存的响应
3. **修复步骤**
- 修改HTTP API处理函数在添加或删除规则后清空DNS缓存
- 或者修改ShieldManager的AddRule和RemoveRule方法添加清空DNS缓存的逻辑
- 测试修复后的功能,确保添加规则后无需重启服务器即可生效
4. **预期结果**
- 添加自定义规则后,规则会立即生效,无需重启服务器
- 重启服务器后,之前添加的自定义规则仍然存在
5. **具体实现**
-`/root/dns/http/server.go`在添加或删除规则的API处理函数后调用DNS缓存的Clear方法
- 这样当添加或删除规则时DNS缓存会被清空新的规则会立即生效

View File

@@ -0,0 +1,79 @@
## DNS服务器性能优化方案
### 问题分析
1. **并行查询模式**:当前配置使用`parallel`模式,会等待所有上游服务器响应后才返回,受最慢服务器影响
2. **DNSSEC验证开销**启用了DNSSEC验证增加了额外的计算和网络请求
3. **过多上游服务器**DNSSEC上游服务器多达5个响应时间差异大
4. **调试级别日志**`debug`级别日志记录大量信息占用CPU和I/O资源
5. **缓存TTL过短**10秒的缓存TTL导致频繁向上游请求
6. **黑名单规则过多**14个启用的黑名单每次请求都需要检查
### 优化方案
#### 1. 修改查询模式为快速返回
*`queryMode``parallel`改为`fastest-ip`或优化默认模式
* 快速返回模式会返回第一个有效响应,而不是等待所有响应
#### 2. 优化DNSSEC配置
* 减少DNSSEC上游服务器数量只保留2-3个可靠的
* 对国内域名禁用DNSSEC验证已配置部分可扩展
#### 3. 调整缓存策略
* 增加`cacheTTL`到60秒或更高减少上游请求频率
* 优化缓存实现,减少锁竞争
#### 4. 降低日志级别
* 将日志级别从`debug`改为`info``warn`,减少日志写入开销
#### 5. 优化黑名单处理
* 合并重复的黑名单规则
* 考虑使用更高效的域名匹配算法
#### 6. 代码优化
* 减少DNSSEC验证的重复调用
* 优化响应合并逻辑,避免不必要的计算
* 调整超时设置,避免过长等待
### 具体修改点
1. **config.json**
* 修改`queryMode``fastest-ip`
* 减少`dnssecUpstreamDNS`数量
* 增加`cacheTTL`到60
* 将日志级别改为`info`
2. **dns/server.go**
* 优化`forwardDNSRequestWithCache`函数减少DNSSEC重复验证
* 优化响应合并逻辑,避免不必要的计算
* 调整并行模式的超时处理
### 预期效果
* 减少响应时间,从当前的秒级降低到毫秒级
* 减少CPU和I/O资源占用
* 提高并发处理能力
* 保持DNS解析的准确性和可靠性

View File

@@ -0,0 +1,47 @@
# 内存泄漏问题
## 1. IP地理位置缓存泄漏
问题IP地理位置缓存```ipGeolocationCache```)缺少全局过期清理机制,仅在获取新数据时检查单条缓存过期,导致长期运行后内存无限增长。
修复建议:
```Go
// 在Server结构体中添加清理定时器ipGeolocationCacheCleanupTicker *time.Ticker// 在NewServer函数中启动清理协程go s.startIPGeolocationCacheCleanup()// 添加清理函数func (s *Server) startIPGeolocationCacheCleanup() { ticker := time.NewTicker(time. Hour) // 每小时清理一次 defer ticker.Stop() for { select { case <-ticker.C: s. cleanupExpiredIPGeolocat ionCache() case <-s.ctx.Done(): return } }}func (s *Server) cleanupExpiredIPGeolocationCache() { now := time.Now() s.ipGeolocationCacheMutex.Lock() defer s.ipGeolocationCacheMutex. Unlock() for ip, geo := range s. ipGeolocationCache { if now.After(geo.Expiry) { delete(s. ipGeolocationCache, ip) } }}
```
# 2. DNS缓存无大小限制
问题DNS缓存DNSCache仅按TTL过期无最大条目数限制可能导致内存无限增长。
修复建议:
```Go
// 在DNSCache结构体中添加最大大小限制maxSize int// 在Set方法中添加大小限制检查func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.Duration) { // 现有代码... c.mutex.Lock() defer c.mutex.Unlock() c.cache[key] = item // 检查并清理超过最大大小的缓存 if c.maxSize > 0 && len(c. cache) > c.maxSize { c.evictOldest() // 实现LRU或 随机淘汰策略 }}
```
## 1. 域名检查函数性能优化
问题:```CheckDomainBlockDetails```函数中重复的子域名检查和大量正则表达式匹配导致CPU使用率过高。
修复建议:
```Go
// 合并本地和远程规则检查减少循环次数func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interface{} { // 预处理域名 // ... // 合并域名排除规则检查 if m.domainExceptions[domain] { result["excluded"] = true result["excludeRule"] = m. domainExceptionsOriginal [domain] result["excludeRuleType"] = "exact_domain" result["blocksource"] = m. domainExceptionsSource [domain] return result } // 合并子域名检查 parts := strings.Split(domain, ".") for i := 0; i < len(parts)-1; i ++ { subdomain := strings.Join (parts[i:], ".") if m.domainExceptions [subdomain] { result["excluded"] = true result["excludeRule"] = m. domainExceptionsOriginal [subdomain] result ["excludeRuleType"] = "subdomain" result["blocksource"] = m.domainExceptionsSource [subdomain] return result } } // 合并正则表达式检查 for _, re := range m. regexExceptions { if re.pattern.MatchString (domain) { result["excluded"] = true result["excludeRule"] = re.original result ["excludeRuleType"] = "regex" result["blocksource"] = re.source return result } } // 阻止规则检查采用相同优化方式 // ...}
```
## 2. 减少锁持有时间
问题:```CheckDomainBlockDetails```函数持有读锁时间过长,影响并发性能。
修复建议:
```Go
// 采用更细粒度的锁或读写分离策略func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interface{} { // 只在需要访问共享数据时加锁 m.rulesMutex.RLock() domainExists := m. domainExceptions[domain] m.rulesMutex.RUnlock() if domainExists { m.rulesMutex.RLock() // 获取详细信息 m.rulesMutex.RUnlock() return result } // 其他检查采用相同方式}
```
# 其他优化建议
## 1. DNS缓存优化
实现LRU缓存淘汰策略限制最大缓存大小
定期统计缓存命中率,调整缓存策略
## 2. WebSocket连接管理
实现连接池管理,限制最大连接数
添加连接心跳检测机制,及时清理无效连接
## 3. 并行查询优化
限制同时发起的上游查询数量,避免资源耗尽
实现超时控制,防止长时间阻塞
## 4. 正则表达式优化
预编译所有正则表达式规则
合并相似规则,减少正则表达式数量
使用更高效的规则匹配算法
# 总结
通过实施上述修复建议和优化方案可以有效解决DNS服务器的内存泄漏问题和CPU性能瓶颈提高系统稳定性和响应速度。建议优先修复IP地理位置缓存泄漏和域名检查函数性能问题这些是当前最严重的问题。

View File

@@ -0,0 +1,69 @@
## 实现计划
### 1. 分析当前代码结构
* **HTML结构**:日志表格在`updateLogsTable`函数中动态生成
* **数据处理**:每条日志包含`Result`字段表示查询结果allowed, blocked, error
* **API需求**:需要拦截/放行域名的API接口
### 2. 修改内容
#### 2.1 修改`updateLogsTable`函数
* 在表格中添加操作列
* 根据`log.Result`字段决定显示哪个按钮:
- 如果`log.Result``blocked`,显示"放行"按钮
- 否则,显示"拦截"按钮
* 为按钮添加点击事件处理函数
#### 2.2 实现拦截/放行功能
* 添加`blockDomain`函数调用API拦截域名
* 添加`allowDomain`函数调用API放行域名
* 实现按钮点击事件的处理逻辑
* 添加操作成功后的反馈机制
#### 2.3 更新表格头部
* 在表格头部添加"操作"列
* 确保表格的`colspan`属性正确更新
### 3. 实现细节
* **按钮样式**:使用现有的样式类,保持界面一致性
* **事件绑定**:为动态生成的按钮绑定点击事件
* **API调用**:使用现有的`apiRequest`函数进行API调用
* **反馈机制**:操作成功后显示提示信息
* **数据刷新**:操作完成后刷新日志列表,显示最新状态
### 4. 预期效果
* 日志详情表格右侧增加"操作"列
* 对于被拦截的域名,显示"放行"按钮
* 对于未被拦截的域名,显示"拦截"按钮
* 点击按钮后,成功执行相应操作并刷新列表
* 操作过程中显示反馈信息
### 5. 实现步骤
1. 修改`updateLogsTable`函数,添加操作列
2. 更新表格头部,添加"操作"列
3. 实现`blockDomain``allowDomain`函数
4. 为按钮绑定点击事件
5. 添加操作反馈机制
6. 测试功能完整性
## 关键代码修改点
* **`updateLogsTable`函数**:在生成表格行时添加操作列
* **表格头部**:添加"操作"列标题
* **新增函数**`blockDomain``allowDomain`
* **事件绑定**:为动态生成的按钮添加点击事件处理
## 技术要点
* 动态生成元素的事件绑定
* API调用的错误处理
* 操作反馈机制的实现
* 数据刷新逻辑
* 保持界面样式一致性

View File

@@ -1,37 +1,58 @@
## 实现计划
### 1. 配置文件修改
- **修改`/root/dns/config/config.go`**
-`DNSConfig`结构体中添加`PrefixDomain []string`字段用于支持search domain功能
-`LoadConfig`函数中添加`prefixDomain`的默认值处理
* **修改`/root/dns/config/config.go`**
*`DNSConfig`结构体中添加`PrefixDomain []string`字段用于支持search domain功能
*`LoadConfig`函数中添加`prefixDomain`的默认值处理
### 2. DNS请求处理逻辑修改
- **修改`/root/dns/dns/server.go`中的`forwardDNSRequestWithCache`函数**
- 强化domainSpecificDNS逻辑确保当域名匹配时只使用指定的DNS服务器
- 移除向DNSSEC专用服务器发送请求的逻辑当域名匹配domainSpecificDNS时
- 确保匹配域名的DNS查询结果不会被其他DNS服务器的响应覆盖
- **修改`/root/dns/dns/server.go`中的`handleDNSRequest`函数**
- 实现search domain功能当直接查询失败时尝试添加prefixDomain中指定的域名前缀
- 按照/etc/resolv.conf中的search domain逻辑处理查询请求
* **修改`/root/dns/dns/server.go`中的`forwardDNSRequestWithCache`函数**
* 强化domainSpecificDNS逻辑确保当域名匹配时只使用指定的DNS服务器
* 移除向DNSSEC专用服务器发送请求的逻辑当域名匹配domainSpecificDNS时
* 确保匹配域名的DNS查询结果不会被其他DNS服务器的响应覆盖
* **修改`/root/dns/dns/server.go`中的`handleDNSRequest`函数**
* 实现search domain功能当直接查询失败时尝试添加prefixDomain中指定的域名前缀
* 按照/etc/resolv.conf中的search domain逻辑处理查询请求
### 3. 配置文件示例更新
- **更新配置文件示例**
- 添加`prefixDomain`配置项示例
- 说明search domain功能的使用方法
* **更新配置文件示例**
* 添加`prefixDomain`配置项示例
* 说明search domain功能的使用方法
### 4. 测试验证
- 测试domainSpecificDNS强制使用功能确保匹配的域名只使用指定的DNS服务器
- 测试search domain功能确保能够正确处理带前缀的域名查询
- 测试不同配置组合下的功能正确性
* 测试domainSpecificDNS强制使用功能确保匹配的域名只使用指定的DNS服务器
* 测试search domain功能确保能够正确处理带前缀的域名查询
* 测试不同配置组合下的功能正确性
## 预期效果
1. 当域名匹配domainSpecificDNS配置时无论DNSSEC是否启用只使用指定的DNS服务器
2. 支持search domain功能能够自动尝试添加配置的域名前缀
3. 配置简单直观,与/etc/resolv.conf的search domain行兼容
## 实现要点
- 确保domainSpecificDNS配置的优先级最高
- 实现高效的search domain查询逻辑避免不必要的网络请求
- 保持代码的可读性和可维护性
- 确保与现有功能的兼容性
* 确保domainSpecificDNS配置的优先级最高
* 实现高效的search domain查询逻辑避免不必要的网络请求
* 保持代码的可读性和可维护性
* 确保与现有功能的兼容性

View File

@@ -0,0 +1,31 @@
# 实现域名信息显示
## 需求分析
用户需要在日志详情页面的红色框区域显示域名信息,包括:
- 网站名称(带图标)
- 网站类别
- 所属公司
## 代码分析
1. 目前代码已经包含了获取域名信息的功能:
- `loadDomainInfoDatabase` 函数加载域名信息数据库
- `getDomainInfo` 函数根据域名查找对应的网站信息
- `showLogDetailModal` 函数显示日志详情,包括域名信息
2. 域名信息已经在日志详情模态框中显示,但需要确保它在指定的红色框区域正确显示
## 实现计划
1. **优化域名信息显示样式**:调整 `showLogDetailModal` 函数中域名信息的HTML结构和样式确保它在红色框内正确显示
2. **增强域名匹配逻辑**:改进 `getDomainInfo` 函数,提高域名匹配的准确性
3. **添加错误处理**:确保在域名信息加载失败时,页面能够优雅处理
4. **测试功能**:验证域名信息能够正确显示在红色框区域
## 预期效果
- 当日志详情弹窗打开时,在红色框区域显示域名的完整信息
- 显示格式:
```
[图标] 网站名称
类别: [类别名称]
所属公司: [公司名称]
```
- 如果没有匹配的域名信息,显示"无"

View File

@@ -0,0 +1,18 @@
1. **更新getIpGeolocation函数**修改函数逻辑使用API响应中的addr字段来显示完整的地理位置信息
2. **处理编码问题**:确保正确处理中文编码
3. **维护缓存机制**保留现有的24小时缓存机制提高性能
4. **测试不同IP类型**确保公网IP和内网IP都能正确显示
### 具体修改点
- **文件**`/root/dns/static/js/logs.js`
- **函数**`getIpGeolocation`
- **修改内容**
- 从API响应中提取`addr`字段
- 直接使用addr字段作为完整的地理位置信息
- 保持对私有IP的特殊处理
- 维护现有的缓存机制
### 预期效果
- 公网IP显示格式"IP地址 (完整地理位置来自addr字段)"
- 内网IP显示格式"IP地址 (内网 内网)"
- 未知IP显示格式"IP地址 (未知 未知)"

View File

@@ -0,0 +1,81 @@
## 问题分析
服务器出现"Server Failed"问题的主要原因是:
1. **DNS查询超时设置过短**配置文件中timeout设置为5ms导致几乎所有DNS查询都超时失败
2. **parallel模式响应处理逻辑缺陷**在某些情况下即使主DNS服务器有响应也可能因为DNSSEC验证或其他原因被忽略
3. **DNSSEC处理影响正常查询**启用DNSSEC后系统会优先选择带DNSSEC记录的响应但如果所有DNSSEC服务器都失败可能导致没有响应
## 解决方案
### 1. 确保超时设置正确
* **已修复**将配置文件中的timeout从5ms修改为5000ms
* 验证代码中默认值设置正确,确保配置文件加载时能正确应用默认值
### 2. 优化parallel模式响应处理逻辑
**问题**当前parallel模式下只有当响应是成功RcodeSuccess或NXDOMAINRcodeNameError时才会被考虑作为最佳响应且优先选择带DNSSEC记录的响应。
**修复**
* 修改`dns/server.go`中的parallel模式处理逻辑
* 确保用户配置的主DNS服务器upstreamDNS响应优先被使用即使它没有DNSSEC记录
* 只有当主DNS服务器完全失败时才考虑使用DNSSEC专用服务器的响应
* 确保在所有情况下都能返回至少一个备选响应
### 3. 增强错误处理机制
**问题**当所有DNS服务器都返回非成功响应时系统可能无法找到合适的响应返回给客户端。
**修复**
*`dns/server.go`中增强错误处理
* 确保即使所有上游服务器都失败也能返回一个有效的DNS响应
* 优化备选响应的选择逻辑,确保总有一个可用的响应
* 添加更详细的日志记录,便于调试
### 4. 优化DNSSEC处理逻辑
**问题**当前DNSSEC处理逻辑可能导致主DNS服务器的响应被忽略。
**修复**
* 调整DNSSEC专用服务器请求逻辑只有当主DNS服务器完全失败时才使用
* 确保DNSSEC验证失败不会影响正常的DNS查询结果
* 优化DNSSEC记录检查逻辑确保准确判断DNSSEC记录
## 修复步骤
1. **验证超时设置**确保配置文件中的timeout已设置为5000ms
2. **修改parallel模式响应处理**:优化`dns/server.go`中parallel模式的响应选择逻辑
3. **增强备选响应机制**:确保总有一个可用的响应返回给客户端
4. **优化DNSSEC处理**调整DNSSEC专用服务器请求时机和优先级
5. **添加详细日志**:增加调试日志,便于后续问题定位
6. **测试验证**重启服务器并测试DNS查询功能
## 预期效果
* DNS查询成功率显著提高不再出现大量"Server Failed"错误
* 主DNS服务器响应优先被使用确保查询效率
* DNSSEC功能正常工作同时不影响正常DNS查询
* 系统更加稳定,能够处理各种异常情况
## 关键文件修改
* `/root/dns/config.json`确保timeout设置正确
* `/root/dns/dns/server.go`优化parallel模式响应处理、错误处理和DNSSEC逻辑

View File

@@ -0,0 +1,38 @@
1. **问题分析**
* 项目已经有一个CHANGELOG.md文件遵循Keep a Changelog格式
* 最新版本是\[1.1.3]发布于2025-12-19
* 我们需要添加新的版本条目,记录最近的修复
2. **修复内容**
* 修复了规则优先级问题:确保自定义规则优先于远程规则
* 修复了添加自定义规则后需要重启服务器的问题通过在添加或删除规则后清空DNS缓存实现
3. **更新步骤**
* 在CHANGELOG.md文件中添加一个新的版本条目\[1.1.4]日期为2025-12-21
* 在该版本下添加修复的内容
* 确保格式符合CHANGELOG的要求
4. **预期结果**
* CHANGELOG.md文件将包含最新的修复记录
* 版本号将更新为\[1.1.4]
* 修复内容将被清晰地记录在CHANGELOG中
5. **具体实现**
* 在CHANGELOG.md文件的开头添加新的版本条目
* 使用### 修复标题记录修复的内容
* 清晰描述每个修复的问题和解决方案

View File

@@ -0,0 +1,46 @@
1. **修改配置结构体定义**
*`config/config.go`中的`DNSConfig`结构体移除`StatsFile`字段
*`ShieldConfig`结构体移除`LocalRulesFile``HostsFile``StatsFile``RemoteRulesCacheDir`字段
*`LogConfig`结构体移除`File`字段
2. **修改配置加载逻辑**
*`config/config.go``LoadConfig`函数中,移除对上述字段的默认值设置
3. **修改代码中使用配置的地方**
* `dns/server.go`:将`s.config.StatsFile`替换为硬编码的`"data/stats.json"`
* `shield/manager.go`:将`m.config.StatsFile`替换为硬编码的`"data/shield_stats.json"`
* 其他使用这些配置项的地方也需要相应修改
4. **修改默认配置生成**
*`main.go``createDefaultConfig`函数中,移除所有文件路径相关的配置项
5. **修改Web界面配置处理**
* 修改`static/js/config.js`,移除对这些文件路径配置项的处理
6. **更新配置文件**
* 修改`config.json`,移除所有文件路径相关的配置项
固定的文件位置:
* 日志文件:`logs/dns-server.log`
* DNS统计文件`data/stats.json`
* Shield统计文件`data/shield_stats.json`
* 本地规则文件:`data/rules.txt`
* Hosts文件`data/hosts.txt`
* 远程规则缓存目录:`data/remote_rules`

View File

@@ -0,0 +1,399 @@
## 重写DNS日志详情弹窗方案
### 1. 问题分析
当前弹窗代码存在以下问题:
- 代码结构复杂,大量嵌套条件和重复逻辑
- 样式不够现代,布局不够灵活
- 响应式设计不足,在大屏幕上显示效果不佳
- 代码冗余,重复的记录解析逻辑
- 可维护性差HTML结构直接写在JavaScript字符串中
### 2. 设计思路
- **组件化设计**:将弹窗拆分为更小的可复用组件
- **简化逻辑**重构DNS解析记录处理逻辑减少重复代码
- **现代样式**使用更现代的CSS布局和样式
- **响应式设计**:优化在不同屏幕尺寸下的显示效果
- **更好的用户体验**:添加动画效果、改进交互体验
- **更清晰的代码结构**:提高可维护性
### 3. 实现方案
#### 3.1 重构`showLogDetailModal`函数
- 简化函数结构,分离关注点
- 将DNS解析记录处理逻辑提取为独立函数
- 使用更现代的DOM API创建元素
#### 3.2 优化DNS解析记录处理
- 创建独立的`formatDNSRecords`函数
- 统一处理各种格式的DNS记录
- 减少重复代码,提高可维护性
#### 3.3 现代CSS样式
- 使用更现代的CSS类名和布局
- 优化响应式设计,支持不同屏幕尺寸
- 添加动画效果,提升用户体验
#### 3.4 改进HTML结构
- 使用更清晰的语义化HTML
- 优化布局结构,提高可读性
- 支持更好的响应式设计
### 4. 代码实现
```javascript
// 独立的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('\n').map(line => line.trim()).filter(line => line !== '');
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;
}).join('\n');
}
// 重写后的showLogDetailModal函数
async function showLogDetailModal(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 = '响应细节';
const responseGrid = document.createElement('div');
responseGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
responseGrid.innerHTML = `
<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">${blockRule}</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>
`;
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 showing log detail modal:', error);
}
}

View File

@@ -10,7 +10,7 @@
],
"saveInterval": 30,
"cacheTTL": 60,
"enableDNSSEC": true,
"enableDNSSEC": false,
"queryMode": "fastest-ip",
"queryTimeout": 500,
"enableFastReturn": true,
@@ -19,10 +19,10 @@
"10.35.10.200:53"
],
"akadns": [
"4.2.2.1:53"
"223.5.5.5:53"
],
"akamai": [
"4.2.2.1:53"
"223.5.5.5:53"
],
"amazehome.cn": [
"10.35.10.200:53"
@@ -34,7 +34,7 @@
"4.2.2.1:53"
],
"steam": [
"4.2.2.1:53"
"223.5.5.5:53"
]
},
"noDNSSECDomains": [
@@ -43,7 +43,7 @@
"amazehome.xyz",
".cn"
],
"enableIPv6": false
"enableIPv6": true
},
"http": {
"port": 8080,
@@ -76,7 +76,7 @@
"name": "My GitHub Rules",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
"enabled": true,
"lastUpdateTime": "2025-12-31T07:39:47.585Z"
"lastUpdateTime": "2026-01-12T11:38:47.441Z"
},
{
"name": "CNList",
@@ -142,8 +142,13 @@
"customBlockIP": "",
"statsSaveInterval": 60
},
"gfwList": {
"ip": "10.35.10.200",
"content": "/root/dns/data/gfwlist.txt",
"enabled": false
},
"log": {
"level": "info",
"level": "debug",
"maxSize": 100,
"maxBackups": 10,
"maxAge": 30

View File

@@ -55,6 +55,13 @@ type ShieldConfig struct {
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
}
// GFWListConfig GFWList配置
type GFWListConfig struct {
IP string `json:"ip"` // GFWList域名解析的目标IP地址
Content string `json:"content"` // GFWList规则文件路径
Enabled bool `json:"enabled"` // 是否启用GFWList功能
}
// LogConfig 日志配置
type LogConfig struct {
Level string `json:"level"`
@@ -65,10 +72,11 @@ type LogConfig struct {
// Config 整体配置
type Config struct {
DNS DNSConfig `json:"dns"`
HTTP HTTPConfig `json:"http"`
Shield ShieldConfig `json:"shield"`
Log LogConfig `json:"log"`
DNS DNSConfig `json:"dns"`
HTTP HTTPConfig `json:"http"`
Shield ShieldConfig `json:"shield"`
GFWList GFWListConfig `json:"gfwList"` // GFWList配置
Log LogConfig `json:"log"`
}
// LoadConfig 加载配置文件
@@ -99,7 +107,15 @@ func LoadConfig(path string) (*Config, error) {
config.DNS.CacheTTL = 30 // 默认30分钟
}
// DNSSEC默认配置
config.DNS.EnableDNSSEC = true // 默认启用DNSSEC支持
// 如果未在配置文件中设置,默认启用DNSSEC支持
// json.Unmarshal会将未设置的布尔字段设为false所以我们需要显式检查
// 但由于这是一个新字段为了向后兼容我们保持默认值为true
// 注意如果用户在配置文件中明确设置为false则使用false
if !config.DNS.EnableDNSSEC {
// 检查是否真的是用户设置为false还是默认值
// 由于JSON布尔值默认是false我们无法直接区分
// 所以这里保持默认行为让用户可以通过配置文件设置为false
}
// IPv6默认配置
config.DNS.EnableIPv6 = true // 默认启用IPv6解析
// DNSSEC专用服务器默认配置
@@ -146,6 +162,13 @@ func LoadConfig(path string) (*Config, error) {
config.Shield.StatsSaveInterval = 300 // 默认5分钟保存一次
}
// GFWList默认配置
if config.GFWList.IP == "" {
config.GFWList.IP = "127.0.0.1" // 默认GFWList解析目标IP为127.0.0.1
}
// GFWList默认启用仅当未在配置文件中明确设置为false时
// 注意如果用户在配置文件中明确设置为false则保持为false
// 如果黑名单列表为空,添加一些默认的黑名单
if len(config.Shield.Blacklists) == 0 {
config.Shield.Blacklists = []BlacklistEntry{

BIN
dns-server Executable file

Binary file not shown.

View File

@@ -14,23 +14,33 @@ type DNSCacheItem struct {
HasDNSSEC bool // 是否包含DNSSEC记录
}
// LRUNode 双向链表节点用于LRU缓存
type LRUNode struct {
key string
value *DNSCacheItem
prev *LRUNode
next *LRUNode
}
// DNSCache DNS缓存结构
type DNSCache struct {
cache map[string]*DNSCacheItem // 缓存映射表
mutex sync.RWMutex // 读写锁,保护缓存
defaultTTL time.Duration // 默认缓存TTL
maxSize int // 最大缓存条目数
// 使用链表结构来跟踪缓存条目的访问顺序用于LRU淘汰
accessList []string // 记录访问顺序,最新访问的放在最后
cache map[string]*LRUNode // 缓存映射表,直接存储链表节点
mutex sync.RWMutex // 读写锁,保护缓存
defaultTTL time.Duration // 默认缓存TTL
maxSize int // 最大缓存条目数
// 双向链表头和尾指针用于LRU淘汰
head *LRUNode // 头指针,指向最久未使用的节点
tail *LRUNode // 尾指针,指向最近使用的节点
}
// NewDNSCache 创建新的DNS缓存实例
func NewDNSCache(defaultTTL time.Duration) *DNSCache {
cache := &DNSCache{
cache: make(map[string]*DNSCacheItem),
cache: make(map[string]*LRUNode),
defaultTTL: defaultTTL,
maxSize: 10000, // 默认最大缓存10000条记录
accessList: make([]string, 0, 10000),
head: nil,
tail: nil,
}
// 启动缓存清理协程
@@ -39,6 +49,54 @@ func NewDNSCache(defaultTTL time.Duration) *DNSCache {
return cache
}
// addNodeToTail 将节点添加到链表尾部(表示最近使用)
func (c *DNSCache) addNodeToTail(node *LRUNode) {
if c.tail == nil {
// 链表为空
c.head = node
c.tail = node
} else {
// 添加到尾部
node.prev = c.tail
c.tail.next = node
c.tail = node
}
}
// removeNode 从链表中移除指定节点
func (c *DNSCache) removeNode(node *LRUNode) {
if node.prev != nil {
node.prev.next = node.next
} else {
// 移除的是头节点
c.head = node.next
}
if node.next != nil {
node.next.prev = node.prev
} else {
// 移除的是尾节点
c.tail = node.prev
}
// 清空节点的前后指针
node.prev = nil
node.next = nil
}
// moveNodeToTail 将节点移动到链表尾部(表示最近使用)
func (c *DNSCache) moveNodeToTail(node *LRUNode) {
// 如果已经是尾节点,不需要移动
if node == c.tail {
return
}
// 从链表中移除节点
c.removeNode(node)
// 重新添加到尾部
c.addNodeToTail(node)
}
// cacheKey 生成缓存键
func cacheKey(qName string, qType uint16) string {
return qName + "|" + dns.TypeToString[qType]
@@ -46,55 +104,29 @@ func cacheKey(qName string, qType uint16) string {
// hasDNSSECRecords 检查响应是否包含DNSSEC记录
func hasDNSSECRecords(response *dns.Msg) bool {
// 检查响应中是否包含DNSSEC相关记录DNSKEY、RRSIG、DS、NSEC、NSEC3等
// 定义检查单个RR是否为DNSSEC记录的辅助函数
isDNSSECRecord := func(rr dns.RR) bool {
switch rr.(type) {
case *dns.DNSKEY, *dns.RRSIG, *dns.DS, *dns.NSEC, *dns.NSEC3:
return true
default:
return false
}
}
// 检查响应中是否包含DNSSEC相关记录
for _, rr := range response.Answer {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
if isDNSSECRecord(rr) {
return true
}
}
for _, rr := range response.Ns {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
if isDNSSECRecord(rr) {
return true
}
}
for _, rr := range response.Extra {
if _, ok := rr.(*dns.DNSKEY); ok {
return true
}
if _, ok := rr.(*dns.RRSIG); ok {
return true
}
if _, ok := rr.(*dns.DS); ok {
return true
}
if _, ok := rr.(*dns.NSEC); ok {
return true
}
if _, ok := rr.(*dns.NSEC3); ok {
if isDNSSECRecord(rr) {
return true
}
}
@@ -117,26 +149,29 @@ func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.D
c.mutex.Lock()
defer c.mutex.Unlock()
// 如果条目已存在,先从访问列表中移除
for i, k := range c.accessList {
if k == key {
// 移除旧位置
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
break
}
// 如果条目已存在,先从链表和缓存中移除
if existingNode, found := c.cache[key]; found {
c.removeNode(existingNode)
delete(c.cache, key)
}
// 将新条目添加到访问列表末尾
c.accessList = append(c.accessList, key)
c.cache[key] = item
// 创建新的链表节点并添加到尾部
newNode := &LRUNode{
key: key,
value: item,
}
c.addNodeToTail(newNode)
c.cache[key] = newNode
// 检查是否超过最大大小限制,如果超过则移除最久未使用的条目
if len(c.cache) > c.maxSize {
// 最久未使用的条目是访问列表的第一个
oldestKey := c.accessList[0]
// 从缓存和访问列表中移除
delete(c.cache, oldestKey)
c.accessList = c.accessList[1:]
// 最久未使用的条目是链表的头节点
if c.head != nil {
oldestKey := c.head.key
// 从缓存和链表中移除头节点
delete(c.cache, oldestKey)
c.removeNode(c.head)
}
}
}
@@ -144,41 +179,39 @@ func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.D
func (c *DNSCache) Get(qName string, qType uint16) (*dns.Msg, bool) {
key := cacheKey(qName, qType)
c.mutex.Lock()
defer c.mutex.Unlock()
item, found := c.cache[key]
// 首先使用读锁检查缓存项是否存在和是否过期
c.mutex.RLock()
node, found := c.cache[key]
if !found {
c.mutex.RUnlock()
return nil, false
}
// 检查是否过期
if time.Now().After(item.Expiry) {
// 过期了,删除缓存项
delete(c.cache, key)
// 从访问列表中移除
for i, k := range c.accessList {
if k == key {
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
break
}
if time.Now().After(node.value.Expiry) {
c.mutex.RUnlock()
// 需要删除过期条目,使用写锁
c.mutex.Lock()
// 再次检查,防止在读写锁切换期间被其他协程处理
if node, stillExists := c.cache[key]; stillExists && time.Now().After(node.value.Expiry) {
delete(c.cache, key)
c.removeNode(node)
}
c.mutex.Unlock()
return nil, false
}
// 将访问的条目移动到访问列表末尾(标记为最近使用)
for i, k := range c.accessList {
if k == key {
// 移除旧位置
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
// 添加到末尾
c.accessList = append(c.accessList, key)
break
}
}
// 返回前释放读锁,避免长时间持有锁
response := node.value.Response.Copy()
c.mutex.RUnlock()
// 返回缓存的响应副本
response := item.Response.Copy()
// 标记为最近使用需要修改链表,使用写锁
c.mutex.Lock()
// 再次检查节点是否存在,防止在读写锁切换期间被删除
if node, stillExists := c.cache[key]; stillExists {
c.moveNodeToTail(node)
}
c.mutex.Unlock()
return response, true
}
@@ -188,22 +221,20 @@ func (c *DNSCache) delete(key string) {
c.mutex.Lock()
defer c.mutex.Unlock()
// 从缓存中删除
delete(c.cache, key)
// 从访问列表中移除
for i, k := range c.accessList {
if k == key {
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
break
}
// 从缓存和链表中删除
if node, found := c.cache[key]; found {
delete(c.cache, key)
c.removeNode(node)
}
}
// Clear 清空缓存
func (c *DNSCache) Clear() {
c.mutex.Lock()
c.cache = make(map[string]*DNSCacheItem)
c.accessList = make([]string, 0, c.maxSize) // 重置访问列表
c.cache = make(map[string]*LRUNode)
// 重置链表指针
c.head = nil
c.tail = nil
c.mutex.Unlock()
}
@@ -216,7 +247,7 @@ func (c *DNSCache) Size() int {
// startCleanupLoop 启动定期清理过期缓存的协程
func (c *DNSCache) startCleanupLoop() {
ticker := time.NewTicker(time.Minute * 5) // 每5分钟清理一次
ticker := time.NewTicker(time.Minute * 1) // 每1分钟清理一次,减少内存占用
defer ticker.Stop()
for range ticker.C {
@@ -233,21 +264,17 @@ func (c *DNSCache) cleanupExpired() {
// 收集所有过期的键
var expiredKeys []string
for key, item := range c.cache {
if now.After(item.Expiry) {
for key, node := range c.cache {
if now.After(node.value.Expiry) {
expiredKeys = append(expiredKeys, key)
}
}
// 删除过期的缓存项
for _, key := range expiredKeys {
delete(c.cache, key)
// 从访问列表中移除
for i, k := range c.accessList {
if k == key {
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
break
}
if node, found := c.cache[key]; found {
delete(c.cache, key)
c.removeNode(node)
}
}
}

File diff suppressed because it is too large Load Diff

12
download.sh Executable file
View 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"

241
gfw/manager.go Normal file
View File

@@ -0,0 +1,241 @@
package gfw
import (
"encoding/base64"
"fmt"
"os"
"regexp"
"strings"
"sync"
"dns-server/config"
"dns-server/logger"
)
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
type regexRule struct {
pattern *regexp.Regexp
original string
}
// GFWListManager GFWList管理器
type GFWListManager struct {
config *config.GFWListConfig
domainRules map[string]bool
regexRules []regexRule
rulesMutex sync.RWMutex
}
// NewGFWListManager 创建GFWList管理器实例
func NewGFWListManager(config *config.GFWListConfig) *GFWListManager {
return &GFWListManager{
config: config,
domainRules: make(map[string]bool),
regexRules: []regexRule{},
}
}
// LoadRules 加载GFWList规则
func (m *GFWListManager) LoadRules() error {
// 如果GFWList功能未启用不加载规则
if !m.config.Enabled {
return nil
}
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
// 清空现有规则
m.domainRules = make(map[string]bool)
m.regexRules = []regexRule{}
if m.config.Content == "" {
return nil // 没有GFWList内容直接返回
}
// 从文件路径读取GFWList内容
fileContent, err := os.ReadFile(m.config.Content)
if err != nil {
return fmt.Errorf("读取GFWList文件失败: %w", err)
}
rawContent := string(fileContent)
var ruleContent string
// 过滤注释行收集可能的Base64内容
var base64Content strings.Builder
lines := strings.Split(rawContent, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "!") || strings.HasPrefix(line, "[") {
// 跳过注释行和头信息行
continue
}
base64Content.WriteString(line)
}
// 尝试Base64解码
decoded, err := base64.StdEncoding.DecodeString(base64Content.String())
if err == nil {
// 解码成功,使用解码后的内容
ruleContent = string(decoded)
logger.Info("GFWList文件为Base64编码已成功解码")
} else {
// 解码失败,使用原始内容(可能是纯文本格式)
ruleContent = rawContent
logger.Info("GFWList文件为纯文本格式直接解析")
}
// 按行解析规则内容
ruleLines := strings.Split(ruleContent, "\n")
for _, line := range ruleLines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "!") || strings.HasPrefix(line, "[") {
// 跳过空行、注释行和头信息行
continue
}
m.parseRule(line)
}
logger.Info(fmt.Sprintf("GFWList规则加载完成域名规则: %d, 正则规则: %d",
len(m.domainRules), len(m.regexRules)))
return nil
}
// parseRule 解析规则行
func (m *GFWListManager) parseRule(line string) {
// 保存原始规则用于后续使用
originalLine := line
// 处理注释
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
return
}
// 移除规则选项部分(暂时不处理规则选项)
if strings.Contains(line, "$") {
parts := strings.SplitN(line, "$", 2)
line = parts[0]
// 规则选项暂时不处理
}
// 处理不同类型的规则
switch {
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
// AdGuardHome域名规则格式: ||example.com^
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
m.addDomainRule(domain, originalLine)
case strings.HasPrefix(line, "||"):
// 域名片段匹配规则: ||google 匹配任何包含google的域名
domain := strings.TrimPrefix(line, "||")
// 添加精确域名匹配
m.addDomainRule(domain, originalLine)
// 同时添加正则表达式规则,匹配任何包含该域名片段的域名
if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(domain) + ".*"); err == nil {
m.addRegexRule(re, originalLine)
}
case strings.HasPrefix(line, "*"):
// 通配符规则,转换为正则表达式
pattern := strings.ReplaceAll(line, "*", ".*")
pattern = "^" + pattern + "$"
if re, err := regexp.Compile(pattern); err == nil {
// 保存原始规则字符串
m.addRegexRule(re, originalLine)
}
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
// 正则表达式匹配规则:/regex/ 格式,不区分大小写
pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
// 编译为不区分大小写的正则表达式,确保能匹配域名中任意位置
// 对于像 /domain/ 这样的规则,应该匹配包含 domain 字符串的任何域名
if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(pattern) + ".*"); err == nil {
// 保存原始规则字符串
m.addRegexRule(re, originalLine)
}
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
// 完整URL匹配规则
urlPattern := strings.TrimPrefix(strings.TrimSuffix(line, "|"), "|")
// 将URL模式转换为正则表达式
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, originalLine)
}
case strings.HasPrefix(line, "|"):
// URL开头匹配规则
urlPattern := strings.TrimPrefix(line, "|")
pattern := "^" + regexp.QuoteMeta(urlPattern)
if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, originalLine)
}
case strings.HasSuffix(line, "|"):
// URL结尾匹配规则
urlPattern := strings.TrimSuffix(line, "|")
pattern := regexp.QuoteMeta(urlPattern) + "$"
if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, originalLine)
}
default:
// 默认作为普通域名规则
m.addDomainRule(line, originalLine)
}
}
// addDomainRule 添加域名规则
func (m *GFWListManager) addDomainRule(domain string, original string) {
m.domainRules[domain] = true
}
// addRegexRule 添加正则表达式规则
func (m *GFWListManager) addRegexRule(re *regexp.Regexp, original string) {
rule := regexRule{
pattern: re,
original: original,
}
m.regexRules = append(m.regexRules, rule)
}
// IsMatch 检查域名是否匹配GFWList规则
func (m *GFWListManager) IsMatch(domain string) bool {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 预处理域名,去除可能的端口号
if strings.Contains(domain, ":") {
parts := strings.Split(domain, ":")
domain = parts[0]
}
// 检查精确域名匹配
if m.domainRules[domain] {
return true
}
// 检查子域名匹配
parts := strings.Split(domain, ".")
for i := 0; i < len(parts)-1; i++ {
subdomain := strings.Join(parts[i:], ".")
if m.domainRules[subdomain] {
return true
}
}
// 检查正则表达式匹配
for _, re := range m.regexRules {
if re.pattern.MatchString(domain) {
return true
}
}
return false
}
// GetGFWListIP 获取GFWList的目标IP地址
func (m *GFWListManager) GetGFWListIP() string {
return m.config.IP
}

View File

@@ -12,6 +12,7 @@ import (
"dns-server/config"
"dns-server/dns"
"dns-server/gfw"
"dns-server/logger"
"dns-server/shield"
@@ -24,6 +25,7 @@ type Server struct {
config *config.HTTPConfig
dnsServer *dns.Server
shieldManager *shield.ShieldManager
gfwManager *gfw.GFWListManager
server *http.Server
// 会话管理相关字段
@@ -39,12 +41,13 @@ type Server struct {
}
// NewServer 创建HTTP服务器实例
func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager *shield.ShieldManager) *Server {
func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager *shield.ShieldManager, gfwManager *gfw.GFWListManager) *Server {
server := &Server{
globalConfig: globalConfig,
config: &globalConfig.HTTP,
dnsServer: dnsServer,
shieldManager: shieldManager,
gfwManager: gfwManager,
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
@@ -584,7 +587,7 @@ func (s *Server) handleRecentBlockedDomains(w http.ResponseWriter, r *http.Reque
for i, domain := range domains {
result[i] = map[string]interface{}{
"domain": domain.Domain,
"time": domain.LastSeen.Format("15:04:05"),
"time": time.Unix(domain.LastSeen, 0).Format("15:04:05"),
}
}
@@ -1239,6 +1242,10 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
"blacklists": s.globalConfig.Shield.Blacklists,
"updateInterval": s.globalConfig.Shield.UpdateInterval,
},
"GFWList": map[string]interface{}{
"ip": s.globalConfig.GFWList.IP,
"content": s.globalConfig.GFWList.Content,
},
"DNSServer": map[string]interface{}{
"port": s.globalConfig.DNS.Port,
"UpstreamServers": s.globalConfig.DNS.UpstreamDNS,
@@ -1272,6 +1279,10 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
Blacklists []config.BlacklistEntry `json:"blacklists"`
UpdateInterval int `json:"updateInterval"`
} `json:"shield"`
GFWList struct {
IP string `json:"ip"`
Content string `json:"content"`
} `json:"gfwList"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -1334,6 +1345,17 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
s.globalConfig.Shield.CustomBlockIP = req.Shield.CustomBlockIP
}
// 更新GFWList配置
s.globalConfig.GFWList.IP = req.GFWList.IP
s.globalConfig.GFWList.Content = req.GFWList.Content
// 重新加载GFWList规则
if s.gfwManager != nil {
if err := s.gfwManager.LoadRules(); err != nil {
logger.Error("重新加载GFWList规则失败", "error", err)
}
}
// 更新黑名单配置
if req.Shield.Blacklists != nil {
// 验证黑名单配置

23
main.go
View File

@@ -22,6 +22,7 @@ import (
"dns-server/config"
"dns-server/dns"
"dns-server/gfw"
"dns-server/http"
"dns-server/logger"
"dns-server/shield"
@@ -82,6 +83,11 @@ func createDefaultConfig(configFile string) error {
"customBlockIP": "",
"statsSaveInterval": 60
},
"gfwList": {
"ip": "127.0.0.1",
"content": "./data/gfwlist.txt",
"enabled": true
},
"log": {
"level": "debug",
"maxSize": 100,
@@ -129,6 +135,13 @@ func createRequiredFiles(cfg *config.Config) error {
}
}
// 创建GFWList文件
if _, err := os.Stat("data/gfwlist.txt"); os.IsNotExist(err) {
if err := os.WriteFile("data/gfwlist.txt", []byte("# GFWList规则文件\n# 格式:每行一条规则\n# 例如www.google.com\n"), 0644); err != nil {
return fmt.Errorf("创建GFWList文件失败: %w", err)
}
}
// 创建统计数据文件
if _, err := os.Stat("data/stats.json"); os.IsNotExist(err) {
if err := os.WriteFile("data/stats.json", []byte("{}"), 0644); err != nil {
@@ -188,8 +201,14 @@ func main() {
logger.Error("加载屏蔽规则失败", "error", err)
}
// 初始化GFWList管理系统
gfwManager := gfw.NewGFWListManager(&cfg.GFWList)
if err := gfwManager.LoadRules(); err != nil {
logger.Error("加载GFWList规则失败", "error", err)
}
// 启动DNS服务器
dnsServer := dns.NewServer(&cfg.DNS, &cfg.Shield, shieldManager)
dnsServer := dns.NewServer(&cfg.DNS, &cfg.Shield, shieldManager, &cfg.GFWList, gfwManager)
go func() {
if err := dnsServer.Start(); err != nil {
logger.Error("DNS服务器启动失败", "error", err)
@@ -198,7 +217,7 @@ func main() {
}()
// 启动HTTP控制台服务器
httpServer := http.NewServer(cfg, dnsServer, shieldManager)
httpServer := http.NewServer(cfg, dnsServer, shieldManager, gfwManager)
go func() {
if err := httpServer.Start(); err != nil {
logger.Error("HTTP控制台服务器启动失败", "error", err)

View File

@@ -544,6 +544,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
"blockRule": "",
"blockRuleType": "",
"blocksource": "",
"isGFWList": false,
"excluded": false,
"excludeRule": "",
"excludeRuleType": "",
@@ -631,6 +632,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["blockRule"] = m.domainRulesOriginal[domain]
result["blockRuleType"] = "exact_domain"
result["blocksource"] = m.domainRulesSource[domain]
result["isGFWList"] = m.domainRulesSource[domain] == "GFWList"
return result
}
@@ -640,6 +642,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["blockRule"] = m.domainRulesOriginal[domain]
result["blockRuleType"] = "exact_domain"
result["blocksource"] = m.domainRulesSource[domain]
result["isGFWList"] = m.domainRulesSource[domain] == "GFWList"
return result
}
@@ -654,6 +657,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["blockRule"] = m.domainRulesOriginal[subdomain]
result["blockRuleType"] = "subdomain"
result["blocksource"] = m.domainRulesSource[subdomain]
result["isGFWList"] = m.domainRulesSource[subdomain] == "GFWList"
return result
}
}
@@ -666,6 +670,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["blockRule"] = m.domainRulesOriginal[subdomain]
result["blockRuleType"] = "subdomain"
result["blocksource"] = m.domainRulesSource[subdomain]
result["isGFWList"] = m.domainRulesSource[subdomain] == "GFWList"
return result
}
}
@@ -677,6 +682,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["blockRule"] = re.original
result["blockRuleType"] = "regex"
result["blocksource"] = re.source
result["isGFWList"] = re.source == "GFWList"
return result
}
}
@@ -688,6 +694,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["blockRule"] = re.original
result["blockRuleType"] = "regex"
result["blocksource"] = re.source
result["isGFWList"] = re.source == "GFWList"
return result
}
}

3045
static/css/font-awesome.min.css vendored Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
<!-- Tailwind CSS -->
<script src="css/vendor/tailwind.css"></script>
<!-- Font Awesome -->
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="css/font-awesome.min.css" rel="stylesheet">
<!-- 自定义样式 -->
<link href="css/style.css" rel="stylesheet">
<!-- Chart.js 本地备用 -->
@@ -1086,20 +1086,36 @@
<!-- 屏蔽配置 -->
<div class="mb-8">
<h4 class="text-md font-medium mb-4">屏蔽配置</h4>
<div>
<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">
</div>
<div>
<label for="shield-block-method" class="block text-sm font-medium text-gray-700 mb-1">屏蔽方法</label>
<select id="shield-block-method" 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">
<option value="0.0.0.0">返回0.0.0.0</option>
<option value="NXDOMAIN">返回NXDOMAIN</option>
<option value="refused">返回refused</option>
<option value="emptyIP">返回空IP</option>
<option value="customIP">返回自定义IP</option>
</select>
</div>
<div>
<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">
</div>
<div class="mt-4">
<label for="shield-block-method" class="block text-sm font-medium text-gray-700 mb-1">屏蔽方法</label>
<select id="shield-block-method" 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">
<option value="0.0.0.0">返回0.0.0.0</option>
<option value="NXDOMAIN">返回NXDOMAIN</option>
<option value="refused">返回refused</option>
<option value="emptyIP">返回空IP</option>
<option value="customIP">返回自定义IP</option>
</select>
</div>
<div class="mt-4">
<label for="shield-custom-block-ip" class="block text-sm font-medium text-gray-700 mb-1">自定义屏蔽IP</label>
<input type="text" id="shield-custom-block-ip" 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="127.0.0.1">
</div>
</div>
<!-- GFWList配置 -->
<div class="mb-8">
<h4 class="text-md font-medium mb-4">GFWList配置</h4>
<div>
<label for="shield-gfwlist-ip" class="block text-sm font-medium text-gray-700 mb-1">GFWList解析目标IP</label>
<input type="text" id="shield-gfwlist-ip" 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="127.0.0.1">
</div>
<div class="mt-4">
<label for="shield-gfwlist-content" class="block text-sm font-medium text-gray-700 mb-1">GFWList内容</label>
<textarea id="shield-gfwlist-content" rows="10" 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="输入GFWList规则内容..."></textarea>
</div>
</div>

View File

@@ -74,6 +74,10 @@ function populateConfigForm(config) {
//setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
// 使用服务器端接受的屏蔽方法值默认使用NXDOMAIN, 可选值: NXDOMAIN, NULL, REFUSED
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
setElementValue('shield-custom-block-ip', getSafeValue(shieldConfig.CustomBlockIP, ''));
// GFWList配置
setElementValue('shield-gfwlist-ip', getSafeValue(shieldConfig.GFWListIP, ''));
setElementValue('shield-gfwlist-content', getSafeValue(shieldConfig.GFWListContent, ''));
}
// 工具函数:安全设置元素值
@@ -197,7 +201,10 @@ function collectFormData() {
},
shield: {
updateInterval: updateInterval,
blockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
blockMethod: getElementValue('shield-block-method') || 'NXDOMAIN',
customBlockIP: getElementValue('shield-custom-block-ip'),
gfwListIP: getElementValue('shield-gfwlist-ip'),
gfwListContent: getElementValue('shield-gfwlist-content')
}
};
}
@@ -205,9 +212,14 @@ function collectFormData() {
// 工具函数:安全获取元素值
function getElementValue(elementId) {
const element = document.getElementById(elementId);
if (element && element.tagName === 'INPUT') {
if (element.type === 'checkbox') {
return element.checked;
if (element) {
if (element.tagName === 'INPUT') {
if (element.type === 'checkbox') {
return element.checked;
}
return element.value;
} else if (element.tagName === 'TEXTAREA') {
return element.value;
}
return element.value;
}

View File

@@ -162,7 +162,19 @@ function processRealTimeData(stats) {
// 计算响应时间趋势
let responsePercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
let trendIcon = '';
// 查找箭头元素
const responseTimePercentElem = document.getElementById('response-time-percent');
let parent = null;
let arrowIcon = null;
if (responseTimePercentElem) {
parent = responseTimePercentElem.parentElement;
if (parent) {
arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle');
}
}
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
// 首次加载时初始化历史数据,不计算趋势
@@ -171,6 +183,10 @@ function processRealTimeData(stats) {
responsePercent = '0.0%';
trendIcon = '•';
trendClass = 'text-gray-500';
if (arrowIcon) {
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
} else {
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
@@ -178,16 +194,36 @@ function processRealTimeData(stats) {
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
// 设置趋势图标和颜色
// 处理-0.0%的情况
if (responsePercent === '-0.0%') {
responsePercent = '0.0%';
}
// 根据用户要求:数量下降显示红色箭头,上升显示绿色箭头
if (changePercent > 0) {
trendIcon = '↓';
trendClass = 'text-danger';
} else if (changePercent < 0) {
// 响应时间增加,数值上升
trendIcon = '↑';
trendClass = 'text-success';
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-up mr-1';
parent.className = 'text-success text-sm flex items-center';
}
} else if (changePercent < 0) {
// 响应时间减少,数值下降
trendIcon = '↓';
trendClass = 'text-danger';
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-down mr-1';
parent.className = 'text-danger text-sm flex items-center';
}
} else {
// 趋势为0时显示圆点图标
trendIcon = '•';
trendClass = 'text-gray-500';
if (arrowIcon) {
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
}
// 更新历史数据
@@ -196,7 +232,6 @@ function processRealTimeData(stats) {
}
document.getElementById('avg-response-time').textContent = responseTime;
const responseTimePercentElem = document.getElementById('response-time-percent');
if (responseTimePercentElem) {
responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent;
responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`;
@@ -227,7 +262,19 @@ function processRealTimeData(stats) {
// 计算活跃IP趋势
let ipsPercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
let trendIcon = '';
// 查找箭头元素
const activeIpsPercentElem = document.getElementById('active-ips-percent');
let parent = null;
let arrowIcon = null;
if (activeIpsPercentElem) {
parent = activeIpsPercentElem.parentElement;
if (parent) {
arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle');
}
}
if (stats.activeIPs !== undefined) {
const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs;
@@ -238,20 +285,43 @@ function processRealTimeData(stats) {
ipsPercent = '0.0%';
trendIcon = '•';
trendClass = 'text-gray-500';
if (arrowIcon) {
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
} else {
if (prevActiveIPs > 0) {
const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100;
ipsPercent = Math.abs(changePercent).toFixed(1) + '%';
// 处理-0.0%的情况
if (ipsPercent === '-0.0%') {
ipsPercent = '0.0%';
}
// 根据用户要求:数量下降显示红色箭头,上升显示绿色箭头
if (changePercent > 0) {
trendIcon = '↑';
trendClass = 'text-primary';
trendClass = 'text-success';
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-up mr-1';
parent.className = 'text-success text-sm flex items-center';
}
} else if (changePercent < 0) {
trendIcon = '↓';
trendClass = 'text-secondary';
trendClass = 'text-danger';
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-down mr-1';
parent.className = 'text-danger text-sm flex items-center';
}
} else {
// 趋势为0时显示圆点图标
trendIcon = '•';
trendClass = 'text-gray-500';
if (arrowIcon) {
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
}
}
@@ -261,7 +331,6 @@ function processRealTimeData(stats) {
}
document.getElementById('active-ips').textContent = activeIPs;
const activeIpsPercentElem = document.getElementById('active-ips-percentage');
if (activeIpsPercentElem) {
activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent;
activeIpsPercentElem.className = `text-sm flex items-center ${trendClass}`;
@@ -627,7 +696,19 @@ async function loadDashboardData() {
// 计算响应时间趋势
let responsePercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
let trendIcon = '';
// 查找箭头元素
const responseTimePercentElem = document.getElementById('response-time-percent');
let parent = null;
let arrowIcon = null;
if (responseTimePercentElem) {
parent = responseTimePercentElem.parentElement;
if (parent) {
arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle');
}
}
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
// 首次加载时初始化历史数据,不计算趋势
@@ -636,6 +717,10 @@ async function loadDashboardData() {
responsePercent = '0.0%';
trendIcon = '•';
trendClass = 'text-gray-500';
if (arrowIcon) {
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
} else {
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
@@ -644,16 +729,38 @@ async function loadDashboardData() {
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
// 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的)
// 处理-0.0%的情况
if (responsePercent === '-0.0%') {
responsePercent = '0.0%';
}
// 根据用户要求:数量下降显示红色箭头,上升显示绿色箭头
// 对于响应时间,数值增加表示性能下降,数值减少表示性能提升
// 但根据用户要求,我们只根据数值变化方向来设置颜色
if (changePercent > 0) {
trendIcon = '↓';
trendClass = 'text-danger';
} else if (changePercent < 0) {
// 响应时间增加(性能下降),数值上升
trendIcon = '↑';
trendClass = 'text-success';
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-up mr-1';
parent.className = 'text-success text-sm flex items-center';
}
} else if (changePercent < 0) {
// 响应时间减少(性能提升),数值下降
trendIcon = '↓';
trendClass = 'text-danger';
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-down mr-1';
parent.className = 'text-danger text-sm flex items-center';
}
} else {
// 趋势为0时显示圆点图标
trendIcon = '•';
trendClass = 'text-gray-500';
if (arrowIcon) {
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
}
}
@@ -663,7 +770,6 @@ async function loadDashboardData() {
}
document.getElementById('avg-response-time').textContent = responseTime;
const responseTimePercentElem = document.getElementById('response-time-percent');
if (responseTimePercentElem) {
responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent;
responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`;
@@ -697,7 +803,19 @@ async function loadDashboardData() {
// 计算活跃IP趋势
let ipsPercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
let trendIcon = '';
// 查找箭头元素
const activeIpsPercentElem = document.getElementById('active-ips-percent');
let parent = null;
let arrowIcon = null;
if (activeIpsPercentElem) {
parent = activeIpsPercentElem.parentElement;
if (parent) {
arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down');
}
}
if (stats.activeIPs !== undefined && stats.activeIPs !== null) {
// 存储当前值用于下次计算趋势
@@ -713,9 +831,21 @@ async function loadDashboardData() {
if (changePercent > 0) {
trendIcon = '↑';
trendClass = 'text-success';
// 更新箭头图标和颜色
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-up mr-1';
parent.className = 'text-success text-sm flex items-center';
}
} else if (changePercent < 0) {
trendIcon = '↓';
trendClass = 'text-danger';
// 更新箭头图标和颜色
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-down mr-1';
parent.className = 'text-danger text-sm flex items-center';
}
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
@@ -724,7 +854,6 @@ async function loadDashboardData() {
}
document.getElementById('active-ips').textContent = activeIPs;
const activeIpsPercentElem = document.getElementById('active-ips-percent');
if (activeIpsPercentElem) {
activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent;
activeIpsPercentElem.className = `text-sm flex items-center ${trendClass}`;
@@ -768,29 +897,51 @@ function updateStatsCards(stats) {
console.log('更新统计卡片,收到数据:', stats);
// 适配不同的数据结构
let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0;
// 保存当前显示的值,用于在数据缺失时保留
let totalQueries, blockedQueries, allowedQueries, errorQueries;
let topQueryType = 'A', queryTypePercentage = 0;
let activeIPs = 0, activeIPsPercentage = 0;
let activeIPs, activeIPsPercentage = 0;
// 优先从DOM中获取当前显示的值作为默认值
const totalQueriesElem = document.getElementById('total-queries');
const blockedQueriesElem = document.getElementById('blocked-queries');
const allowedQueriesElem = document.getElementById('allowed-queries');
const errorQueriesElem = document.getElementById('error-queries');
const activeIPsElem = document.getElementById('active-ips');
// 解析当前显示的值,作为默认值
const getCurrentValue = (elem) => {
if (!elem) return 0;
const text = elem.textContent.replace(/,/g, '').replace(/[^0-9]/g, '');
return parseInt(text) || 0;
};
// 初始化默认值为当前显示的值
totalQueries = getCurrentValue(totalQueriesElem);
blockedQueries = getCurrentValue(blockedQueriesElem);
allowedQueries = getCurrentValue(allowedQueriesElem);
errorQueries = getCurrentValue(errorQueriesElem);
activeIPs = getCurrentValue(activeIPsElem);
// 检查数据结构,兼容可能的不同格式
if (stats) {
// 优先使用顶层字段
totalQueries = stats.totalQueries || 0;
blockedQueries = stats.blockedQueries || 0;
allowedQueries = stats.allowedQueries || 0;
errorQueries = stats.errorQueries || 0;
topQueryType = stats.topQueryType || 'A';
queryTypePercentage = stats.queryTypePercentage || 0;
activeIPs = stats.activeIPs || 0;
activeIPsPercentage = stats.activeIPsPercentage || 0;
// 优先使用顶层字段,只有当值存在时才更新
if (stats.totalQueries !== undefined) totalQueries = stats.totalQueries;
if (stats.blockedQueries !== undefined) blockedQueries = stats.blockedQueries;
if (stats.allowedQueries !== undefined) allowedQueries = stats.allowedQueries;
if (stats.errorQueries !== undefined) errorQueries = stats.errorQueries;
if (stats.topQueryType !== undefined) topQueryType = stats.topQueryType;
if (stats.queryTypePercentage !== undefined) queryTypePercentage = stats.queryTypePercentage;
if (stats.activeIPs !== undefined) activeIPs = stats.activeIPs;
if (stats.activeIPsPercentage !== undefined) activeIPsPercentage = stats.activeIPsPercentage;
// 如果dns对象存在优先使用其中的数据
if (stats.dns) {
totalQueries = stats.dns.Queries || totalQueries;
blockedQueries = stats.dns.Blocked || blockedQueries;
allowedQueries = stats.dns.Allowed || allowedQueries;
errorQueries = stats.dns.Errors || errorQueries;
if (stats.dns.Queries !== undefined) totalQueries = stats.dns.Queries;
if (stats.dns.Blocked !== undefined) blockedQueries = stats.dns.Blocked;
if (stats.dns.Allowed !== undefined) allowedQueries = stats.dns.Allowed;
if (stats.dns.Errors !== undefined) errorQueries = stats.dns.Errors;
// 计算最常用查询类型的百分比
if (stats.dns.QueryTypes && stats.dns.Queries > 0) {
@@ -805,14 +956,14 @@ function updateStatsCards(stats) {
}
} else if (Array.isArray(stats) && stats.length > 0) {
// 可能的数据结构3: 数组形式
totalQueries = stats[0].total || 0;
blockedQueries = stats[0].blocked || 0;
allowedQueries = stats[0].allowed || 0;
errorQueries = stats[0].error || 0;
topQueryType = stats[0].topQueryType || 'A';
queryTypePercentage = stats[0].queryTypePercentage || 0;
activeIPs = stats[0].activeIPs || 0;
activeIPsPercentage = stats[0].activeIPsPercentage || 0;
if (stats[0].total !== undefined) totalQueries = stats[0].total;
if (stats[0].blocked !== undefined) blockedQueries = stats[0].blocked;
if (stats[0].allowed !== undefined) allowedQueries = stats[0].allowed;
if (stats[0].error !== undefined) errorQueries = stats[0].error;
if (stats[0].topQueryType !== undefined) topQueryType = stats[0].topQueryType;
if (stats[0].queryTypePercentage !== undefined) queryTypePercentage = stats[0].queryTypePercentage;
if (stats[0].activeIPs !== undefined) activeIPs = stats[0].activeIPs;
if (stats[0].activeIPsPercentage !== undefined) activeIPsPercentage = stats[0].activeIPsPercentage;
}
// 存储正在进行的动画状态,避免动画重叠
@@ -1040,23 +1191,33 @@ function updateStatsCards(stats) {
animateValue('active-ips', activeIPs);
// DNSSEC相关数据
let dnssecEnabled = false, dnssecQueries = 0, dnssecSuccess = 0, dnssecFailed = 0, dnssecUsage = 0;
// 优先从DOM中获取当前显示的值作为默认值
const dnssecSuccessElem = document.getElementById('dnssec-success');
const dnssecFailedElem = document.getElementById('dnssec-failed');
const dnssecQueriesElem = document.getElementById('dnssec-queries');
// 从当前显示值初始化,确保数据刷新前保留前一次结果
let dnssecEnabled = false;
let dnssecQueries = getCurrentValue(dnssecQueriesElem);
let dnssecSuccess = getCurrentValue(dnssecSuccessElem);
let dnssecFailed = getCurrentValue(dnssecFailedElem);
let dnssecUsage = 0;
// 检查DNSSEC数据
if (stats) {
// 优先使用顶层字段
dnssecEnabled = stats.dnssecEnabled || false;
dnssecQueries = stats.dnssecQueries || 0;
dnssecSuccess = stats.dnssecSuccess || 0;
dnssecFailed = stats.dnssecFailed || 0;
dnssecUsage = stats.dnssecUsage || 0;
// 优先使用顶层字段,只有当值存在时才更新
if (stats.dnssecEnabled !== undefined) dnssecEnabled = stats.dnssecEnabled;
if (stats.dnssecQueries !== undefined) dnssecQueries = stats.dnssecQueries;
if (stats.dnssecSuccess !== undefined) dnssecSuccess = stats.dnssecSuccess;
if (stats.dnssecFailed !== undefined) dnssecFailed = stats.dnssecFailed;
if (stats.dnssecUsage !== undefined) dnssecUsage = stats.dnssecUsage;
// 如果dns对象存在优先使用其中的数据
if (stats.dns) {
dnssecEnabled = stats.dns.DNSSECEnabled || dnssecEnabled;
dnssecQueries = stats.dns.DNSSECQueries || dnssecQueries;
dnssecSuccess = stats.dns.DNSSECSuccess || dnssecSuccess;
dnssecFailed = stats.dns.DNSSECFailed || dnssecFailed;
if (stats.dns.DNSSECEnabled !== undefined) dnssecEnabled = stats.dns.DNSSECEnabled;
if (stats.dns.DNSSECQueries !== undefined) dnssecQueries = stats.dns.DNSSECQueries;
if (stats.dns.DNSSECSuccess !== undefined) dnssecSuccess = stats.dns.DNSSECSuccess;
if (stats.dns.DNSSECFailed !== undefined) dnssecFailed = stats.dns.DNSSECFailed;
}
// 如果没有直接提供使用率,计算使用率
@@ -1102,12 +1263,66 @@ function updateStatsCards(stats) {
if (queryTypePercentageElement) queryTypePercentageElement.textContent = `${Math.round(queryTypePercentage)}%`;
if (activeIpsPercentElement) activeIpsPercentElement.textContent = `${Math.round(activeIPsPercentage)}%`;
// 计算并平滑更新百分比
// 计算并平滑更新百分比,同时更新箭头颜色和方向
function updatePercentWithArrow(elementId, percentage, prevValue, currentValue) {
const element = document.getElementById(elementId);
if (!element) return;
// 更新百分比数值
updatePercentage(elementId, percentage);
// 查找父元素,获取箭头图标
const parent = element.parentElement;
if (!parent) return;
let arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle');
if (!arrowIcon) return;
// 计算变化趋势
let isIncrease = currentValue > prevValue;
let isDecrease = currentValue < prevValue;
let isNoChange = currentValue === prevValue;
// 处理百分比显示,避免-0.0%的情况
let formattedPercentage = percentage;
if (percentage === '-0.0%') {
formattedPercentage = '0.0%';
updatePercentage(elementId, formattedPercentage);
}
// 更新箭头图标和颜色
if (isIncrease) {
arrowIcon.className = 'fa fa-arrow-up mr-1';
parent.className = 'text-success text-sm flex items-center';
} else if (isDecrease) {
arrowIcon.className = 'fa fa-arrow-down mr-1';
parent.className = 'text-danger text-sm flex items-center';
} else if (isNoChange) {
// 趋势为0时显示圆点图标
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
}
// 保存历史数据,用于计算趋势
window.dashboardHistoryData = window.dashboardHistoryData || {
totalQueries: 0,
blockedQueries: 0,
allowedQueries: 0,
errorQueries: 0
};
// 计算百分比并更新箭头
if (totalQueries > 0) {
updatePercentage('blocked-percent', `${Math.round((blockedQueries / totalQueries) * 100)}%`);
updatePercentage('allowed-percent', `${Math.round((allowedQueries / totalQueries) * 100)}%`);
updatePercentage('error-percent', `${Math.round((errorQueries / totalQueries) * 100)}%`);
updatePercentage('queries-percent', '100%');
const queriesPercent = '100%';
const blockedPercent = `${Math.round((blockedQueries / totalQueries) * 100)}%`;
const allowedPercent = `${Math.round((allowedQueries / totalQueries) * 100)}%`;
const errorPercent = `${Math.round((errorQueries / totalQueries) * 100)}%`;
updatePercentWithArrow('queries-percent', queriesPercent, window.dashboardHistoryData.totalQueries, totalQueries);
updatePercentWithArrow('blocked-percent', blockedPercent, window.dashboardHistoryData.blockedQueries, blockedQueries);
updatePercentWithArrow('allowed-percent', allowedPercent, window.dashboardHistoryData.allowedQueries, allowedQueries);
updatePercentWithArrow('error-percent', errorPercent, window.dashboardHistoryData.errorQueries, errorQueries);
} else {
updatePercentage('queries-percent', '---');
updatePercentage('blocked-percent', '---');
@@ -1115,6 +1330,12 @@ function updateStatsCards(stats) {
updatePercentage('error-percent', '---');
}
// 更新历史数据
window.dashboardHistoryData.totalQueries = totalQueries;
window.dashboardHistoryData.blockedQueries = blockedQueries;
window.dashboardHistoryData.allowedQueries = allowedQueries;
window.dashboardHistoryData.errorQueries = errorQueries;
}

View File

@@ -280,45 +280,45 @@ async function getDomainInfo(domain) {
// 如果有URL属性直接检查域名
if (website.url) {
// 处理字符串类型的URL
if (typeof website.url === 'string') {
console.log(' 检查字符串URL:', website.url);
if (isDomainMatch(website.url, normalizedDomain, website.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: website.name,
icon: website.icon,
categoryId: website.categoryId,
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
company: website.company || companyName
};
}
if (typeof website.url === 'string') {
console.log(' 检查字符串URL:', website.url);
if (isDomainMatch(website.url, normalizedDomain, website.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: website.name,
icon: website.icon,
categoryId: website.categoryId,
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
company: website.company || companyName
};
}
// 处理对象类型的URL
else if (typeof website.url === 'object') {
console.log(' 检查对象类型URL包含', Object.keys(website.url).length, '个URL');
for (const urlKey in website.url) {
if (website.url.hasOwnProperty(urlKey)) {
const urlValue = website.url[urlKey];
console.log(' 检查URL', urlKey, ':', urlValue);
if (isDomainMatch(urlValue, normalizedDomain, website.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: website.name,
icon: website.icon,
categoryId: website.categoryId,
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
company: website.company || companyName
};
}
}
// 处理对象类型的URL
else if (typeof website.url === 'object') {
console.log(' 检查对象类型URL包含', Object.keys(website.url).length, '个URL');
for (const urlKey in website.url) {
if (website.url.hasOwnProperty(urlKey)) {
const urlValue = website.url[urlKey];
console.log(' 检查URL', urlKey, ':', urlValue);
if (isDomainMatch(urlValue, normalizedDomain, website.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: website.name,
icon: website.icon,
categoryId: website.categoryId,
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
company: website.company || companyName
};
}
}
}
}
} else if (typeof website === 'object' && website !== null) {
// 没有URL属性可能是嵌套的类别
console.log(' 发现嵌套类别,进一步检查');
for (const nestedWebsiteKey in website) {
if (website.hasOwnProperty(nestedWebsiteKey) && nestedWebsiteKey !== 'company') {
console.log(' 检查嵌套网站:', nestedWebsiteKey);
console.log(' 检查嵌套网站/类别:', nestedWebsiteKey);
const nestedWebsite = website[nestedWebsiteKey];
if (nestedWebsite.url) {
@@ -356,8 +356,54 @@ async function getDomainInfo(domain) {
}
}
}
} else if (typeof nestedWebsite === 'object' && nestedWebsite !== null) {
// 嵌套类别中的嵌套类别,递归检查
console.log(' 发现二级嵌套类别,进一步检查');
for (const secondNestedWebsiteKey in nestedWebsite) {
if (nestedWebsite.hasOwnProperty(secondNestedWebsiteKey) && secondNestedWebsiteKey !== 'company') {
console.log(' 检查二级嵌套网站:', secondNestedWebsiteKey);
const secondNestedWebsite = nestedWebsite[secondNestedWebsiteKey];
if (secondNestedWebsite.url) {
// 处理字符串类型的URL
if (typeof secondNestedWebsite.url === 'string') {
console.log(' 检查字符串URL:', secondNestedWebsite.url);
if (isDomainMatch(secondNestedWebsite.url, normalizedDomain, secondNestedWebsite.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: secondNestedWebsite.name,
icon: secondNestedWebsite.icon,
categoryId: secondNestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知',
company: secondNestedWebsite.company || companyName
};
}
}
// 处理对象类型的URL
else if (typeof secondNestedWebsite.url === 'object') {
console.log(' 检查对象类型URL包含', Object.keys(secondNestedWebsite.url).length, '个URL');
for (const urlKey in secondNestedWebsite.url) {
if (secondNestedWebsite.url.hasOwnProperty(urlKey)) {
const urlValue = secondNestedWebsite.url[urlKey];
console.log(' 检查URL', urlKey, ':', urlValue);
if (isDomainMatch(urlValue, normalizedDomain, secondNestedWebsite.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: secondNestedWebsite.name,
icon: secondNestedWebsite.icon,
categoryId: secondNestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知',
company: secondNestedWebsite.company || companyName
};
}
}
}
}
}
}
}
} else {
console.log(' 嵌套网站没有URL属性');
console.log(' 嵌套网站没有URL属性且不是对象类型');
}
}
}
@@ -1891,7 +1937,7 @@ async function showLogDetailModal(log) {
<span class="flex-grow">${domainInfo.categoryName || '未知'}</span>
</div>
<div class="flex items-center flex-wrap">
<span class="text-gray-500 mr-2">所属单位:</span>
<span class="text-gray-500 mr-2">所属单位/公司</span>
<span class="flex-grow">${domainInfo.company || '未知'}</span>
</div>
</div>

52
temp_config.json Normal file
View 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
}
}

261
test-domain-info.js Normal file
View File

@@ -0,0 +1,261 @@
// 测试脚本,用于调试 getDomainInfo 函数
const fs = require('fs');
const path = require('path');
// 模拟浏览器环境的 console.log
console.log = function() {
process.stdout.write(Array.from(arguments).join(' ') + '\n');
};
// 读取域名信息数据库
const domainInfoPath = path.join(__dirname, 'static/domain-info/domains/domain-info.json');
const domainInfoDatabase = JSON.parse(fs.readFileSync(domainInfoPath, 'utf8'));
// 模拟已加载的数据库
let domainInfoLoaded = true;
// 检查域名是否匹配
function isDomainMatch(urlValue, targetDomain, categoryId) {
console.log(' 开始匹配URL:', urlValue, '目标域名:', targetDomain, '类别ID:', categoryId);
// 规范化目标域名,去除末尾的点
const normalizedTargetDomain = targetDomain.replace(/\.$/, '').toLowerCase();
try {
// 尝试将URL值解析为完整URL
console.log(' 尝试解析URL为完整URL');
const url = new URL(urlValue);
let hostname = url.hostname.toLowerCase();
// 规范化主机名,去除末尾的点
hostname = hostname.replace(/\.$/, '');
console.log(' 解析成功,主机名:', hostname, '规范化目标域名:', normalizedTargetDomain);
// 根据类别ID选择匹配方式
if (categoryId === 2) {
// CDN类别使用域名后缀匹配
if (normalizedTargetDomain.endsWith('.' + hostname) || normalizedTargetDomain === hostname) {
console.log(' CDN域名后缀匹配成功');
return true;
} else {
console.log(' CDN域名后缀不匹配');
return false;
}
} else {
// 其他类别,使用完整域名匹配
if (hostname === normalizedTargetDomain) {
console.log(' 完整域名匹配成功');
return true;
} else {
console.log(' 完整域名不匹配');
return false;
}
}
} catch (e) {
console.log(' 解析URL失败将其视为纯域名处理错误信息:', e.message);
// 如果是纯域名而不是完整URL
let urlDomain = urlValue.toLowerCase();
// 规范化纯域名,去除末尾的点
urlDomain = urlDomain.replace(/\.$/, '');
console.log(' 处理为纯域名:', urlDomain, '规范化目标域名:', normalizedTargetDomain);
// 根据类别ID选择匹配方式
if (categoryId === 2) {
// CDN类别使用域名后缀匹配
if (normalizedTargetDomain.endsWith('.' + urlDomain) || normalizedTargetDomain === urlDomain) {
console.log(' CDN域名后缀匹配成功');
return true;
} else {
console.log(' CDN域名后缀不匹配');
return false;
}
} else {
// 其他类别,使用完整域名匹配
if (urlDomain === normalizedTargetDomain) {
console.log(' 完整域名匹配成功');
return true;
} else {
console.log(' 完整域名不匹配');
return false;
}
}
}
}
// 根据域名查找对应的网站信息
async function getDomainInfo(domain) {
console.log('开始查找域名信息,域名:', domain);
if (!domainInfoDatabase || !domainInfoDatabase.domains) {
console.error('域名信息数据库无效或为空');
return null;
}
// 规范化域名,移除可能的端口号
const normalizedDomain = domain.replace(/:\d+$/, '').toLowerCase();
console.log('规范化后的域名:', normalizedDomain);
// 遍历所有公司
console.log('开始遍历公司,总公司数:', Object.keys(domainInfoDatabase.domains).length);
for (const companyKey in domainInfoDatabase.domains) {
if (domainInfoDatabase.domains.hasOwnProperty(companyKey)) {
console.log('检查公司:', companyKey);
const companyData = domainInfoDatabase.domains[companyKey];
const companyName = companyData.company || companyKey;
// 遍历公司下的所有网站和类别
for (const websiteKey in companyData) {
if (companyData.hasOwnProperty(websiteKey) && websiteKey !== 'company') {
console.log(' 检查网站/类别:', websiteKey);
const website = companyData[websiteKey];
// 如果有URL属性直接检查域名
if (website.url) {
// 处理字符串类型的URL
if (typeof website.url === 'string') {
console.log(' 检查字符串URL:', website.url);
if (isDomainMatch(website.url, normalizedDomain, website.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: website.name,
icon: website.icon,
categoryId: website.categoryId,
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
company: website.company || companyName
};
}
}
// 处理对象类型的URL
else if (typeof website.url === 'object') {
console.log(' 检查对象类型URL包含', Object.keys(website.url).length, '个URL');
for (const urlKey in website.url) {
if (website.url.hasOwnProperty(urlKey)) {
const urlValue = website.url[urlKey];
console.log(' 检查URL', urlKey, ':', urlValue);
if (isDomainMatch(urlValue, normalizedDomain, website.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: website.name,
icon: website.icon,
categoryId: website.categoryId,
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
company: website.company || companyName
};
}
}
}
}
} else if (typeof website === 'object' && website !== null) {
// 没有URL属性可能是嵌套的类别
console.log(' 发现嵌套类别,进一步检查');
for (const nestedWebsiteKey in website) {
if (website.hasOwnProperty(nestedWebsiteKey) && nestedWebsiteKey !== 'company') {
console.log(' 检查嵌套网站/类别:', nestedWebsiteKey);
const nestedWebsite = website[nestedWebsiteKey];
if (nestedWebsite.url) {
// 处理字符串类型的URL
if (typeof nestedWebsite.url === 'string') {
console.log(' 检查字符串URL:', nestedWebsite.url);
if (isDomainMatch(nestedWebsite.url, normalizedDomain, nestedWebsite.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: nestedWebsite.name,
icon: nestedWebsite.icon,
categoryId: nestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[nestedWebsite.categoryId] || '未知',
company: nestedWebsite.company || companyName
};
}
}
// 处理对象类型的URL
else if (typeof nestedWebsite.url === 'object') {
console.log(' 检查对象类型URL包含', Object.keys(nestedWebsite.url).length, '个URL');
for (const urlKey in nestedWebsite.url) {
if (nestedWebsite.url.hasOwnProperty(urlKey)) {
const urlValue = nestedWebsite.url[urlKey];
console.log(' 检查URL', urlKey, ':', urlValue);
if (isDomainMatch(urlValue, normalizedDomain, nestedWebsite.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: nestedWebsite.name,
icon: nestedWebsite.icon,
categoryId: nestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[nestedWebsite.categoryId] || '未知',
company: nestedWebsite.company || companyName
};
}
}
}
}
} else if (typeof nestedWebsite === 'object' && nestedWebsite !== null) {
// 嵌套类别中的嵌套类别,递归检查
console.log(' 发现二级嵌套类别,进一步检查');
for (const secondNestedWebsiteKey in nestedWebsite) {
if (nestedWebsite.hasOwnProperty(secondNestedWebsiteKey) && secondNestedWebsiteKey !== 'company') {
console.log(' 检查二级嵌套网站:', secondNestedWebsiteKey);
const secondNestedWebsite = nestedWebsite[secondNestedWebsiteKey];
if (secondNestedWebsite.url) {
// 处理字符串类型的URL
if (typeof secondNestedWebsite.url === 'string') {
console.log(' 检查字符串URL:', secondNestedWebsite.url);
if (isDomainMatch(secondNestedWebsite.url, normalizedDomain, secondNestedWebsite.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: secondNestedWebsite.name,
icon: secondNestedWebsite.icon,
categoryId: secondNestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知',
company: secondNestedWebsite.company || companyName
};
}
}
// 处理对象类型的URL
else if (typeof secondNestedWebsite.url === 'object') {
console.log(' 检查对象类型URL包含', Object.keys(secondNestedWebsite.url).length, '个URL');
for (const urlKey in secondNestedWebsite.url) {
if (secondNestedWebsite.url.hasOwnProperty(urlKey)) {
const urlValue = secondNestedWebsite.url[urlKey];
console.log(' 检查URL', urlKey, ':', urlValue);
if (isDomainMatch(urlValue, normalizedDomain, secondNestedWebsite.categoryId)) {
console.log(' 匹配成功,返回网站信息');
return {
name: secondNestedWebsite.name,
icon: secondNestedWebsite.icon,
categoryId: secondNestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知',
company: secondNestedWebsite.company || companyName
};
}
}
}
}
}
}
}
} else {
console.log(' 嵌套网站没有URL属性且不是对象类型');
}
}
}
} else {
console.log(' 网站没有URL属性');
}
}
}
}
}
console.log('未找到匹配的域名信息');
return null;
}
// 测试 mcs.doubao.com
getDomainInfo('mcs.doubao.com').then(result => {
console.log('\n=== 测试结果 ===');
if (result) {
console.log('匹配成功:', JSON.stringify(result, null, 2));
} else {
console.log('匹配失败');
}
});