From 2eb133dc7a2a7572c11bf6d83eb465098fd69a98 Mon Sep 17 00:00:00 2001 From: lirui Date: Sun, 4 Jan 2026 22:18:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0dns=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/nodeprobe/main.go | 65 +++++++++ internal/dns/dns.go | 320 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 internal/dns/dns.go diff --git a/cmd/nodeprobe/main.go b/cmd/nodeprobe/main.go index ecd1d34..258f9cb 100644 --- a/cmd/nodeprobe/main.go +++ b/cmd/nodeprobe/main.go @@ -8,6 +8,7 @@ import ( "syscall" "time" + "github.com/meowrain/nodeprobe/internal/dns" "github.com/meowrain/nodeprobe/internal/netstat" ) @@ -27,6 +28,21 @@ func main() { // 2. 设置刷新间隔(默认 1 秒) interval := 1 * time.Second + // 启动DNS服务器 + // 使用 Cloudflare DoH (也可以用 Google: https://dns.google/dns-query) + dnsServer := dns.NewDNSServerWithDoH(5353, "https://1.1.1.1/dns-query") + // 或者使用传统UDP: dnsServer := dns.NewDNSServer(5353, "8.8.8.8:53") + err = dnsServer.Start() + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to start DNS server: %v\n", err) + fmt.Println("Continuing without DNS monitoring...") + dnsServer = nil + } else { + defer dnsServer.Stop() + fmt.Println("Configure sing-box to use DNS: 127.0.0.1:5353") + time.Sleep(2 * time.Second) // 给用户时间看到DNS服务器启动信息 + } + // 3. 隐藏光标 fmt.Print("\033[?25l") defer fmt.Print("\033[?25h") // 退出时恢复光标 @@ -45,6 +61,9 @@ func main() { rates := netstat.CalculateRate(prevStats, currStats, interval) printHeader() printStats(rates, currStats) + if dnsServer != nil { + printDNSQueries(dnsServer) + } prevStats = currStats // 7. 主循环 @@ -64,12 +83,18 @@ func main() { moveCursorToTop() printHeader() printStats(rates, currStats) + if dnsServer != nil { + printDNSQueries(dnsServer) + } // 更新上一次的数据 prevStats = currStats case <-sigChan: fmt.Print("\033[?25h") // 恢复光标 + if dnsServer != nil { + dnsServer.Stop() + } fmt.Println("\nExiting...") return } @@ -135,6 +160,46 @@ func printStats(rates []netstat.TrafficRate, stats []netstat.InterfaceStats) { fmt.Print("\033[J") } +// printDNSQueries 显示DNS查询记录 +func printDNSQueries(dnsServer *dns.DNSServer) { + queries := dnsServer.GetRecentQueries(15) + + fmt.Println() + fmt.Println("DNS Queries from sing-box") + fmt.Println("=", strings.Repeat("=", 100)) + fmt.Printf("%-50s %-10s %-10s %-15s\n", "Domain", "Type", "Count", "Last Seen") + fmt.Println("-", strings.Repeat("-", 100)) + + if len(queries) == 0 { + fmt.Println(" No DNS queries yet. Waiting for sing-box traffic...") + } else { + for _, q := range queries { + domain := q.Domain + if len(domain) > 48 { + domain = domain[:45] + "..." + } + + elapsed := time.Since(q.Timestamp) + timeStr := "" + if elapsed < time.Minute { + timeStr = fmt.Sprintf("%ds ago", int(elapsed.Seconds())) + } else if elapsed < time.Hour { + timeStr = fmt.Sprintf("%dm ago", int(elapsed.Minutes())) + } else { + timeStr = fmt.Sprintf("%dh ago", int(elapsed.Hours())) + } + + fmt.Printf("%-50s %-10s %-10d %-15s\n", + domain, + q.QueryType, + q.Count, + timeStr, + ) + } + } + fmt.Println() +} + // 生成流量条形图 func makeTrafficBar(bps, maxBps float64, isDownload bool) string { // ANSI 颜色代码 diff --git a/internal/dns/dns.go b/internal/dns/dns.go new file mode 100644 index 0000000..9135ecb --- /dev/null +++ b/internal/dns/dns.go @@ -0,0 +1,320 @@ +package dns + +import ( + "bytes" + "crypto/tls" + "encoding/binary" + "fmt" + "io" + "net" + "net/http" + "sync" + "time" +) + +// DNSQuery DNS查询记录 +type DNSQuery struct { + Domain string + QueryType string + Timestamp time.Time + Count int +} + +type DNSServer struct { + Port int + UpstreamDNS string + UseDoH bool + DoHURL string + queries map[string]*DNSQuery + queriesMutex sync.RWMutex + conn *net.UDPConn + httpClient *http.Client +} + +// NewDNSServer 创建DNS服务器 +func NewDNSServer(port int, upstreamDNS string) *DNSServer { + return &DNSServer{ + Port: port, + UpstreamDNS: upstreamDNS, + UseDoH: false, + queries: make(map[string]*DNSQuery), + httpClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + }, + }, + } +} + +// NewDNSServerWithDoH 创建使用DoH的DNS服务器 +func NewDNSServerWithDoH(port int, dohURL string) *DNSServer { + return &DNSServer{ + Port: port, + UseDoH: true, + DoHURL: dohURL, + queries: make(map[string]*DNSQuery), + httpClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + }, + }, + } +} + +// Start 启动DNS服务器 +func (s *DNSServer) Start() error { + addr := net.UDPAddr{ + Port: s.Port, + IP: net.ParseIP("0.0.0.0"), + } + + conn, err := net.ListenUDP("udp", &addr) + if err != nil { + return fmt.Errorf("failed to start DNS server: %v", err) + } + + s.conn = conn + fmt.Printf("DNS Server started on 0.0.0.0:%d\n", s.Port) + if s.UseDoH { + fmt.Printf("Using DoH: %s\n", s.DoHURL) + } else { + fmt.Printf("Upstream DNS: %s\n", s.UpstreamDNS) + } + + go s.handleRequests() + return nil +} + +// Stop 停止DNS服务器 +func (s *DNSServer) Stop() { + if s.conn != nil { + s.conn.Close() + } +} + +// handleRequests 处理DNS请求 +func (s *DNSServer) handleRequests() { + buffer := make([]byte, 512) + + for { + n, clientAddr, err := s.conn.ReadFromUDP(buffer) + if err != nil { + continue + } + + // 解析DNS查询 + domain, queryType := s.parseDNSQuery(buffer[:n]) + if domain != "" { + s.recordQuery(domain, queryType) + } + + // 转发到上游DNS + go s.forwardQuery(buffer[:n], clientAddr) + } +} + +// forwardQuery 转发DNS查询到上游服务器 +func (s *DNSServer) forwardQuery(query []byte, clientAddr *net.UDPAddr) { + var response []byte + var err error + + if s.UseDoH { + response, err = s.forwardQueryDoH(query) + } else { + response, err = s.forwardQueryUDP(query) + } + + if err != nil { + return + } + + // 返回给客户端 + s.conn.WriteToUDP(response, clientAddr) +} + +// forwardQueryUDP 通过传统UDP转发DNS查询 +func (s *DNSServer) forwardQueryUDP(query []byte) ([]byte, error) { + // 连接上游DNS + upstreamAddr, err := net.ResolveUDPAddr("udp", s.UpstreamDNS) + if err != nil { + return nil, err + } + + upstreamConn, err := net.DialUDP("udp", nil, upstreamAddr) + if err != nil { + return nil, err + } + defer upstreamConn.Close() + + // 设置超时 + upstreamConn.SetDeadline(time.Now().Add(5 * time.Second)) + + // 发送查询 + _, err = upstreamConn.Write(query) + if err != nil { + return nil, err + } + + // 接收响应 + response := make([]byte, 512) + n, err := upstreamConn.Read(response) + if err != nil { + return nil, err + } + + return response[:n], nil +} + +// forwardQueryDoH 通过DoH转发DNS查询 +func (s *DNSServer) forwardQueryDoH(query []byte) ([]byte, error) { + // 创建HTTP POST请求 + req, err := http.NewRequest("POST", s.DoHURL, bytes.NewReader(query)) + if err != nil { + return nil, err + } + + // 设置DoH请求头 + req.Header.Set("Content-Type", "application/dns-message") + req.Header.Set("Accept", "application/dns-message") + + // 发送请求 + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("DoH server returned status %d", resp.StatusCode) + } + + // 读取响应 + response, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return response, nil +} + +// parseDNSQuery 解析DNS查询包 +func (s *DNSServer) parseDNSQuery(data []byte) (string, string) { + if len(data) < 12 { + return "", "" + } + + // DNS header is 12 bytes + // Questions start at byte 12 + offset := 12 + domain := "" + + for offset < len(data) { + length := int(data[offset]) + if length == 0 { + break + } + if length > 63 { + // Pointer, stop parsing + break + } + offset++ + if offset+length > len(data) { + break + } + if domain != "" { + domain += "." + } + domain += string(data[offset : offset+length]) + offset += length + } + + // Parse query type + offset++ // Skip null terminator + queryType := "A" + if offset+2 <= len(data) { + qtype := binary.BigEndian.Uint16(data[offset : offset+2]) + switch qtype { + case 1: + queryType = "A" + case 28: + queryType = "AAAA" + case 5: + queryType = "CNAME" + case 15: + queryType = "MX" + case 16: + queryType = "TXT" + default: + queryType = fmt.Sprintf("TYPE%d", qtype) + } + } + + return domain, queryType +} + +// recordQuery 记录DNS查询 +func (s *DNSServer) recordQuery(domain, queryType string) { + s.queriesMutex.Lock() + defer s.queriesMutex.Unlock() + + key := domain + if q, exists := s.queries[key]; exists { + q.Count++ + q.Timestamp = time.Now() + } else { + s.queries[key] = &DNSQuery{ + Domain: domain, + QueryType: queryType, + Timestamp: time.Now(), + Count: 1, + } + } + + // 限制缓存大小 + if len(s.queries) > 200 { + // 删除最旧的条目 + oldestKey := "" + oldestTime := time.Now() + for key, q := range s.queries { + if q.Timestamp.Before(oldestTime) { + oldestTime = q.Timestamp + oldestKey = key + } + } + if oldestKey != "" { + delete(s.queries, oldestKey) + } + } +} + +// GetRecentQueries 获取最近的DNS查询 +func (s *DNSServer) GetRecentQueries(limit int) []DNSQuery { + s.queriesMutex.RLock() + defer s.queriesMutex.RUnlock() + + queries := make([]DNSQuery, 0, len(s.queries)) + for _, q := range s.queries { + queries = append(queries, *q) + } + + // 按时间排序(最新的在前) + for i := 0; i < len(queries)-1; i++ { + for j := i + 1; j < len(queries); j++ { + if queries[j].Timestamp.After(queries[i].Timestamp) { + queries[i], queries[j] = queries[j], queries[i] + } + } + } + + if len(queries) > limit { + queries = queries[:limit] + } + + return queries +}