增加更多匹配的域名信息
This commit is contained in:
79
.trae/documents/DNS服务器性能优化方案.md
Normal file
79
.trae/documents/DNS服务器性能优化方案.md
Normal 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解析的准确性和可靠性
|
||||
|
||||
@@ -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字段
|
||||
|
||||
|
||||
52
.trae/documents/plan_20251225_123636.md
Normal file
52
.trae/documents/plan_20251225_123636.md
Normal 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
|
||||
|
||||
42
.trae/documents/plan_20251225_125022.md
Normal file
42
.trae/documents/plan_20251225_125022.md
Normal 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查询失败的情况
|
||||
|
||||
53
.trae/documents/plan_20251225_130822.md
Normal file
53
.trae/documents/plan_20251225_130822.md
Normal 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查询点
|
||||
52
.trae/documents/plan_20251225_150849.md
Normal file
52
.trae/documents/plan_20251225_150849.md
Normal 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代码
|
||||
43
.trae/documents/plan_20251225_151740.md
Normal file
43
.trae/documents/plan_20251225_151740.md
Normal 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数据
|
||||
31
.trae/documents/plan_20251225_154403.md
Normal file
31
.trae/documents/plan_20251225_154403.md
Normal 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. 测试不同类型的解析记录,确保都能正确显示
|
||||
34
.trae/documents/plan_20251225_161050.md
Normal file
34
.trae/documents/plan_20251225_161050.md
Normal 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. 测试验证
|
||||
- 验证修改后的代码是否有语法错误
|
||||
- 验证查询日志详情弹窗是否正常显示
|
||||
- 验证屏蔽规则列已被成功移除
|
||||
- 验证其他功能是否正常工作
|
||||
56
.trae/documents/plan_20251226_133837.md
Normal file
56
.trae/documents/plan_20251226_133837.md
Normal 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. 测试验证
|
||||
|
||||
* 验证修改后的代码是否有语法错误
|
||||
|
||||
* 验证查询日志详情弹窗是否正常显示
|
||||
|
||||
* 验证屏蔽规则列已被成功移除
|
||||
|
||||
* 验证其他功能是否正常工作
|
||||
|
||||
51
.trae/documents/plan_20251226_142616.md
Normal file
51
.trae/documents/plan_20251226_142616.md
Normal 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查询响应时间是否明显减少
|
||||
* 验证是否解决了返回客户端超时的问题
|
||||
* 验证其他功能是否正常工作
|
||||
* 验证上游服务器状态跟踪是否准确
|
||||
78
.trae/documents/plan_20251228_100157.md
Normal file
78
.trae/documents/plan_20251228_100157.md
Normal 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查询响应时间是否明显减少
|
||||
|
||||
* 验证是否解决了返回客户端超时的问题
|
||||
|
||||
* 验证其他功能是否正常工作
|
||||
|
||||
* 验证上游服务器状态跟踪是否准确
|
||||
|
||||
20
.trae/documents/plan_20251229_142423.md
Normal file
20
.trae/documents/plan_20251229_142423.md
Normal 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)
|
||||
- 输出对应的公司名称(如北京百度网讯科技有限公司)
|
||||
33
.trae/documents/plan_20251229_175254.md
Normal file
33
.trae/documents/plan_20251229_175254.md
Normal 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将能够正确匹配到金山办公公司的金山文档,显示网站名称、图标、类别和所属公司。
|
||||
32
.trae/documents/plan_20260102_110215.md
Normal file
32
.trae/documents/plan_20260102_110215.md
Normal 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 服务器和本地服务器,所以不能删除这个函数
|
||||
- 确保修改后所有其他功能仍然正常工作
|
||||
32
.trae/documents/plan_20260102_110918.md
Normal file
32
.trae/documents/plan_20260102_110918.md
Normal 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相关规则,这些是广告过滤规则,不是代码
|
||||
- 确保所有修改都不会影响现有功能
|
||||
- 编译并测试修改后的代码,确保功能正常
|
||||
113
.trae/documents/plan_20260104_105942.md
Normal file
113
.trae/documents/plan_20260104_105942.md
Normal 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格式,提高了兼容性和灵活性。
|
||||
196
.trae/documents/plan_20260105_074926.md
Normal file
196
.trae/documents/plan_20260105_074926.md
Normal 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. 测试修复效果,验证平均响应时间计算是否正确
|
||||
46
.trae/documents/plan_20260114_150542.md
Normal file
46
.trae/documents/plan_20260114_150542.md
Normal 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. 测试验证
|
||||
86
.trae/documents/优化DNS服务器parallel模式响应时间.md
Normal file
86
.trae/documents/优化DNS服务器parallel模式响应时间.md
Normal 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. **不同配置测试**:测试不同超时时间和服务器数量的影响
|
||||
182
.trae/documents/修复DNS服务器CNAME处理和NXDOMAIN错误问题.md
Normal file
182
.trae/documents/修复DNS服务器CNAME处理和NXDOMAIN错误问题.md
Normal 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 错误的问题!
|
||||
58
.trae/documents/修复Web页面解析类型卡片百分比显示问题.md
Normal file
58
.trae/documents/修复Web页面解析类型卡片百分比显示问题.md
Normal 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查询逻辑,避免不必要的网络请求
|
||||
|
||||
* 保持代码的可读性和可维护性
|
||||
|
||||
* 确保与现有功能的兼容性
|
||||
|
||||
20
.trae/documents/修复picsum.photos域名匹配问题.md
Normal file
20
.trae/documents/修复picsum.photos域名匹配问题.md
Normal 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中的条目。
|
||||
56
.trae/documents/修复域名信息为空的问题.md
Normal file
56
.trae/documents/修复域名信息为空的问题.md
Normal 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`等已配置的域名。
|
||||
44
.trae/documents/修复拦截和放行功能.md
Normal file
44
.trae/documents/修复拦截和放行功能.md
Normal 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`的规则处理逻辑保持一致
|
||||
|
||||
56
.trae/documents/修复拦截放行功能.md
Normal file
56
.trae/documents/修复拦截放行功能.md
Normal 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路径
|
||||
|
||||
**预期效果**:
|
||||
|
||||
* 拦截功能正常工作,规则正确添加到本地规则列表
|
||||
|
||||
* 放行功能正常工作,规则正确添加到本地规则列表
|
||||
|
||||
* 日志状态立即更新,显示正确的拦截/放行状态
|
||||
|
||||
* 显示详细的操作反馈和错误信息
|
||||
|
||||
67
.trae/documents/修复日志显示undefined问题.md
Normal file
67
.trae/documents/修复日志显示undefined问题.md
Normal 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. 测试日志详情模态框,确保所有字段都能正确显示
|
||||
|
||||
21
.trae/documents/修复添加自定义规则后需要重启服务器的问题.md
Normal file
21
.trae/documents/修复添加自定义规则后需要重启服务器的问题.md
Normal 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缓存会被清空,新的规则会立即生效
|
||||
79
.trae/documents/修改gfwList配置从内容到文件路径.md
Normal file
79
.trae/documents/修改gfwList配置从内容到文件路径.md
Normal 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解析的准确性和可靠性
|
||||
|
||||
47
.trae/documents/内存泄露问题.md
Normal file
47
.trae/documents/内存泄露问题.md
Normal 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地理位置缓存泄漏和域名检查函数性能问题,这些是当前最严重的问题。
|
||||
69
.trae/documents/在查询日志详情界面增加操作列.md
Normal file
69
.trae/documents/在查询日志详情界面增加操作列.md
Normal 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调用的错误处理
|
||||
* 操作反馈机制的实现
|
||||
* 数据刷新逻辑
|
||||
* 保持界面样式一致性
|
||||
@@ -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查询逻辑,避免不必要的网络请求
|
||||
|
||||
* 保持代码的可读性和可维护性
|
||||
|
||||
* 确保与现有功能的兼容性
|
||||
|
||||
|
||||
31
.trae/documents/实现域名信息显示.md
Normal file
31
.trae/documents/实现域名信息显示.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 实现域名信息显示
|
||||
|
||||
## 需求分析
|
||||
用户需要在日志详情页面的红色框区域显示域名信息,包括:
|
||||
- 网站名称(带图标)
|
||||
- 网站类别
|
||||
- 所属公司
|
||||
|
||||
## 代码分析
|
||||
1. 目前代码已经包含了获取域名信息的功能:
|
||||
- `loadDomainInfoDatabase` 函数加载域名信息数据库
|
||||
- `getDomainInfo` 函数根据域名查找对应的网站信息
|
||||
- `showLogDetailModal` 函数显示日志详情,包括域名信息
|
||||
|
||||
2. 域名信息已经在日志详情模态框中显示,但需要确保它在指定的红色框区域正确显示
|
||||
|
||||
## 实现计划
|
||||
1. **优化域名信息显示样式**:调整 `showLogDetailModal` 函数中域名信息的HTML结构和样式,确保它在红色框内正确显示
|
||||
2. **增强域名匹配逻辑**:改进 `getDomainInfo` 函数,提高域名匹配的准确性
|
||||
3. **添加错误处理**:确保在域名信息加载失败时,页面能够优雅处理
|
||||
4. **测试功能**:验证域名信息能够正确显示在红色框区域
|
||||
|
||||
## 预期效果
|
||||
- 当日志详情弹窗打开时,在红色框区域显示域名的完整信息
|
||||
- 显示格式:
|
||||
```
|
||||
[图标] 网站名称
|
||||
类别: [类别名称]
|
||||
所属公司: [公司名称]
|
||||
```
|
||||
- 如果没有匹配的域名信息,显示"无"
|
||||
18
.trae/documents/实现客户端IP地址位置显示.md
Normal file
18
.trae/documents/实现客户端IP地址位置显示.md
Normal 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地址 (未知 未知)"
|
||||
81
.trae/documents/彻底解决DNS服务器Server Failed问题.md
Normal file
81
.trae/documents/彻底解决DNS服务器Server Failed问题.md
Normal 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)或NXDOMAIN(RcodeNameError)时才会被考虑作为最佳响应,且优先选择带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逻辑
|
||||
|
||||
38
.trae/documents/更新CHANGELOG.md记录最近的修复.md
Normal file
38
.trae/documents/更新CHANGELOG.md记录最近的修复.md
Normal 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文件的开头添加新的版本条目
|
||||
|
||||
* 使用### 修复标题记录修复的内容
|
||||
|
||||
* 清晰描述每个修复的问题和解决方案
|
||||
|
||||
46
.trae/documents/配置文件移除文件路径定义并固定文件位置.md
Normal file
46
.trae/documents/配置文件移除文件路径定义并固定文件位置.md
Normal 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`
|
||||
|
||||
399
.trae/documents/重写DNS日志详情弹窗.md
Normal file
399
.trae/documents/重写DNS日志详情弹窗.md
Normal 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);
|
||||
}
|
||||
}
|
||||
19
config.json
19
config.json
@@ -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
|
||||
|
||||
@@ -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
BIN
dns-server
Executable file
Binary file not shown.
243
dns/cache.go
243
dns/cache.go
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
697
dns/server.go
697
dns/server.go
File diff suppressed because it is too large
Load Diff
12
download.sh
Executable file
12
download.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e -f -u -x
|
||||
|
||||
# This script syncs companies DB that we bundle with AdGuard Home. The source
|
||||
# for this database is https://github.com/AdguardTeam/companiesdb.
|
||||
#
|
||||
trackers_url='https://raw.githubusercontent.com/AdguardTeam/companiesdb/main/dist/trackers.json'
|
||||
output='./trackers.json'
|
||||
readonly trackers_url output
|
||||
|
||||
curl -o "$output" -v "$trackers_url"
|
||||
241
gfw/manager.go
Normal file
241
gfw/manager.go
Normal 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
|
||||
}
|
||||
@@ -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
23
main.go
@@ -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)
|
||||
|
||||
@@ -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
3045
static/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/css/webfonts/fontawesome-webfont.eot
Normal file
BIN
static/css/webfonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
2671
static/css/webfonts/fontawesome-webfont.svg
Normal file
2671
static/css/webfonts/fontawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 434 KiB |
BIN
static/css/webfonts/fontawesome-webfont.woff
Normal file
BIN
static/css/webfonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
static/css/webfonts/fontawesome-webfont.woff2
Normal file
BIN
static/css/webfonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
52
temp_config.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"dns": {
|
||||
"port": 5353,
|
||||
"upstreamDNS": [
|
||||
"223.5.5.5:53",
|
||||
"223.6.6.6:53",
|
||||
"117.50.10.10:53",
|
||||
"10.35.10.200:53"
|
||||
],
|
||||
"dnssecUpstreamDNS": [
|
||||
"117.50.10.10:53",
|
||||
"101.226.4.6:53",
|
||||
"218.30.118.6:53",
|
||||
"208.67.220.220:53",
|
||||
"208.67.222.222:53"
|
||||
],
|
||||
"timeout": 5000,
|
||||
"statsFile": "data/stats.json",
|
||||
"saveInterval": 300,
|
||||
"cacheTTL": 30,
|
||||
"enableDNSSEC": true,
|
||||
"queryMode": "parallel",
|
||||
"domainSpecificDNS": {
|
||||
"amazehome.xyz": ["10.35.10.200:53"]
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"port": 8081,
|
||||
"host": "0.0.0.0",
|
||||
"enableAPI": true,
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
},
|
||||
"shield": {
|
||||
"localRulesFile": "data/rules.txt",
|
||||
"blacklists": [],
|
||||
"updateInterval": 3600,
|
||||
"hostsFile": "data/hosts.txt",
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"statsFile": "./data/shield_stats.json",
|
||||
"statsSaveInterval": 60,
|
||||
"remoteRulesCacheDir": "data/remote_rules"
|
||||
},
|
||||
"log": {
|
||||
"file": "logs/dns-server-5353.log",
|
||||
"level": "debug",
|
||||
"maxSize": 100,
|
||||
"maxBackups": 10,
|
||||
"maxAge": 30
|
||||
}
|
||||
}
|
||||
261
test-domain-info.js
Normal file
261
test-domain-info.js
Normal 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('匹配失败');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user