mirror of
https://github.com/coredns/coredns.git
synced 2026-06-02 15:20:23 -04:00
258 lines
6.9 KiB
Go
258 lines
6.9 KiB
Go
|
|
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)
|
||
|
|
}
|