diff --git a/config.json b/config.json index 80a0ff9..ef1423d 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "dns": { - "port": 5353, + "port": 53, "upstreamDNS": [ "223.5.5.5:53", "223.6.6.6:53" @@ -115,4 +115,4 @@ "maxBackups": 10, "maxAge": 30 } -} \ No newline at end of file +} diff --git a/dns/server.go b/dns/server.go index 2cc9853..ae0a74c 100644 --- a/dns/server.go +++ b/dns/server.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net" + "net/http" "os" "path/filepath" "runtime" @@ -37,10 +38,18 @@ type ClientStats struct { LastSeen time.Time } +// IPGeolocation IP地理位置信息 +type IPGeolocation struct { + Country string `json:"country"` // 国家 + City string `json:"city"` // 城市 + Expiry time.Time `json:"expiry"` // 缓存过期时间 +} + // QueryLog 查询日志记录 type QueryLog struct { Timestamp time.Time // 查询时间 ClientIP string // 客户端IP + Location string // IP地理位置(国家 城市) Domain string // 查询域名 QueryType string // 查询类型 ResponseTime int64 // 响应时间(ms) @@ -93,6 +102,11 @@ type Server struct { saveDone chan struct{} // 用于通知保存协程停止 stopped bool // 服务器是否已经停止 stoppedMutex sync.Mutex // 保护stopped标志的互斥锁 + + // IP地理位置缓存 + ipGeolocationCache map[string]*IPGeolocation // IP地址到地理位置的映射 + ipGeolocationCacheMutex sync.RWMutex // 保护IP地理位置缓存的互斥锁 + ipGeolocationCacheTTL time.Duration // 缓存有效期 } // Stats DNS服务器统计信息 @@ -144,6 +158,9 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie maxQueryLogs: 10000, // 最大保存10000条日志 saveDone: make(chan struct{}), stopped: false, // 初始化为未停止状态 + // IP地理位置缓存初始化 + ipGeolocationCache: make(map[string]*IPGeolocation), + ipGeolocationCacheTTL: 24 * time.Hour, // 缓存有效期24小时 } // 加载已保存的统计数据 @@ -575,10 +592,14 @@ func (s *Server) updateStats(update func(*Stats)) { // addQueryLog 添加查询日志 func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string) { + // 获取IP地理位置 + location := s.getIpGeolocation(clientIP) + // 创建日志记录 log := QueryLog{ Timestamp: time.Now(), ClientIP: clientIP, + Location: location, Domain: domain, QueryType: queryType, ResponseTime: responseTime, @@ -907,6 +928,83 @@ func (s *Server) GetMonthlyStats() map[string]int64 { return result } +// getIpGeolocation 获取IP地址的地理位置信息 +func (s *Server) getIpGeolocation(ip string) string { + // 检查IP是否为本地地址 + if ip == "127.0.0.1" || ip == "::1" { + return "本地 本地" + } + + // 先检查缓存 + s.ipGeolocationCacheMutex.RLock() + geo, exists := s.ipGeolocationCache[ip] + s.ipGeolocationCacheMutex.RUnlock() + + // 如果缓存存在且未过期,直接返回 + if exists && time.Now().Before(geo.Expiry) { + return fmt.Sprintf("%s %s", geo.Country, geo.City) + } + + // 缓存不存在或已过期,从API获取 + geoInfo, err := s.fetchIpGeolocationFromAPI(ip) + if err != nil { + logger.Error("获取IP地理位置失败", "ip", ip, "error", err) + return "未知 未知" + } + + // 保存到缓存 + s.ipGeolocationCacheMutex.Lock() + s.ipGeolocationCache[ip] = &IPGeolocation{ + Country: geoInfo["country"].(string), + City: geoInfo["city"].(string), + Expiry: time.Now().Add(s.ipGeolocationCacheTTL), + } + s.ipGeolocationCacheMutex.Unlock() + + // 返回格式化的地理位置 + return fmt.Sprintf("%s %s", geoInfo["country"].(string), geoInfo["city"].(string)) +} + +// fetchIpGeolocationFromAPI 从第三方API获取IP地理位置信息 +func (s *Server) fetchIpGeolocationFromAPI(ip string) (map[string]interface{}, error) { + // 使用ip-api.com获取IP地理位置信息 + url := fmt.Sprintf("http://ip-api.com/json/%s?fields=country,city", ip) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // 读取响应内容 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // 解析JSON响应 + var result map[string]interface{} + err = json.Unmarshal(body, &result) + if err != nil { + return nil, err + } + + // 检查API返回状态 + status, ok := result["status"].(string) + if !ok || status != "success" { + return nil, fmt.Errorf("API返回错误状态: %v", result) + } + + // 确保国家和城市字段存在 + if _, ok := result["country"]; !ok { + result["country"] = "未知" + } + if _, ok := result["city"]; !ok { + result["city"] = "未知" + } + + return result, nil +} + // loadStatsData 从文件加载统计数据 func (s *Server) loadStatsData() { if s.config.StatsFile == "" { diff --git a/server.log b/server.log new file mode 100644 index 0000000..64307d1 --- /dev/null +++ b/server.log @@ -0,0 +1,41 @@ +2025/11/30 11:09:05 正在创建所需的文件和文件夹... +2025/11/30 11:09:05 所需文件和文件夹创建成功 +time="2025-11-30T11:09:05+08:00" level=debug msg="尝试加载Shield统计数据" file=/root/dnsbak/data/shield_stats.json +time="2025-11-30T11:09:05+08:00" level=info msg="Shield计数数据加载成功" blocked_entries=0 resolved_entries=0 +time="2025-11-30T11:09:05+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt" +time="2025-11-30T11:09:05+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/dsjh.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/anti-remoterequests.txt" +time="2025-11-30T11:09:07+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/url-based-adguard.txt" +time="2025-11-30T11:09:07+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt" +time="2025-11-30T11:09:08+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/malware.txt" +time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/costomize.txt" +time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/rules/AWAvenue-Ads-Rule.txt" +time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/cheat.txt" +time="2025-11-30T11:09:10+08:00" level=info msg="规则加载完成,域名规则: 189895, 排除规则: 653, 正则规则: 24094, hosts规则: 0" +time="2025-11-30T11:09:10+08:00" level=info msg="统计数据加载成功" +time="2025-11-30T11:09:10+08:00" level=info msg="查询日志加载成功" count=8608 +time="2025-11-30T11:09:10+08:00" level=info msg="DNS服务器已启动,监听端口: 5353" +time="2025-11-30T11:09:10+08:00" level=info msg="HTTP控制台已启动,监听端口: 8081" +time="2025-11-30T11:09:10+08:00" level=info msg="DNS TCP服务器启动,监听端口: 5353" +time="2025-11-30T11:09:10+08:00" level=info msg="启动Shield计数数据自动保存功能" file=./data/shield_stats.json interval=60 +time="2025-11-30T11:09:10+08:00" level=info msg="HTTP控制台服务器启动,监听地址: 0.0.0.0:8081" +time="2025-11-30T11:09:10+08:00" level=info msg="规则自动更新已启动" interval=3600 +time="2025-11-30T11:09:10+08:00" level=info msg="DNS UDP服务器启动,监听端口: 5353" +time="2025-11-30T11:09:10+08:00" level=info msg="启动统计数据自动保存功能" file=data/stats.json interval=300 +time="2025-11-30T11:09:10+08:00" level=error msg="DNS UDP服务器启动失败" error="listen udp :5353: bind: address already in use" +time="2025-11-30T11:09:10+08:00" level=info msg="Shield计数数据保存成功" blocked_entries=0 file=/root/dnsbak/data/shield_stats.json resolved_entries=0 +2025/11/30 11:09:18 正在关闭服务... +time="2025-11-30T11:09:18+08:00" level=info msg="统计数据保存成功" file=/root/dnsbak/data/stats.json +time="2025-11-30T11:09:18+08:00" level=info msg="查询日志保存成功" file=/root/dnsbak/data/querylog.json +time="2025-11-30T11:09:18+08:00" level=info msg="DNS服务器已停止" +time="2025-11-30T11:09:18+08:00" level=error msg="HTTP控制台服务器启动失败" error="http: Server closed" +time="2025-11-30T11:09:18+08:00" level=info msg="HTTP控制台服务器已停止" +time="2025-11-30T11:09:18+08:00" level=info msg="Shield计数数据保存成功" blocked_entries=0 file=/root/dnsbak/data/shield_stats.json resolved_entries=0 +time="2025-11-30T11:09:18+08:00" level=info msg="规则自动更新已停止" +2025/11/30 11:09:18 服务已关闭 +time="2025-11-30T11:09:18+08:00" level=warning msg="日志系统已关闭" diff --git a/static/index.html b/static/index.html index 10cb62b..399acab 100644 --- a/static/index.html +++ b/static/index.html @@ -942,14 +942,14 @@ 屏蔽规则 - - - - -
暂无查询日志
- - - + + + + +
暂无查询日志
+ + + diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 9e977d9..7504c06 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -6,8 +6,8 @@ let dnsRequestsChart = null; let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗) let queryTypeChart = null; // 解析类型统计饼图 let intervalId = null; -let wsConnection = null; -let wsReconnectTimer = null; +let dashboardWsConnection = null; +let dashboardWsReconnectTimer = null; // 存储统计卡片图表实例 let statCardCharts = {}; // 存储统计卡片历史数据 @@ -53,22 +53,22 @@ function connectWebSocket() { console.log('正在连接WebSocket:', wsUrl); // 创建WebSocket连接 - wsConnection = new WebSocket(wsUrl); + dashboardWsConnection = new WebSocket(wsUrl); // 连接打开事件 - wsConnection.onopen = function() { + dashboardWsConnection.onopen = function() { console.log('WebSocket连接已建立'); showNotification('数据更新成功', 'success'); // 清除重连计时器 - if (wsReconnectTimer) { - clearTimeout(wsReconnectTimer); - wsReconnectTimer = null; + if (dashboardWsReconnectTimer) { + clearTimeout(dashboardWsReconnectTimer); + dashboardWsReconnectTimer = null; } }; // 接收消息事件 - wsConnection.onmessage = function(event) { + dashboardWsConnection.onmessage = function(event) { try { const data = JSON.parse(event.data); @@ -82,16 +82,16 @@ function connectWebSocket() { }; // 连接关闭事件 - wsConnection.onclose = function(event) { + dashboardWsConnection.onclose = function(event) { console.warn('WebSocket连接已关闭,代码:', event.code); - wsConnection = null; + dashboardWsConnection = null; // 设置重连 setupReconnect(); }; // 连接错误事件 - wsConnection.onerror = function(error) { + dashboardWsConnection.onerror = function(error) { console.error('WebSocket连接错误:', error); }; @@ -104,14 +104,14 @@ function connectWebSocket() { // 设置重连逻辑 function setupReconnect() { - if (wsReconnectTimer) { + if (dashboardWsReconnectTimer) { return; // 已经有重连计时器在运行 } const reconnectDelay = 5000; // 5秒后重连 console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`); - wsReconnectTimer = setTimeout(() => { + dashboardWsReconnectTimer = setTimeout(() => { connectWebSocket(); }, reconnectDelay); } @@ -362,15 +362,15 @@ function fallbackToIntervalRefresh() { // 清理资源 function cleanupResources() { // 清除WebSocket连接 - if (wsConnection) { - wsConnection.close(); - wsConnection = null; + if (dashboardWsConnection) { + dashboardWsConnection.close(); + dashboardWsConnection = null; } // 清除重连计时器 - if (wsReconnectTimer) { - clearTimeout(wsReconnectTimer); - wsReconnectTimer = null; + if (dashboardWsReconnectTimer) { + clearTimeout(dashboardWsReconnectTimer); + dashboardWsReconnectTimer = null; } // 清除定时刷新 diff --git a/static/js/logs.js b/static/js/logs.js index 358e3ba..ac43872 100644 --- a/static/js/logs.js +++ b/static/js/logs.js @@ -10,6 +10,14 @@ let logsChart = null; let currentSortField = ''; let currentSortDirection = 'desc'; // 默认降序 +// IP地理位置缓存 +let ipGeolocationCache = {}; +const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时 + +// WebSocket连接和重连计时器 +let logsWsConnection = null; +let logsWsReconnectTimer = null; + // 初始化查询日志页面 function initLogsPage() { console.log('初始化查询日志页面'); @@ -36,15 +44,15 @@ function initLogsPage() { // 清理资源 function cleanupLogsResources() { // 清除WebSocket连接 - if (wsConnection) { - wsConnection.close(); - wsConnection = null; + if (logsWsConnection) { + logsWsConnection.close(); + logsWsConnection = null; } // 清除重连计时器 - if (wsReconnectTimer) { - clearTimeout(wsReconnectTimer); - wsReconnectTimer = null; + if (logsWsReconnectTimer) { + clearTimeout(logsWsReconnectTimer); + logsWsReconnectTimer = null; } } @@ -190,9 +198,14 @@ function updateSortIcons() { // 加载日志统计数据 function loadLogsStats() { - fetch('/api/logs/stats') - .then(response => response.json()) + // 使用封装的apiRequest函数进行API调用 + apiRequest('/logs/stats') .then(data => { + if (data && data.error) { + console.error('加载日志统计数据失败:', data.error); + return; + } + // 更新统计卡片 document.getElementById('logs-total-queries').textContent = data.totalQueries; document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms'; @@ -216,32 +229,50 @@ function loadLogs() { } // 构建请求URL - let url = `/api/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`; + let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`; // 添加过滤条件 if (currentFilter) { - url += `&result=${currentFilter}`; + endpoint += `&result=${currentFilter}`; } // 添加搜索条件 if (currentSearch) { - url += `&search=${encodeURIComponent(currentSearch)}`; + endpoint += `&search=${encodeURIComponent(currentSearch)}`; } // 添加排序条件 if (currentSortField) { - url += `&sort=${currentSortField}&direction=${currentSortDirection}`; + endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`; } - fetch(url) - .then(response => response.json()) + // 使用封装的apiRequest函数进行API调用 + apiRequest(endpoint) .then(data => { + if (data && data.error) { + console.error('加载日志详情失败:', data.error); + // 隐藏加载状态 + if (loadingEl) { + loadingEl.classList.add('hidden'); + } + return; + } + // 加载日志总数 - return fetch('/api/logs/count').then(response => response.json()).then(countData => { + return apiRequest('/logs/count').then(countData => { return { logs: data, count: countData.count }; }); }) .then(result => { + if (!result || !result.logs) { + console.error('加载日志详情失败: 无效的响应数据'); + // 隐藏加载状态 + if (loadingEl) { + loadingEl.classList.add('hidden'); + } + return; + } + const logs = result.logs; const totalLogs = result.count; @@ -358,7 +389,10 @@ function updateLogsTable(logs) {
${formattedTime}
${formattedDate}
- ${log.ClientIP} + +
${log.ClientIP}
+
${log.Location || '未知 未知'}
+
${log.Domain}
类型: ${log.QueryType}, ${statusText}
@@ -396,9 +430,13 @@ function initLogsChart() { if (!ctx) return; // 获取24小时统计数据 - fetch('/api/hourly-stats') - .then(response => response.json()) + apiRequest('/hourly-stats') .then(data => { + if (data && data.error) { + console.error('初始化日志图表失败:', data.error); + return; + } + // 创建图表 logsChart = new Chart(ctx, { type: 'line', @@ -446,24 +484,29 @@ function initLogsChart() { function updateLogsChart(range) { if (!logsChart) return; - let url = ''; + let endpoint = ''; switch (range) { case '24h': - url = '/api/hourly-stats'; + endpoint = '/hourly-stats'; break; case '7d': - url = '/api/daily-stats'; + endpoint = '/daily-stats'; break; case '30d': - url = '/api/monthly-stats'; + endpoint = '/monthly-stats'; break; default: - url = '/api/hourly-stats'; + endpoint = '/hourly-stats'; } - fetch(url) - .then(response => response.json()) + // 使用封装的apiRequest函数进行API调用 + apiRequest(endpoint) .then(data => { + if (data && data.error) { + console.error('更新日志图表失败:', data.error); + return; + } + // 更新图表数据 logsChart.data.labels = data.labels; logsChart.data.datasets[0].data = data.data; @@ -484,15 +527,15 @@ function connectLogsWebSocket() { console.log('正在连接WebSocket:', wsUrl); // 创建WebSocket连接 - wsConnection = new WebSocket(wsUrl); + logsWsConnection = new WebSocket(wsUrl); // 连接打开事件 - wsConnection.onopen = function() { + logsWsConnection.onopen = function() { console.log('WebSocket连接已建立'); }; // 接收消息事件 - wsConnection.onmessage = function(event) { + logsWsConnection.onmessage = function(event) { try { const data = JSON.parse(event.data); @@ -507,16 +550,16 @@ function connectLogsWebSocket() { }; // 连接关闭事件 - wsConnection.onclose = function(event) { + logsWsConnection.onclose = function(event) { console.warn('WebSocket连接已关闭,代码:', event.code); - wsConnection = null; + logsWsConnection = null; // 设置重连 setupLogsReconnect(); }; // 连接错误事件 - wsConnection.onerror = function(error) { + logsWsConnection.onerror = function(error) { console.error('WebSocket连接错误:', error); }; @@ -527,14 +570,14 @@ function connectLogsWebSocket() { // 设置重连逻辑 function setupLogsReconnect() { - if (wsReconnectTimer) { + if (logsWsReconnectTimer) { return; // 已经有重连计时器在运行 } const reconnectDelay = 5000; // 5秒后重连 console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`); - wsReconnectTimer = setTimeout(() => { + logsWsReconnectTimer = setTimeout(() => { connectLogsWebSocket(); }, reconnectDelay); } diff --git a/static/login.html b/static/login.html index c0ffa10..f2ea214 100644 --- a/static/login.html +++ b/static/login.html @@ -161,7 +161,11 @@ }) .then(response => { if (!response.ok) { - throw new Error('登录失败'); + if (response.status === 401) { + throw new Error('未知用户名或密码'); + } else { + throw new Error('登录失败'); + } } return response.json(); }) @@ -170,7 +174,11 @@ // 登录成功,重定向到主页 window.location.href = '/'; } else { - throw new Error(data.error || '登录失败'); + if (data.error === '用户名或密码错误') { + throw new Error('未知用户名或密码'); + } else { + throw new Error(data.error || '登录失败'); + } } }) .catch(error => {