Files
coredns/plugin/forward/forward.go
Filippo125 e4990abfa3 feat(forward): add source_address directive (#8011)
* 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>
2026-07-09 17:38:22 -07:00

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