// Package dnsreverse implements the dns_reverse module. // It enriches already-known hosts by performing reverse-DNS lookups // for every host that has no hostname recorded yet. package dnsreverse import ( "context" "fmt" "log/slog" "net" "strings" "time" "github.com/cereda-systems/nexus-scanner/internal/db" ) // Module enriches hosts in the database with reverse-DNS hostnames. type Module struct { site string store *db.Store log *slog.Logger } // New returns a new dns_reverse module. func New(site string, store *db.Store) *Module { return &Module{ site: site, store: store, log: slog.With("module", "dns_reverse"), } } func (m *Module) Name() string { return "dns_reverse" } func (m *Module) Interval() time.Duration { return 10 * time.Minute } // Run looks up hostnames for all hosts that have none recorded yet. func (m *Module) Run(ctx context.Context) error { scanID, err := m.store.BeginScan(m.Name()) if err != nil { return fmt.Errorf("begin scan: %w", err) } scanErr := m.scan(ctx) if endErr := m.store.EndScan(scanID, scanErr); endErr != nil { m.log.Error("record scan end", "err", endErr) } return scanErr } func (m *Module) scan(ctx context.Context) error { hosts, err := m.store.ListHosts("") if err != nil { return fmt.Errorf("list hosts: %w", err) } resolved := 0 for _, h := range hosts { // Skip hosts that already have a hostname. if h.Hostname != "" { continue } select { case <-ctx.Done(): return ctx.Err() default: } names, err := net.DefaultResolver.LookupAddr(ctx, h.IP) if err != nil { // A lookup failure is not fatal — continue with the next host. m.log.Debug("reverse lookup failed", "ip", h.IP, "err", err) continue } if len(names) == 0 { continue } // LookupAddr returns FQDNs with a trailing dot — strip it. hostname := strings.TrimSuffix(names[0], ".") h.Hostname = hostname if err := m.store.UpsertHost(h); err != nil { m.log.Error("upsert host", "ip", h.IP, "err", err) continue } m.log.Info("hostname resolved", "ip", h.IP, "hostname", hostname) resolved++ } m.log.Info("scan complete", "checked", len(hosts), "resolved", resolved) return nil }