mirror of
https://github.com/coredns/coredns.git
synced 2026-07-10 09:40:12 -04:00
* fix(dnssec): avoid caching empty signing results (#7996) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * save only Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * do #8008 Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Fix Address used if tcp Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Fix bad using of dialer type Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * core: Add full TSIG verification in gRPC transport (#8006) * core: Add full TSIG verification in gRPC transport This PR add full TSIG verification in gRPC using dns.TsigVerify() so invalid signatures and timestamps are correctly detected instead of only checking key presence. Signed-off-by: Yong Tang <yong.tang.github@outlook.com> * Fix Signed-off-by: Yong Tang <yong.tang.github@outlook.com> * Fix Signed-off-by: Yong Tang <yong.tang.github@outlook.com> --------- Signed-off-by: Yong Tang <yong.tang.github@outlook.com> Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * core: Add full TSIG verification in QUIC transport (#8007) * core: Add full TSIG verification in QUIC transport This PR add full TSIG verification in QUIC using dns.TsigVerify() Signed-off-by: Yong Tang <yong.tang.github@outlook.com> * Fix Signed-off-by: Yong Tang <yong.tang.github@outlook.com> --------- Signed-off-by: Yong Tang <yong.tang.github@outlook.com> Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(test): deduplicate TSIG test helpers (#8009) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(dnssec): return nil sigs on sign error (#7999) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(dnssec): return nil from ParseKeyFile on error (#8000) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(dnsserver): allow view server blocks in any declaration order (#8001) When using the view plugin, filtered and unfiltered server blocks can share the same zone and port. The zone overlap validation rejected this configuration when the unfiltered block was not declared last, because filtered configs treated an already-registered zone as an error. Skip the 'already defined' check for configs that have filter functions, since they are expected to coexist with an unfiltered catch-all block on the same zone/port. Fixes #7733 Signed-off-by: umut-polat <52835619+umut-polat@users.noreply.github.com> Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(doh): use per-connection local address for PROXY protocol (#8005) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(transfer): batch AXFR records by message size instead of count (#8002) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(tls): use temp dir for keylog test path (#8010) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Rename local_address option to source_address Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Rename local_address also in readme Add test Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Resolve change request in pr Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix ci lint Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Improve doc on source_address routing needs Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Remove added timeout Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * add use of source address also for health check query Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * remove untrailing newline from health_test.go Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix file format Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Update plugin/forward/setup_test.go Co-authored-by: Ville Vesilehto <ville@vesilehto.fi> Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Remove dead code in TestHealthLocalAddress Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Fix misspelling Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Try to set default timeout Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix format in health.go Signed-off-by: Filippo <filippo.ferrazini@gmail.com> --------- Signed-off-by: Filippo <filippo.ferrazini@gmail.com> Signed-off-by: Yong Tang <yong.tang.github@outlook.com> Signed-off-by: umut-polat <52835619+umut-polat@users.noreply.github.com> Co-authored-by: Ville Vesilehto <ville@vesilehto.fi> Co-authored-by: Yong Tang <yong.tang.github@outlook.com> Co-authored-by: Umut Polat <52835619+umut-polat@users.noreply.github.com> Co-authored-by: Cedric Wang <wangzongqi@msn.com>
345 lines
9.5 KiB
Go
345 lines
9.5 KiB
Go
// Package forward implements a forwarding proxy. It caches an upstream net.Conn for some time, so if the same
|
|
// client returns the upstream's Conn will be precached. Depending on how you benchmark this looks to be
|
|
// 50% faster than just opening a new connection for every client. It works with UDP and TCP and uses
|
|
// inband healthchecking.
|
|
package forward
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/coredns/coredns/plugin"
|
|
"github.com/coredns/coredns/plugin/debug"
|
|
"github.com/coredns/coredns/plugin/dnstap"
|
|
"github.com/coredns/coredns/plugin/metadata"
|
|
clog "github.com/coredns/coredns/plugin/pkg/log"
|
|
proxyPkg "github.com/coredns/coredns/plugin/pkg/proxy"
|
|
"github.com/coredns/coredns/request"
|
|
|
|
"github.com/miekg/dns"
|
|
ot "github.com/opentracing/opentracing-go"
|
|
otext "github.com/opentracing/opentracing-go/ext"
|
|
)
|
|
|
|
var log = clog.NewWithPlugin("forward")
|
|
|
|
const (
|
|
defaultExpire = 10 * time.Second
|
|
defaultReadTimeout = 2 * time.Second
|
|
hcInterval = 500 * time.Millisecond
|
|
)
|
|
|
|
// Forward represents a plugin instance that can proxy requests to another (DNS) server. It has a list
|
|
// of proxies each representing one upstream proxy.
|
|
type Forward struct {
|
|
concurrent int64 // atomic counters need to be first in struct for proper alignment
|
|
|
|
proxies []*proxyPkg.Proxy
|
|
p Policy
|
|
hcInterval time.Duration
|
|
|
|
from string
|
|
ignored []string
|
|
|
|
nextAlternateRcodes []int
|
|
nextOnNodata bool
|
|
|
|
tlsConfig *tls.Config
|
|
tlsServerName string
|
|
maxfails uint32
|
|
expire time.Duration
|
|
maxAge time.Duration
|
|
readTimeout time.Duration
|
|
maxIdleConns int
|
|
dohMethod string
|
|
maxConcurrent int64
|
|
failfastUnhealthyUpstreams bool
|
|
failoverRcodes []int
|
|
maxConnectAttempts uint32
|
|
sourceAddress net.IP
|
|
|
|
// Hostname resolution fields
|
|
resolver []string // custom resolver IPs for hostname TO resolution
|
|
toEntries []toEntry // ordered TO entries preserving config order
|
|
|
|
opts proxyPkg.Options // also here for testing
|
|
|
|
// ErrLimitExceeded indicates that a query was rejected because the number of concurrent queries has exceeded
|
|
// the maximum allowed (maxConcurrent)
|
|
ErrLimitExceeded error
|
|
|
|
tapPlugins []*dnstap.Dnstap // when dnstap plugins are loaded, we use to this to send messages out.
|
|
|
|
Next plugin.Handler
|
|
}
|
|
|
|
// New returns a new Forward.
|
|
func New() *Forward {
|
|
f := &Forward{maxfails: 2, tlsConfig: new(tls.Config), expire: defaultExpire, readTimeout: defaultReadTimeout, p: new(random), from: ".", hcInterval: hcInterval, dohMethod: http.MethodPost, opts: proxyPkg.Options{ForceTCP: false, PreferUDP: false, HCRecursionDesired: true, HCDomain: "."}}
|
|
return f
|
|
}
|
|
|
|
// SetProxy appends p to the proxy list and starts healthchecking.
|
|
func (f *Forward) SetProxy(p *proxyPkg.Proxy) {
|
|
f.proxies = append(f.proxies, p)
|
|
p.Start(f.hcInterval)
|
|
}
|
|
|
|
// SetProxyOptions setup proxy options
|
|
func (f *Forward) SetProxyOptions(opts proxyPkg.Options) {
|
|
f.opts = opts
|
|
}
|
|
|
|
// SetTapPlugin appends one or more dnstap plugins to the tap plugin list.
|
|
func (f *Forward) SetTapPlugin(tapPlugin *dnstap.Dnstap) {
|
|
f.tapPlugins = append(f.tapPlugins, tapPlugin)
|
|
if nextPlugin, ok := tapPlugin.Next.(*dnstap.Dnstap); ok {
|
|
f.SetTapPlugin(nextPlugin)
|
|
}
|
|
}
|
|
|
|
// Len returns the number of configured proxies.
|
|
func (f *Forward) Len() int { return len(f.proxies) }
|
|
|
|
// Name implements plugin.Handler.
|
|
func (f *Forward) Name() string { return "forward" }
|
|
|
|
// ServeDNS implements plugin.Handler.
|
|
func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
|
state := request.Request{W: w, Req: r}
|
|
if !f.match(state) {
|
|
return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
|
|
}
|
|
|
|
if f.maxConcurrent > 0 {
|
|
count := atomic.AddInt64(&(f.concurrent), 1)
|
|
defer atomic.AddInt64(&(f.concurrent), -1)
|
|
if count > f.maxConcurrent {
|
|
maxConcurrentRejectCount.Add(1)
|
|
return dns.RcodeRefused, f.ErrLimitExceeded
|
|
}
|
|
}
|
|
|
|
fails := 0
|
|
var span, child ot.Span
|
|
var upstreamErr error
|
|
span = ot.SpanFromContext(ctx)
|
|
i := 0
|
|
list := f.List()
|
|
deadline := time.Now().Add(defaultTimeout)
|
|
start := time.Now()
|
|
connectAttempts := uint32(0)
|
|
|
|
for time.Now().Before(deadline) && ctx.Err() == nil && (f.maxConnectAttempts == 0 || connectAttempts < f.maxConnectAttempts) {
|
|
if i >= len(list) {
|
|
// reached the end of list, reset to begin
|
|
i = 0
|
|
fails = 0
|
|
}
|
|
|
|
proxy := list[i]
|
|
i++
|
|
if proxy.Down(f.maxfails) {
|
|
fails++
|
|
if fails < len(f.proxies) {
|
|
continue
|
|
}
|
|
|
|
healthcheckBrokenCount.Add(1)
|
|
// All upstreams are dead, return servfail if all upstreams are down
|
|
if f.failfastUnhealthyUpstreams {
|
|
break
|
|
}
|
|
// assume healthcheck is completely broken and randomly
|
|
// select an upstream to connect to.
|
|
r := new(random)
|
|
proxy = r.List(f.proxies)[0]
|
|
}
|
|
|
|
if span != nil {
|
|
child = span.Tracer().StartSpan("connect", ot.ChildOf(span.Context()))
|
|
otext.PeerAddress.Set(child, proxy.Addr())
|
|
ctx = ot.ContextWithSpan(ctx, child)
|
|
}
|
|
|
|
metadata.SetValueFunc(ctx, "forward/upstream", func() string {
|
|
return proxy.Addr()
|
|
})
|
|
|
|
var (
|
|
ret *dns.Msg
|
|
localAddr net.Addr
|
|
upstreamProto string
|
|
err error
|
|
)
|
|
opts := f.opts
|
|
|
|
for {
|
|
ret, localAddr, upstreamProto, err = proxy.Connect(ctx, state, opts)
|
|
|
|
if err == proxyPkg.ErrCachedClosed { // Remote side closed conn, can only happen with TCP.
|
|
continue
|
|
}
|
|
// Retry with TCP if truncated and prefer_udp configured.
|
|
if ret != nil && ret.Truncated && !opts.ForceTCP && opts.PreferUDP {
|
|
opts.ForceTCP = true
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
if child != nil {
|
|
child.Finish()
|
|
}
|
|
|
|
if len(f.tapPlugins) != 0 {
|
|
toDnstap(ctx, f, proxy.Addr(), localAddr, upstreamProto, state, ret, start)
|
|
}
|
|
|
|
upstreamErr = err
|
|
|
|
if err != nil {
|
|
// Kick off health check to see if *our* upstream is broken.
|
|
if f.maxfails != 0 {
|
|
proxy.Healthcheck()
|
|
}
|
|
|
|
// If a per-request connect-attempt cap is configured, count this
|
|
// failed connect attempt and stop retrying when the cap is hit.
|
|
if f.maxConnectAttempts > 0 {
|
|
connectAttempts++
|
|
if connectAttempts >= f.maxConnectAttempts {
|
|
break
|
|
}
|
|
}
|
|
|
|
if fails < len(f.proxies) {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
// Check if the reply is correct; if not return FormErr.
|
|
if !state.Match(ret) {
|
|
debug.Hexdumpf(ret, "Wrong reply for id: %d, %s %d", ret.Id, state.QName(), state.QType())
|
|
|
|
formerr := new(dns.Msg)
|
|
formerr.SetRcode(state.Req, dns.RcodeFormatError)
|
|
w.WriteMsg(formerr)
|
|
return 0, nil
|
|
}
|
|
|
|
// Check if we have a failover Rcode defined, check if we match on the code
|
|
tryNext := false
|
|
for _, failoverRcode := range f.failoverRcodes {
|
|
// if we match, we continue to the next upstream in the list
|
|
if failoverRcode == ret.Rcode {
|
|
if fails < len(f.proxies) {
|
|
tryNext = true
|
|
}
|
|
}
|
|
}
|
|
if tryNext {
|
|
fails++
|
|
continue
|
|
}
|
|
|
|
// Check if we have an alternate Rcode defined, check if we match on the code
|
|
for _, alternateRcode := range f.nextAlternateRcodes {
|
|
if alternateRcode == ret.Rcode && f.Next != nil { // In case we do not have a Next handler, just continue normally
|
|
if _, ok := f.Next.(*Forward); ok { // Only continue if the next forwarder is also a Forworder
|
|
return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
if f.nextOnNodata && f.Next != nil {
|
|
if ret.Rcode == dns.RcodeSuccess && isEmpty(ret) {
|
|
if _, ok := f.Next.(*Forward); ok {
|
|
return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
w.WriteMsg(ret)
|
|
return 0, nil
|
|
}
|
|
|
|
if upstreamErr != nil {
|
|
return dns.RcodeServerFailure, upstreamErr
|
|
}
|
|
|
|
return dns.RcodeServerFailure, ErrNoHealthy
|
|
}
|
|
|
|
func (f *Forward) match(state request.Request) bool {
|
|
if !plugin.Name(f.from).Matches(state.Name()) || !f.isAllowedDomain(state.Name()) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (f *Forward) isAllowedDomain(name string) bool {
|
|
if dns.Name(name) == dns.Name(f.from) {
|
|
return true
|
|
}
|
|
|
|
for _, ignore := range f.ignored {
|
|
if plugin.Name(ignore).Matches(name) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isEmpty(r *dns.Msg) bool {
|
|
if len(r.Answer) == 0 {
|
|
return true
|
|
}
|
|
|
|
for _, r := range r.Answer {
|
|
if r != nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ForceTCP returns if TCP is forced to be used even when the request comes in over UDP.
|
|
func (f *Forward) ForceTCP() bool { return f.opts.ForceTCP }
|
|
|
|
// PreferUDP returns if UDP is preferred to be used even when the request comes in over TCP.
|
|
func (f *Forward) PreferUDP() bool { return f.opts.PreferUDP }
|
|
|
|
// List returns a set of proxies to be used for this client depending on the policy in f.
|
|
func (f *Forward) List() []*proxyPkg.Proxy { return f.p.List(f.proxies) }
|
|
|
|
var (
|
|
// ErrNoHealthy means no healthy proxies left.
|
|
ErrNoHealthy = errors.New("no healthy proxies")
|
|
// ErrNoForward means no forwarder defined.
|
|
ErrNoForward = errors.New("no forwarder defined")
|
|
// ErrCachedClosed means cached connection was closed by peer.
|
|
ErrCachedClosed = errors.New("cached connection was closed by peer")
|
|
)
|
|
|
|
// Options holds various Options that can be set.
|
|
type Options struct {
|
|
// ForceTCP use TCP protocol for upstream DNS request. Has precedence over PreferUDP flag
|
|
ForceTCP bool
|
|
// PreferUDP use UDP protocol for upstream DNS request.
|
|
PreferUDP bool
|
|
// HCRecursionDesired sets recursion desired flag for Proxy healthcheck requests
|
|
HCRecursionDesired bool
|
|
// HCDomain sets domain for Proxy healthcheck requests
|
|
HCDomain string
|
|
}
|
|
|
|
var defaultTimeout = 5 * time.Second
|