|
@@ -1,6 +1,7 @@
|
|
|
package libnetwork
|
|
|
|
|
|
import (
|
|
|
+ "context"
|
|
|
"fmt"
|
|
|
"math/rand"
|
|
|
"net"
|
|
@@ -11,29 +12,10 @@ import (
|
|
|
"github.com/docker/docker/libnetwork/types"
|
|
|
"github.com/miekg/dns"
|
|
|
"github.com/sirupsen/logrus"
|
|
|
+ "golang.org/x/sync/semaphore"
|
|
|
+ "golang.org/x/time/rate"
|
|
|
)
|
|
|
|
|
|
-// Resolver represents the embedded DNS server in Docker. It operates
|
|
|
-// by listening on container's loopback interface for DNS queries.
|
|
|
-type Resolver interface {
|
|
|
- // Start starts the name server for the container
|
|
|
- Start() error
|
|
|
- // Stop stops the name server for the container. Stopped resolver
|
|
|
- // can be reused after running the SetupFunc again.
|
|
|
- Stop()
|
|
|
- // SetupFunc provides the setup function that should be run
|
|
|
- // in the container's network namespace.
|
|
|
- SetupFunc(int) func()
|
|
|
- // NameServer returns the IP of the DNS resolver for the
|
|
|
- // containers.
|
|
|
- NameServer() string
|
|
|
- // SetExtServers configures the external nameservers the resolver
|
|
|
- // should use to forward queries
|
|
|
- SetExtServers([]extDNSEntry)
|
|
|
- // ResolverOptions returns resolv.conf options that should be set
|
|
|
- ResolverOptions() []string
|
|
|
-}
|
|
|
-
|
|
|
// DNSBackend represents a backend DNS resolver used for DNS name
|
|
|
// resolution. All the queries to the resolver are forwarded to the
|
|
|
// backend resolver.
|
|
@@ -60,24 +42,25 @@ type DNSBackend interface {
|
|
|
}
|
|
|
|
|
|
const (
|
|
|
- dnsPort = "53"
|
|
|
- ptrIPv4domain = ".in-addr.arpa."
|
|
|
- ptrIPv6domain = ".ip6.arpa."
|
|
|
- respTTL = 600
|
|
|
- maxExtDNS = 3 // max number of external servers to try
|
|
|
- extIOTimeout = 4 * time.Second
|
|
|
- defaultRespSize = 512
|
|
|
- maxConcurrent = 1024
|
|
|
- logInterval = 2 * time.Second
|
|
|
+ dnsPort = "53"
|
|
|
+ ptrIPv4domain = ".in-addr.arpa."
|
|
|
+ ptrIPv6domain = ".ip6.arpa."
|
|
|
+ respTTL = 600
|
|
|
+ maxExtDNS = 3 // max number of external servers to try
|
|
|
+ extIOTimeout = 4 * time.Second
|
|
|
+ maxConcurrent = 1024
|
|
|
+ logInterval = 2 * time.Second
|
|
|
)
|
|
|
|
|
|
type extDNSEntry struct {
|
|
|
IPStr string
|
|
|
+ port uint16 // for testing
|
|
|
HostLoopback bool
|
|
|
}
|
|
|
|
|
|
-// resolver implements the Resolver interface
|
|
|
-type resolver struct {
|
|
|
+// Resolver is the embedded DNS server in Docker. It operates by listening on
|
|
|
+// the container's loopback interface for DNS queries.
|
|
|
+type Resolver struct {
|
|
|
backend DNSBackend
|
|
|
extDNSList [maxExtDNS]extDNSEntry
|
|
|
server *dns.Server
|
|
@@ -85,26 +68,30 @@ type resolver struct {
|
|
|
tcpServer *dns.Server
|
|
|
tcpListen *net.TCPListener
|
|
|
err error
|
|
|
- count int32
|
|
|
- tStamp time.Time
|
|
|
- queryLock sync.Mutex
|
|
|
listenAddress string
|
|
|
proxyDNS bool
|
|
|
startCh chan struct{}
|
|
|
+
|
|
|
+ fwdSem *semaphore.Weighted // Limit the number of concurrent external DNS requests in-flight
|
|
|
+ logInverval rate.Sometimes // Rate-limit logging about hitting the fwdSem limit
|
|
|
}
|
|
|
|
|
|
// NewResolver creates a new instance of the Resolver
|
|
|
-func NewResolver(address string, proxyDNS bool, backend DNSBackend) Resolver {
|
|
|
- return &resolver{
|
|
|
+func NewResolver(address string, proxyDNS bool, backend DNSBackend) *Resolver {
|
|
|
+ return &Resolver{
|
|
|
backend: backend,
|
|
|
proxyDNS: proxyDNS,
|
|
|
listenAddress: address,
|
|
|
err: fmt.Errorf("setup not done yet"),
|
|
|
startCh: make(chan struct{}, 1),
|
|
|
+ fwdSem: semaphore.NewWeighted(maxConcurrent),
|
|
|
+ logInverval: rate.Sometimes{Interval: logInterval},
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) SetupFunc(port int) func() {
|
|
|
+// SetupFunc returns the setup function that should be run in the container's
|
|
|
+// network namespace.
|
|
|
+func (r *Resolver) SetupFunc(port int) func() {
|
|
|
return func() {
|
|
|
var err error
|
|
|
|
|
@@ -135,7 +122,8 @@ func (r *resolver) SetupFunc(port int) func() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) Start() error {
|
|
|
+// Start starts the name server for the container.
|
|
|
+func (r *Resolver) Start() error {
|
|
|
r.startCh <- struct{}{}
|
|
|
defer func() { <-r.startCh }()
|
|
|
|
|
@@ -148,7 +136,7 @@ func (r *resolver) Start() error {
|
|
|
return fmt.Errorf("setting up IP table rules failed: %v", err)
|
|
|
}
|
|
|
|
|
|
- s := &dns.Server{Handler: r, PacketConn: r.conn}
|
|
|
+ s := &dns.Server{Handler: dns.HandlerFunc(r.serveDNS), PacketConn: r.conn}
|
|
|
r.server = s
|
|
|
go func() {
|
|
|
if err := s.ActivateAndServe(); err != nil {
|
|
@@ -156,7 +144,7 @@ func (r *resolver) Start() error {
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
- tcpServer := &dns.Server{Handler: r, Listener: r.tcpListen}
|
|
|
+ tcpServer := &dns.Server{Handler: dns.HandlerFunc(r.serveDNS), Listener: r.tcpListen}
|
|
|
r.tcpServer = tcpServer
|
|
|
go func() {
|
|
|
if err := tcpServer.ActivateAndServe(); err != nil {
|
|
@@ -166,7 +154,9 @@ func (r *resolver) Start() error {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) Stop() {
|
|
|
+// Stop stops the name server for the container. A stopped resolver can be
|
|
|
+// reused after running the SetupFunc again.
|
|
|
+func (r *Resolver) Stop() {
|
|
|
r.startCh <- struct{}{}
|
|
|
defer func() { <-r.startCh }()
|
|
|
|
|
@@ -179,12 +169,12 @@ func (r *resolver) Stop() {
|
|
|
r.conn = nil
|
|
|
r.tcpServer = nil
|
|
|
r.err = fmt.Errorf("setup not done yet")
|
|
|
- r.tStamp = time.Time{}
|
|
|
- r.count = 0
|
|
|
- r.queryLock = sync.Mutex{}
|
|
|
+ r.fwdSem = semaphore.NewWeighted(maxConcurrent)
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) SetExtServers(extDNS []extDNSEntry) {
|
|
|
+// SetExtServers configures the external nameservers the resolver should use
|
|
|
+// when forwarding queries.
|
|
|
+func (r *Resolver) SetExtServers(extDNS []extDNSEntry) {
|
|
|
l := len(extDNS)
|
|
|
if l > maxExtDNS {
|
|
|
l = maxExtDNS
|
|
@@ -194,11 +184,13 @@ func (r *resolver) SetExtServers(extDNS []extDNSEntry) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) NameServer() string {
|
|
|
+// NameServer returns the IP of the DNS resolver for the containers.
|
|
|
+func (r *Resolver) NameServer() string {
|
|
|
return r.listenAddress
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) ResolverOptions() []string {
|
|
|
+// ResolverOptions returns resolv.conf options that should be set.
|
|
|
+func (r *Resolver) ResolverOptions() []string {
|
|
|
return []string{"ndots:0"}
|
|
|
}
|
|
|
|
|
@@ -230,7 +222,7 @@ func createRespMsg(query *dns.Msg) *dns.Msg {
|
|
|
return resp
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) handleMXQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
+func (r *Resolver) handleMXQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
name := query.Question[0].Name
|
|
|
addrv4, _ := r.backend.ResolveName(name, types.IPv4)
|
|
|
addrv6, _ := r.backend.ResolveName(name, types.IPv6)
|
|
@@ -247,7 +239,7 @@ func (r *resolver) handleMXQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
return resp, nil
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) handleIPQuery(query *dns.Msg, ipType int) (*dns.Msg, error) {
|
|
|
+func (r *Resolver) handleIPQuery(query *dns.Msg, ipType int) (*dns.Msg, error) {
|
|
|
var (
|
|
|
addr []net.IP
|
|
|
ipv6Miss bool
|
|
@@ -289,27 +281,24 @@ func (r *resolver) handleIPQuery(query *dns.Msg, ipType int) (*dns.Msg, error) {
|
|
|
return resp, nil
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) handlePTRQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
- var (
|
|
|
- parts []string
|
|
|
- ptr = query.Question[0].Name
|
|
|
- )
|
|
|
-
|
|
|
- if strings.HasSuffix(ptr, ptrIPv4domain) {
|
|
|
- parts = strings.Split(ptr, ptrIPv4domain)
|
|
|
- } else if strings.HasSuffix(ptr, ptrIPv6domain) {
|
|
|
- parts = strings.Split(ptr, ptrIPv6domain)
|
|
|
- } else {
|
|
|
- return nil, fmt.Errorf("invalid PTR query, %v", ptr)
|
|
|
+func (r *Resolver) handlePTRQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
+ ptr := query.Question[0].Name
|
|
|
+ name, after, found := strings.Cut(ptr, ptrIPv4domain)
|
|
|
+ if !found || after != "" {
|
|
|
+ name, after, found = strings.Cut(ptr, ptrIPv6domain)
|
|
|
+ }
|
|
|
+ if !found || after != "" {
|
|
|
+ // Not a known IPv4 or IPv6 PTR domain.
|
|
|
+ // Maybe the external DNS servers know what to do with the query?
|
|
|
+ return nil, nil
|
|
|
}
|
|
|
|
|
|
- host := r.backend.ResolveIP(parts[0])
|
|
|
-
|
|
|
- if len(host) == 0 {
|
|
|
+ host := r.backend.ResolveIP(name)
|
|
|
+ if host == "" {
|
|
|
return nil, nil
|
|
|
}
|
|
|
|
|
|
- logrus.Debugf("[resolver] lookup for IP %s: name %s", parts[0], host)
|
|
|
+ logrus.Debugf("[resolver] lookup for IP %s: name %s", name, host)
|
|
|
fqdn := dns.Fqdn(host)
|
|
|
|
|
|
resp := new(dns.Msg)
|
|
@@ -323,7 +312,7 @@ func (r *resolver) handlePTRQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
return resp, nil
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) handleSRVQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
+func (r *Resolver) handleSRVQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
svc := query.Question[0].Name
|
|
|
srv, ip := r.backend.ResolveService(svc)
|
|
|
|
|
@@ -351,28 +340,10 @@ func (r *resolver) handleSRVQuery(query *dns.Msg) (*dns.Msg, error) {
|
|
|
return resp, nil
|
|
|
}
|
|
|
|
|
|
-func truncateResp(resp *dns.Msg, maxSize int, isTCP bool) {
|
|
|
- if !isTCP {
|
|
|
- resp.Truncated = true
|
|
|
- }
|
|
|
-
|
|
|
- srv := resp.Question[0].Qtype == dns.TypeSRV
|
|
|
- // trim the Answer RRs one by one till the whole message fits
|
|
|
- // within the reply size
|
|
|
- for resp.Len() > maxSize {
|
|
|
- resp.Answer = resp.Answer[:len(resp.Answer)-1]
|
|
|
-
|
|
|
- if srv && len(resp.Extra) > 0 {
|
|
|
- resp.Extra = resp.Extra[:len(resp.Extra)-1]
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (r *resolver) ServeDNS(w dns.ResponseWriter, query *dns.Msg) {
|
|
|
+func (r *Resolver) serveDNS(w dns.ResponseWriter, query *dns.Msg) {
|
|
|
var (
|
|
|
- extConn net.Conn
|
|
|
- resp *dns.Msg
|
|
|
- err error
|
|
|
+ resp *dns.Msg
|
|
|
+ err error
|
|
|
)
|
|
|
|
|
|
if query == nil || len(query.Question) == 0 {
|
|
@@ -397,199 +368,200 @@ func (r *resolver) ServeDNS(w dns.ResponseWriter, query *dns.Msg) {
|
|
|
logrus.Debugf("[resolver] query type %s is not supported by the embedded DNS and will be forwarded to external DNS", dns.TypeToString[queryType])
|
|
|
}
|
|
|
|
|
|
+ reply := func(msg *dns.Msg) {
|
|
|
+ if err = w.WriteMsg(msg); err != nil {
|
|
|
+ logrus.WithError(err).Errorf("[resolver] failed to write response")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if err != nil {
|
|
|
logrus.WithError(err).Errorf("[resolver] failed to handle query: %s (%s)", queryName, dns.TypeToString[queryType])
|
|
|
+ reply(new(dns.Msg).SetRcode(query, dns.RcodeServerFailure))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if resp == nil {
|
|
|
- // If the backend doesn't support proxying dns request
|
|
|
- // fail the response
|
|
|
- if !r.proxyDNS {
|
|
|
- resp = new(dns.Msg)
|
|
|
- resp.SetRcode(query, dns.RcodeServerFailure)
|
|
|
- if err := w.WriteMsg(resp); err != nil {
|
|
|
- logrus.WithError(err).Error("[resolver] error writing dns response")
|
|
|
+ if resp != nil {
|
|
|
+ // We are the authoritative DNS server for this request so it's
|
|
|
+ // on us to truncate the response message to the size limit
|
|
|
+ // negotiated by the client.
|
|
|
+ maxSize := dns.MinMsgSize
|
|
|
+ if w.LocalAddr().Network() == "tcp" {
|
|
|
+ maxSize = dns.MaxMsgSize
|
|
|
+ } else {
|
|
|
+ if optRR := query.IsEdns0(); optRR != nil {
|
|
|
+ if udpsize := int(optRR.UDPSize()); udpsize > maxSize {
|
|
|
+ maxSize = udpsize
|
|
|
+ }
|
|
|
}
|
|
|
- return
|
|
|
}
|
|
|
+ resp.Truncate(maxSize)
|
|
|
+ reply(resp)
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
+ if r.proxyDNS {
|
|
|
// If the user sets ndots > 0 explicitly and the query is
|
|
|
// in the root domain don't forward it out. We will return
|
|
|
// failure and let the client retry with the search domain
|
|
|
- // attached
|
|
|
- switch queryType {
|
|
|
- case dns.TypeA, dns.TypeAAAA:
|
|
|
- if r.backend.NdotsSet() && !strings.Contains(strings.TrimSuffix(queryName, "."), ".") {
|
|
|
- resp = createRespMsg(query)
|
|
|
- }
|
|
|
+ // attached.
|
|
|
+ if (queryType == dns.TypeA || queryType == dns.TypeAAAA) && r.backend.NdotsSet() &&
|
|
|
+ !strings.Contains(strings.TrimSuffix(queryName, "."), ".") {
|
|
|
+ resp = createRespMsg(query)
|
|
|
+ } else {
|
|
|
+ resp = r.forwardExtDNS(w.LocalAddr().Network(), query)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- proto := w.LocalAddr().Network()
|
|
|
- maxSize := 0
|
|
|
- if proto == "tcp" {
|
|
|
- maxSize = dns.MaxMsgSize - 1
|
|
|
- } else if proto == "udp" {
|
|
|
- optRR := query.IsEdns0()
|
|
|
- if optRR != nil {
|
|
|
- maxSize = int(optRR.UDPSize())
|
|
|
- }
|
|
|
- if maxSize < defaultRespSize {
|
|
|
- maxSize = defaultRespSize
|
|
|
- }
|
|
|
+ if resp == nil {
|
|
|
+ // We were unable to get an answer from any of the upstream DNS
|
|
|
+ // servers or the backend doesn't support proxying DNS requests.
|
|
|
+ resp = new(dns.Msg).SetRcode(query, dns.RcodeServerFailure)
|
|
|
}
|
|
|
+ reply(resp)
|
|
|
+}
|
|
|
|
|
|
- if resp != nil {
|
|
|
- if resp.Len() > maxSize {
|
|
|
- truncateResp(resp, maxSize, proto == "tcp")
|
|
|
+func (r *Resolver) dialExtDNS(proto string, server extDNSEntry) (net.Conn, error) {
|
|
|
+ var (
|
|
|
+ extConn net.Conn
|
|
|
+ dialErr error
|
|
|
+ )
|
|
|
+ extConnect := func() {
|
|
|
+ if server.port == 0 {
|
|
|
+ server.port = 53
|
|
|
}
|
|
|
- } else {
|
|
|
- for i := 0; i < maxExtDNS; i++ {
|
|
|
- extDNS := &r.extDNSList[i]
|
|
|
- if extDNS.IPStr == "" {
|
|
|
- break
|
|
|
- }
|
|
|
- extConnect := func() {
|
|
|
- addr := fmt.Sprintf("%s:%d", extDNS.IPStr, 53)
|
|
|
- extConn, err = net.DialTimeout(proto, addr, extIOTimeout)
|
|
|
- }
|
|
|
+ addr := fmt.Sprintf("%s:%d", server.IPStr, server.port)
|
|
|
+ extConn, dialErr = net.DialTimeout(proto, addr, extIOTimeout)
|
|
|
+ }
|
|
|
|
|
|
- if extDNS.HostLoopback {
|
|
|
- extConnect()
|
|
|
- } else {
|
|
|
- execErr := r.backend.ExecFunc(extConnect)
|
|
|
- if execErr != nil {
|
|
|
- logrus.Warn(execErr)
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
- if err != nil {
|
|
|
- logrus.WithField("retries", i).Warnf("[resolver] connect failed: %s", err)
|
|
|
- continue
|
|
|
- }
|
|
|
- logrus.Debugf("[resolver] query %s (%s) from %s, forwarding to %s:%s", queryName, dns.TypeToString[queryType],
|
|
|
- extConn.LocalAddr().String(), proto, extDNS.IPStr)
|
|
|
+ if server.HostLoopback {
|
|
|
+ extConnect()
|
|
|
+ } else {
|
|
|
+ execErr := r.backend.ExecFunc(extConnect)
|
|
|
+ if execErr != nil {
|
|
|
+ return nil, execErr
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if dialErr != nil {
|
|
|
+ return nil, dialErr
|
|
|
+ }
|
|
|
|
|
|
- // Timeout has to be set for every IO operation.
|
|
|
- if err := extConn.SetDeadline(time.Now().Add(extIOTimeout)); err != nil {
|
|
|
- logrus.WithError(err).Error("[resolver] error setting conn deadline")
|
|
|
- }
|
|
|
- co := &dns.Conn{
|
|
|
- Conn: extConn,
|
|
|
- UDPSize: uint16(maxSize),
|
|
|
- }
|
|
|
- defer co.Close()
|
|
|
-
|
|
|
- // limits the number of outstanding concurrent queries.
|
|
|
- if !r.forwardQueryStart() {
|
|
|
- old := r.tStamp
|
|
|
- r.tStamp = time.Now()
|
|
|
- if r.tStamp.Sub(old) > logInterval {
|
|
|
- logrus.Errorf("[resolver] more than %v concurrent queries from %s", maxConcurrent, extConn.LocalAddr().String())
|
|
|
- }
|
|
|
- continue
|
|
|
- }
|
|
|
+ return extConn, nil
|
|
|
+}
|
|
|
|
|
|
- err = co.WriteMsg(query)
|
|
|
- if err != nil {
|
|
|
- r.forwardQueryEnd()
|
|
|
- logrus.Debugf("[resolver] send to DNS server failed, %s", err)
|
|
|
- continue
|
|
|
- }
|
|
|
+func (r *Resolver) forwardExtDNS(proto string, query *dns.Msg) *dns.Msg {
|
|
|
+ queryName, queryType := query.Question[0].Name, query.Question[0].Qtype
|
|
|
+ for _, extDNS := range r.extDNSList {
|
|
|
+ if extDNS.IPStr == "" {
|
|
|
+ break
|
|
|
+ }
|
|
|
|
|
|
- resp, err = co.ReadMsg()
|
|
|
- // Truncated DNS replies should be sent to the client so that the
|
|
|
- // client can retry over TCP
|
|
|
- if err != nil && (resp == nil || !resp.Truncated) {
|
|
|
- r.forwardQueryEnd()
|
|
|
- logrus.WithError(err).Warnf("[resolver] failed to read from DNS server: %s, query: %s", extConn.RemoteAddr().String(), query.Question[0].String())
|
|
|
- continue
|
|
|
- }
|
|
|
- r.forwardQueryEnd()
|
|
|
+ // limits the number of outstanding concurrent queries.
|
|
|
+ ctx, cancel := context.WithTimeout(context.Background(), extIOTimeout)
|
|
|
+ err := r.fwdSem.Acquire(ctx, 1)
|
|
|
+ cancel()
|
|
|
+ if err != nil {
|
|
|
+ r.logInverval.Do(func() {
|
|
|
+ logrus.Errorf("[resolver] more than %v concurrent queries", maxConcurrent)
|
|
|
+ })
|
|
|
+ return new(dns.Msg).SetRcode(query, dns.RcodeRefused)
|
|
|
+ }
|
|
|
+ resp := func() *dns.Msg {
|
|
|
+ defer r.fwdSem.Release(1)
|
|
|
+ return r.exchange(proto, extDNS, query)
|
|
|
+ }()
|
|
|
+ if resp == nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
|
|
|
- if resp == nil {
|
|
|
- logrus.Debugf("[resolver] external DNS %s:%s returned empty response for %q", proto, extDNS.IPStr, queryName)
|
|
|
+ switch resp.Rcode {
|
|
|
+ case dns.RcodeServerFailure, dns.RcodeRefused:
|
|
|
+ // Server returned FAILURE: continue with the next external DNS server
|
|
|
+ // Server returned REFUSED: this can be a transitional status, so continue with the next external DNS server
|
|
|
+ logrus.Debugf("[resolver] external DNS %s:%s responded with %s for %q", proto, extDNS.IPStr, statusString(resp.Rcode), queryName)
|
|
|
+ continue
|
|
|
+ case dns.RcodeNameError:
|
|
|
+ // Server returned NXDOMAIN. Stop resolution if it's an authoritative answer (see RFC 8020: https://tools.ietf.org/html/rfc8020#section-2)
|
|
|
+ logrus.Debugf("[resolver] external DNS %s:%s responded with %s for %q", proto, extDNS.IPStr, statusString(resp.Rcode), queryName)
|
|
|
+ if resp.Authoritative {
|
|
|
break
|
|
|
}
|
|
|
- switch resp.Rcode {
|
|
|
- case dns.RcodeServerFailure, dns.RcodeRefused:
|
|
|
- // Server returned FAILURE: continue with the next external DNS server
|
|
|
- // Server returned REFUSED: this can be a transitional status, so continue with the next external DNS server
|
|
|
- logrus.Debugf("[resolver] external DNS %s:%s responded with %s for %q", proto, extDNS.IPStr, statusString(resp.Rcode), queryName)
|
|
|
- continue
|
|
|
- case dns.RcodeNameError:
|
|
|
- // Server returned NXDOMAIN. Stop resolution if it's an authoritative answer (see RFC 8020: https://tools.ietf.org/html/rfc8020#section-2)
|
|
|
- logrus.Debugf("[resolver] external DNS %s:%s responded with %s for %q", proto, extDNS.IPStr, statusString(resp.Rcode), queryName)
|
|
|
- if resp.Authoritative {
|
|
|
- break
|
|
|
- }
|
|
|
- continue
|
|
|
- case dns.RcodeSuccess:
|
|
|
- // All is well
|
|
|
- default:
|
|
|
- // Server gave some error. Log the error, and continue with the next external DNS server
|
|
|
- logrus.Debugf("[resolver] external DNS %s:%s responded with %s (code %d) for %q", proto, extDNS.IPStr, statusString(resp.Rcode), resp.Rcode, queryName)
|
|
|
- continue
|
|
|
- }
|
|
|
- answers := 0
|
|
|
- for _, rr := range resp.Answer {
|
|
|
- h := rr.Header()
|
|
|
- switch h.Rrtype {
|
|
|
- case dns.TypeA:
|
|
|
- answers++
|
|
|
- ip := rr.(*dns.A).A
|
|
|
- logrus.Debugf("[resolver] received A record %q for %q from %s:%s", ip, h.Name, proto, extDNS.IPStr)
|
|
|
- r.backend.HandleQueryResp(h.Name, ip)
|
|
|
- case dns.TypeAAAA:
|
|
|
- answers++
|
|
|
- ip := rr.(*dns.AAAA).AAAA
|
|
|
- logrus.Debugf("[resolver] received AAAA record %q for %q from %s:%s", ip, h.Name, proto, extDNS.IPStr)
|
|
|
- r.backend.HandleQueryResp(h.Name, ip)
|
|
|
- }
|
|
|
- }
|
|
|
- if resp.Answer == nil || answers == 0 {
|
|
|
- logrus.Debugf("[resolver] external DNS %s:%s did not return any %s records for %q", proto, extDNS.IPStr, dns.TypeToString[queryType], queryName)
|
|
|
+ continue
|
|
|
+ case dns.RcodeSuccess:
|
|
|
+ // All is well
|
|
|
+ default:
|
|
|
+ // Server gave some error. Log the error, and continue with the next external DNS server
|
|
|
+ logrus.Debugf("[resolver] external DNS %s:%s responded with %s (code %d) for %q", proto, extDNS.IPStr, statusString(resp.Rcode), resp.Rcode, queryName)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ answers := 0
|
|
|
+ for _, rr := range resp.Answer {
|
|
|
+ h := rr.Header()
|
|
|
+ switch h.Rrtype {
|
|
|
+ case dns.TypeA:
|
|
|
+ answers++
|
|
|
+ ip := rr.(*dns.A).A
|
|
|
+ logrus.Debugf("[resolver] received A record %q for %q from %s:%s", ip, h.Name, proto, extDNS.IPStr)
|
|
|
+ r.backend.HandleQueryResp(h.Name, ip)
|
|
|
+ case dns.TypeAAAA:
|
|
|
+ answers++
|
|
|
+ ip := rr.(*dns.AAAA).AAAA
|
|
|
+ logrus.Debugf("[resolver] received AAAA record %q for %q from %s:%s", ip, h.Name, proto, extDNS.IPStr)
|
|
|
+ r.backend.HandleQueryResp(h.Name, ip)
|
|
|
}
|
|
|
- resp.Compress = true
|
|
|
- break
|
|
|
}
|
|
|
- if resp == nil {
|
|
|
- return
|
|
|
+ if resp.Answer == nil || answers == 0 {
|
|
|
+ logrus.Debugf("[resolver] external DNS %s:%s did not return any %s records for %q", proto, extDNS.IPStr, dns.TypeToString[queryType], queryName)
|
|
|
}
|
|
|
+ resp.Compress = true
|
|
|
+ return resp
|
|
|
}
|
|
|
|
|
|
- if err = w.WriteMsg(resp); err != nil {
|
|
|
- logrus.WithError(err).Errorf("[resolver] failed to write response")
|
|
|
- }
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
-func statusString(responseCode int) string {
|
|
|
- if s, ok := dns.RcodeToString[responseCode]; ok {
|
|
|
- return s
|
|
|
+func (r *Resolver) exchange(proto string, extDNS extDNSEntry, query *dns.Msg) *dns.Msg {
|
|
|
+ extConn, err := r.dialExtDNS(proto, extDNS)
|
|
|
+ if err != nil {
|
|
|
+ logrus.WithError(err).Warn("[resolver] connect failed")
|
|
|
+ return nil
|
|
|
}
|
|
|
- return "UNKNOWN"
|
|
|
-}
|
|
|
-
|
|
|
-func (r *resolver) forwardQueryStart() bool {
|
|
|
- r.queryLock.Lock()
|
|
|
- defer r.queryLock.Unlock()
|
|
|
-
|
|
|
- if r.count == maxConcurrent {
|
|
|
- return false
|
|
|
+ defer extConn.Close()
|
|
|
+
|
|
|
+ log := logrus.WithFields(logrus.Fields{
|
|
|
+ "dns-server": extConn.RemoteAddr().Network() + ":" + extConn.RemoteAddr().String(),
|
|
|
+ "client-addr": extConn.LocalAddr().Network() + ":" + extConn.LocalAddr().String(),
|
|
|
+ "question": query.Question[0].String(),
|
|
|
+ })
|
|
|
+ log.Debug("[resolver] forwarding query")
|
|
|
+
|
|
|
+ resp, _, err := (&dns.Client{
|
|
|
+ Timeout: extIOTimeout,
|
|
|
+ // Following the robustness principle, make a best-effort
|
|
|
+ // attempt to receive oversized response messages without
|
|
|
+ // truncating them on our end to forward verbatim to the client.
|
|
|
+ // Some DNS servers (e.g. Mikrotik RouterOS) don't support
|
|
|
+ // EDNS(0) and may send replies over UDP longer than 512 bytes
|
|
|
+ // regardless of what size limit, if any, was advertized in the
|
|
|
+ // query message. Note that ExchangeWithConn will override this
|
|
|
+ // value if it detects an EDNS OPT record in query so only
|
|
|
+ // oversized replies to non-EDNS queries will benefit.
|
|
|
+ UDPSize: dns.MaxMsgSize,
|
|
|
+ }).ExchangeWithConn(query, &dns.Conn{Conn: extConn})
|
|
|
+ if err != nil {
|
|
|
+ logrus.WithError(err).Errorf("[resolver] failed to query DNS server: %s, query: %s", extConn.RemoteAddr().String(), query.Question[0].String())
|
|
|
+ return nil
|
|
|
}
|
|
|
- r.count++
|
|
|
|
|
|
- return true
|
|
|
+ if resp == nil {
|
|
|
+ // Should be impossible, so make noise if it happens anyway.
|
|
|
+ log.Error("[resolver] external DNS returned empty response")
|
|
|
+ }
|
|
|
+ return resp
|
|
|
}
|
|
|
|
|
|
-func (r *resolver) forwardQueryEnd() {
|
|
|
- r.queryLock.Lock()
|
|
|
- defer r.queryLock.Unlock()
|
|
|
-
|
|
|
- if r.count == 0 {
|
|
|
- logrus.Error("[resolver] invalid concurrent query count")
|
|
|
- } else {
|
|
|
- r.count--
|
|
|
+func statusString(responseCode int) string {
|
|
|
+ if s, ok := dns.RcodeToString[responseCode]; ok {
|
|
|
+ return s
|
|
|
}
|
|
|
+ return "UNKNOWN"
|
|
|
}
|