package resolver import ( "context" "fmt" "net" "net/netip" "strings" "time" ) func ParseStaticEntries(lines []string, logf func(string, ...any)) (entries [][3]string, skipped int) { for _, ln := range lines { s := strings.TrimSpace(ln) if s == "" || strings.HasPrefix(s, "#") { continue } comment := "" if idx := strings.Index(s, "#"); idx >= 0 { comment = strings.TrimSpace(s[idx+1:]) s = strings.TrimSpace(s[:idx]) } if s == "" || IsPrivateIPv4(s) { continue } rawBase := strings.SplitN(s, "/", 2)[0] if strings.Contains(s, "/") { if _, err := netip.ParsePrefix(s); err != nil { skipped++ if logf != nil { logf("static skip invalid prefix %q: %v", s, err) } continue } } else { if _, err := netip.ParseAddr(rawBase); err != nil { skipped++ if logf != nil { logf("static skip invalid ip %q: %v", s, err) } continue } } entries = append(entries, [3]string{s, rawBase, comment}) } return entries, skipped } func ResolveStaticLabels(entries [][3]string, dnsForPtr string, ptrCache map[string]any, ttl int, logf func(string, ...any)) (map[string][]string, int, int) { now := int(time.Now().Unix()) result := map[string][]string{} ptrLookups := 0 ptrErrors := 0 for _, e := range entries { ipEntry, baseIP, comment := e[0], e[1], e[2] var labels []string if comment != "" { labels = append(labels, "*"+comment) } if comment == "" { if cached, ok := ptrCache[baseIP].(map[string]any); ok { names, _ := cached["names"].([]any) last, _ := cached["last_resolved"].(float64) if len(names) > 0 && last > 0 && now-int(last) <= ttl { for _, n := range names { if s, ok := n.(string); ok && s != "" { labels = append(labels, "*"+s) } } } } if len(labels) == 0 { ptrLookups++ names, err := DigPTR(baseIP, dnsForPtr, 3*time.Second, logf) if err != nil { ptrErrors++ } if len(names) > 0 { ptrCache[baseIP] = map[string]any{"names": names, "last_resolved": now} for _, n := range names { labels = append(labels, "*"+n) } } } } if len(labels) == 0 { labels = []string{"*[STATIC-IP]"} } result[ipEntry] = labels if logf != nil { logf("static %s -> %v", ipEntry, labels) } } return result, ptrLookups, ptrErrors } func DigPTR(ip, upstream string, timeout time.Duration, logf func(string, ...any)) ([]string, error) { server, port := SplitDNS(upstream) if server == "" { return nil, fmt.Errorf("upstream empty") } if port == "" { port = "53" } addr := net.JoinHostPort(server, port) r := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{} return d.DialContext(ctx, "udp", addr) }, } ctx, cancel := context.WithTimeout(context.Background(), timeout) names, err := r.LookupAddr(ctx, ip) cancel() if err != nil { if logf != nil { logf("ptr error %s via %s: %v", ip, addr, err) } return nil, err } seen := map[string]struct{}{} var out []string for _, n := range names { n = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(n)), ".") if n == "" { continue } if _, ok := seen[n]; !ok { seen[n] = struct{}{} out = append(out, n) } } return out, nil }