mirror of
https://github.com/coredns/coredns.git
synced 2026-06-02 15:20:23 -04:00
Signed-off-by: Dmytro Alieksieiev <1865999+dragoangel@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
33c71b1554
commit
ce0e5a6f39
257
plugin/forward/resolve.go
Normal file
257
plugin/forward/resolve.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package forward
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/parse"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// hostEntry represents a hostname-based TO address that needs DNS resolution.
|
||||
type hostEntry struct {
|
||||
hostname string // the hostname to resolve (e.g., "rbldnsd.rbldnsd.svc.cluster.local")
|
||||
port string // port (e.g., "53", "853")
|
||||
transport string // "dns" or "tls"
|
||||
zone string // TLS server name zone (from %zone syntax)
|
||||
}
|
||||
|
||||
// toEntry represents a single TO address from the config, preserving order.
|
||||
type toEntry struct {
|
||||
static bool // true for IP/file-based entries
|
||||
addrs []string // for static: resolved by HostPortOrFile
|
||||
entry hostEntry // for dynamic: hostname to resolve
|
||||
}
|
||||
|
||||
// classifyToAddrs processes TO addresses in order, returning an ordered list of
|
||||
// toEntries that preserves config ordering.
|
||||
func classifyToAddrs(toAddrs []string) ([]toEntry, error) {
|
||||
var entries []toEntry
|
||||
for _, h := range toAddrs {
|
||||
// Try HostPortOrFile first - this handles IPs and files
|
||||
hosts, parseErr := parse.HostPortOrFile(h)
|
||||
if parseErr == nil {
|
||||
entries = append(entries, toEntry{static: true, addrs: hosts})
|
||||
continue
|
||||
}
|
||||
|
||||
// Only fall through to hostname parsing if the error specifically
|
||||
// indicates the address is not an IP or file. Other errors (like
|
||||
// "no nameservers found" from file parsing) should be propagated.
|
||||
if !strings.Contains(parseErr.Error(), "not an IP address or file") {
|
||||
return nil, parseErr
|
||||
}
|
||||
|
||||
// Not an IP or file - check if it's a valid hostname
|
||||
entry, ok := parseAsHostEntry(h)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not an IP address, file, or valid domain: %q", h)
|
||||
}
|
||||
entries = append(entries, toEntry{static: false, entry: entry})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// parseAsHostEntry attempts to parse a TO address as a hostname-based entry.
|
||||
func parseAsHostEntry(h string) (hostEntry, bool) {
|
||||
cleanH, zone := splitZone(h)
|
||||
trans, host := parse.Transport(cleanH)
|
||||
|
||||
// Only dns and tls transports are supported for hostname resolution
|
||||
if trans != transport.DNS && trans != transport.TLS {
|
||||
return hostEntry{}, false
|
||||
}
|
||||
|
||||
hostname := host
|
||||
port := transport.Port
|
||||
if trans == transport.TLS {
|
||||
port = transport.TLSPort
|
||||
}
|
||||
|
||||
// Check if there's a port
|
||||
if h2, p, err := net.SplitHostPort(host); err == nil {
|
||||
hostname = h2
|
||||
port = p
|
||||
}
|
||||
|
||||
hostname = strings.Trim(hostname, "[]")
|
||||
|
||||
// Validate as domain name
|
||||
if _, ok := dns.IsDomainName(hostname); !ok || hostname == "" {
|
||||
return hostEntry{}, false
|
||||
}
|
||||
|
||||
// Make sure it's not actually an IP
|
||||
if net.ParseIP(hostname) != nil {
|
||||
return hostEntry{}, false
|
||||
}
|
||||
|
||||
return hostEntry{
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
transport: trans,
|
||||
zone: zone,
|
||||
}, true
|
||||
}
|
||||
|
||||
// expandAndDedup resolves all toEntries in order, expands hostnames to IPs,
|
||||
// and deduplicates by first-seen address. Returns the deduplicated address list.
|
||||
func expandAndDedup(entries []toEntry, resolvers []string) ([]string, error) {
|
||||
seen := make(map[string]bool)
|
||||
var result []string
|
||||
|
||||
for _, e := range entries {
|
||||
var addrs []string
|
||||
if e.static {
|
||||
addrs = e.addrs
|
||||
} else {
|
||||
resolved, err := resolveHostEntry(e.entry, resolvers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs = resolved
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
// Normalize the address for dedup comparison
|
||||
key := normalizeAddr(addr)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
result = append(result, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// normalizeAddr extracts the canonical IP:port from an address string
|
||||
// (stripping transport prefix and zone) for deduplication.
|
||||
func normalizeAddr(addr string) string {
|
||||
host, _ := splitZone(addr)
|
||||
_, h := parse.Transport(host)
|
||||
return h
|
||||
}
|
||||
|
||||
// resolveHostEntry resolves a single hostname entry and returns its addresses.
|
||||
func resolveHostEntry(entry hostEntry, resolvers []string) ([]string, error) {
|
||||
ips, err := lookupHost(entry.hostname, resolvers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %v", entry.hostname, err)
|
||||
}
|
||||
var addrs []string
|
||||
for _, ip := range ips {
|
||||
addrs = append(addrs, formatResolvedAddr(ip, entry.port, entry.transport, entry.zone))
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// formatResolvedAddr formats a resolved IP into an address string compatible
|
||||
// with the proxy creation code in parseStanza.
|
||||
func formatResolvedAddr(ip, port, trans, zone string) string {
|
||||
isIPv6 := strings.Contains(ip, ":")
|
||||
|
||||
switch trans {
|
||||
case transport.TLS:
|
||||
if zone != "" {
|
||||
if isIPv6 {
|
||||
return transport.TLS + "://[" + ip + "%" + zone + "]:" + port
|
||||
}
|
||||
return transport.TLS + "://" + ip + "%" + zone + ":" + port
|
||||
}
|
||||
return transport.TLS + "://" + net.JoinHostPort(ip, port)
|
||||
default: // transport.DNS
|
||||
return net.JoinHostPort(ip, port)
|
||||
}
|
||||
}
|
||||
|
||||
// lookupHost resolves a hostname to IP addresses using the specified resolvers.
|
||||
// If resolvers is empty, the system resolver (/etc/resolv.conf) is used.
|
||||
func lookupHost(hostname string, resolvers []string) ([]string, error) {
|
||||
if len(resolvers) == 0 {
|
||||
return systemLookup(hostname)
|
||||
}
|
||||
return dnsLookup(hostname, resolvers)
|
||||
}
|
||||
|
||||
// systemLookup resolves using the system resolver (/etc/resolv.conf).
|
||||
func systemLookup(hostname string) ([]string, error) {
|
||||
ips, err := net.LookupHost(hostname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("no addresses found for %q", hostname)
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// dnsLookup resolves a hostname using specific DNS resolver addresses.
|
||||
// Each resolver can be a bare IP (port 53 is assumed) or an IP:port pair.
|
||||
// It tries each resolver in order until one succeeds.
|
||||
func dnsLookup(hostname string, resolvers []string) ([]string, error) {
|
||||
c := new(dns.Client)
|
||||
c.ReadTimeout = 2 * time.Second
|
||||
c.WriteTimeout = 2 * time.Second
|
||||
|
||||
var lastErr error
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
resolverAddr := resolver
|
||||
if _, _, err := net.SplitHostPort(resolver); err != nil {
|
||||
resolverAddr = net.JoinHostPort(resolver, transport.Port)
|
||||
}
|
||||
var ips []string
|
||||
|
||||
// Try A records
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(hostname), dns.TypeA)
|
||||
m.RecursionDesired = true
|
||||
|
||||
r, _, err := c.Exchange(m, resolverAddr)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if r != nil {
|
||||
for _, ans := range r.Answer {
|
||||
if a, ok := ans.(*dns.A); ok {
|
||||
ips = append(ips, a.A.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try AAAA
|
||||
m = new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(hostname), dns.TypeAAAA)
|
||||
m.RecursionDesired = true
|
||||
|
||||
r, _, err = c.Exchange(m, resolverAddr)
|
||||
if err != nil {
|
||||
if len(ips) > 0 {
|
||||
return ips, nil // we have A records, AAAA failure is OK
|
||||
}
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if r != nil {
|
||||
for _, ans := range r.Answer {
|
||||
if aaaa, ok := ans.(*dns.AAAA); ok {
|
||||
ips = append(ips, aaaa.AAAA.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return nil, fmt.Errorf("no addresses found for %q: %v", hostname, lastErr)
|
||||
}
|
||||
return nil, fmt.Errorf("no addresses found for %q", hostname)
|
||||
}
|
||||
Reference in New Issue
Block a user