dns.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package dns
  2. import (
  3. "context"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "net"
  9. "regexp"
  10. "slices"
  11. "strings"
  12. )
  13. var (
  14. DNSLookupAddr = net.LookupAddr
  15. DNSLookupHost = net.LookupHost
  16. )
  17. type Dns struct {
  18. cache *DnsCache
  19. ctx context.Context
  20. }
  21. func New(ctx context.Context, cache *DnsCache) *Dns {
  22. return &Dns{
  23. cache: cache,
  24. ctx: ctx,
  25. }
  26. }
  27. // ReverseDNS performs a reverse DNS lookup for the given IP address and trims the trailing dot from the results.
  28. func (d *Dns) ReverseDNS(addr string) ([]string, error) {
  29. slog.Debug("DNS: performing reverse lookup", "addr", addr)
  30. if cached, ok := d.getCachedReverse(addr); ok {
  31. return cached, nil
  32. }
  33. names, err := DNSLookupAddr(addr)
  34. if err != nil {
  35. if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
  36. slog.Debug("DNS: no PTR record found", "addr", addr)
  37. return []string{}, nil
  38. }
  39. slog.Error("DNS: reverse lookup failed", "addr", addr, "err", err)
  40. return nil, err
  41. }
  42. slog.Debug("DNS: reverse lookup successful", "addr", addr, "names", names)
  43. trimmedNames := make([]string, len(names))
  44. for i, name := range names {
  45. trimmedNames[i] = strings.TrimSuffix(name, ".")
  46. }
  47. d.reverseCachePut(addr, trimmedNames)
  48. return trimmedNames, nil
  49. }
  50. // LookupHost performs a forward DNS lookup for the given hostname.
  51. func (d *Dns) LookupHost(host string) ([]string, error) {
  52. slog.Debug("DNS: performing forward lookup", "host", host)
  53. if cached, ok := d.getCachedForward(host); ok {
  54. return cached, nil
  55. }
  56. addrs, err := DNSLookupHost(host)
  57. if err != nil {
  58. if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
  59. slog.Debug("DNS: no A/AAAA record found", "host", host)
  60. return []string{}, nil
  61. }
  62. slog.Error("DNS: forward lookup failed", "host", host, "err", err)
  63. return nil, err
  64. }
  65. slog.Debug("DNS: forward lookup successful", "host", host, "addrs", addrs)
  66. d.forwardCachePut(host, addrs)
  67. return addrs, nil
  68. }
  69. // verifyFCrDNSInternal performs the second half of the FCrDNS check, using a
  70. // pre-fetched list of names to perform the forward lookups.
  71. func (d *Dns) verifyFCrDNSInternal(addr string, names []string) bool {
  72. for _, name := range names {
  73. if cached, err := d.LookupHost(name); err == nil {
  74. if slices.Contains(cached, addr) {
  75. slog.Info("DNS: forward lookup confirmed original IP", "name", name, "addr", addr)
  76. return true
  77. }
  78. continue
  79. }
  80. }
  81. slog.Info("DNS: could not confirm original IP in forward lookups", "addr", addr)
  82. return false
  83. }
  84. // VerifyFCrDNS performs a forward-confirmed reverse DNS (FCrDNS) lookup for the given IP address,
  85. // optionally matching against a provided pattern.
  86. func (d *Dns) VerifyFCrDNS(addr string, pattern *string) bool {
  87. var patternVal string
  88. if pattern != nil {
  89. patternVal = *pattern
  90. }
  91. slog.Debug("DNS: performing FCrDNS lookup", "addr", addr, "pattern", patternVal)
  92. names, err := d.ReverseDNS(addr)
  93. if err != nil {
  94. return false
  95. }
  96. if len(names) == 0 {
  97. return pattern == nil // If no pattern specified, check is passed
  98. }
  99. // If a pattern is provided, check for a match.
  100. if pattern != nil {
  101. anyNameMatched := false
  102. for _, name := range names {
  103. matched, err := regexp.MatchString(*pattern, name)
  104. if err != nil {
  105. slog.Error("DNS: verifyFCrDNS invalid regex pattern", "err", err)
  106. return false // Invalid pattern is a failure.
  107. }
  108. if matched {
  109. anyNameMatched = true
  110. break
  111. }
  112. }
  113. if !anyNameMatched {
  114. slog.Debug("DNS: FCrDNS no PTR matches the pattern", "addr", addr, "pattern", *pattern)
  115. return false
  116. }
  117. slog.Debug("DNS: FCrDNS PTR matched pattern, proceeding with forward check", "addr", addr, "pattern", *pattern)
  118. }
  119. // If we're here, either there was no pattern, or the pattern matched.
  120. // Proceed with the forward lookup confirmation.
  121. return d.verifyFCrDNSInternal(addr, names)
  122. }
  123. // ArpaReverseIP performs translation from ip v4/v6 to arpa reverse notation
  124. func (d *Dns) ArpaReverseIP(addr string) (string, error) {
  125. ip := net.ParseIP(addr)
  126. if ip == nil {
  127. return addr, errors.New("invalid IP address")
  128. }
  129. if ipv4 := ip.To4(); ipv4 != nil {
  130. return fmt.Sprintf("%d.%d.%d.%d", ipv4[3], ipv4[2], ipv4[1], ipv4[0]), nil
  131. }
  132. ipv6 := ip.To16()
  133. if ipv6 == nil {
  134. return addr, errors.New("invalid IPv6 address")
  135. }
  136. hexBytes := make([]byte, hex.EncodedLen(len(ipv6)))
  137. hex.Encode(hexBytes, ipv6)
  138. var sb strings.Builder
  139. sb.Grow(len(hexBytes)*2 - 1)
  140. for i := len(hexBytes) - 1; i >= 0; i-- {
  141. sb.WriteByte(hexBytes[i])
  142. if i > 0 {
  143. sb.WriteByte('.')
  144. }
  145. }
  146. return sb.String(), nil
  147. }