Files
coredns/plugin/transfer/transfer.go
Ville Vesilehto 3080ec0448 lint(errorlint): handle wrapped errors
Enable errorlint and preserve wrapped error chains so runtime checks
and tests classify failures correctly. This also makes Route53
surface insert failures instead of silently dropping them.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
2026-04-25 11:57:32 +03:00

236 lines
6.5 KiB
Go

package transfer
import (
"context"
"errors"
"net"
"github.com/coredns/coredns/plugin"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
var log = clog.NewWithPlugin("transfer")
// Transfer is a plugin that handles zone transfers.
type Transfer struct {
Transferers []Transferer // List of plugins that implement Transferer
xfrs []*xfr
tsigSecret map[string]string
Next plugin.Handler
}
type xfr struct {
Zones []string
to []string
}
// Transferer may be implemented by plugins to enable zone transfers
type Transferer interface {
// Transfer returns a channel to which it writes responses to the transfer request.
// If the plugin is not authoritative for the zone, it should immediately return the
// transfer.ErrNotAuthoritative error. This is important otherwise the transfer plugin can
// use plugin X while it should transfer the data from plugin Y.
//
// If serial is 0, handle as an AXFR request. Transfer should send all records
// in the zone to the channel. The SOA should be written to the channel first, followed
// by all other records, including all NS + glue records. The implementation is also responsible
// for sending the last SOA record (to signal end of the transfer). This plugin will just grab
// these records and send them back to the requester, there is little validation done.
//
// If serial is not 0, it will be handled as an IXFR request. If the serial is equal to or greater (newer) than
// the current serial for the zone, send a single SOA record to the channel and then close it.
// If the serial is less (older) than the current serial for the zone, perform an AXFR fallback
// by proceeding as if an AXFR was requested (as above).
Transfer(zone string, serial uint32) (<-chan []dns.RR, error)
}
var (
// ErrNotAuthoritative is returned by Transfer() when the plugin is not authoritative for the zone.
ErrNotAuthoritative = errors.New("not authoritative for zone")
)
// ServeDNS implements the plugin.Handler interface.
func (t *Transfer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
if state.QType() != dns.TypeAXFR && state.QType() != dns.TypeIXFR {
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
}
if state.Proto() != "tcp" {
return dns.RcodeRefused, nil
}
x := longestMatch(t.xfrs, state.Name())
if x == nil {
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
}
if !x.allowed(state) {
// write msg here, so logging will pick it up
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(m)
return 0, nil
}
// Get serial from request if this is an IXFR.
var serial uint32
if state.QType() == dns.TypeIXFR {
if len(r.Ns) != 1 {
return dns.RcodeServerFailure, nil
}
soa, ok := r.Ns[0].(*dns.SOA)
if !ok {
return dns.RcodeServerFailure, nil
}
serial = soa.Serial
}
// Get a receiving channel from the first Transferer plugin that returns one.
var pchan <-chan []dns.RR
var err error
for _, p := range t.Transferers {
pchan, err = p.Transfer(state.Name(), serial)
if errors.Is(err, ErrNotAuthoritative) {
// plugin was not authoritative for the zone, try next plugin
continue
}
if err != nil {
return dns.RcodeServerFailure, err
}
break
}
if pchan == nil {
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
}
// Send response to client
ch := make(chan *dns.Envelope)
tr := new(dns.Transfer)
if r.IsTsig() != nil {
tr.TsigSecret = t.tsigSecret
}
errCh := make(chan error)
go func() {
if err := tr.Out(w, r, ch); err != nil {
errCh <- err
}
close(errCh)
}()
rrs := []dns.RR{}
l := 0
batchSize := 0
var soa *dns.SOA
for records := range pchan {
if x, ok := records[0].(*dns.SOA); ok && soa == nil {
soa = x
}
for _, rr := range records {
rrLen := dns.Len(rr)
// Flush the batch before it exceeds the 64KB TCP message limit.
// The 12-byte header and question section are not counted in rrLen,
// so we use a conservative threshold to leave room for framing.
if len(rrs) > 0 && batchSize+rrLen > 63000 {
select {
case ch <- &dns.Envelope{RR: rrs}:
case err := <-errCh:
go func() {
for range pchan {
}
}()
return dns.RcodeServerFailure, err
}
l += len(rrs)
rrs = []dns.RR{}
batchSize = 0
}
rrs = append(rrs, rr)
batchSize += rrLen
}
}
// if we are here and we only hold 1 soa (len(rrs) == 1) and soa != nil, and IXFR fallback should
// be performed. We haven't send anything on ch yet, so that can be closed (and waited for), and we only
// need to return the SOA back to the client and return.
if len(rrs) == 1 && soa != nil { // soa should never be nil...
close(ch)
err := <-errCh
if err != nil {
return dns.RcodeServerFailure, err
}
m := new(dns.Msg)
m.SetReply(r)
m.Answer = []dns.RR{soa}
w.WriteMsg(m)
log.Infof("Outgoing noop, incremental transfer for up to date zone %q to %s for %d SOA serial", state.QName(), state.IP(), soa.Serial)
return 0, nil
}
if len(rrs) > 0 {
select {
case ch <- &dns.Envelope{RR: rrs}:
case err := <-errCh:
close(ch)
return dns.RcodeServerFailure, err
}
l += len(rrs)
}
close(ch) // Even though we close the channel here, we still have
err = <-errCh // to wait before we can return and close the connection.
if err != nil {
return dns.RcodeServerFailure, err
}
logserial := uint32(0)
if soa != nil {
logserial = soa.Serial
}
log.Infof("Outgoing transfer of %d records of zone %q to %s for %d SOA serial", l, state.QName(), state.IP(), logserial)
return 0, nil
}
func (x xfr) allowed(state request.Request) bool {
for _, h := range x.to {
if h == "*" {
return true
}
to, _, err := net.SplitHostPort(h)
if err != nil {
return false
}
// If remote IP matches we accept. TODO(): make this works with ranges
if to == state.IP() {
return true
}
}
return false
}
// Find the first transfer instance for which the queried zone is the longest match. When nothing
// is found nil is returned.
func longestMatch(xfrs []*xfr, name string) *xfr {
// TODO(xxx): optimize and make it a map (or maps)
var x *xfr
zone := "" // longest zone match wins
for _, xfr := range xfrs {
if z := plugin.Zones(xfr.Zones).Matches(name); z != "" {
if len(z) > len(zone) || (len(z) == len(zone) && z > zone) {
zone = z
x = xfr
}
}
}
return x
}
// Name implements the Handler interface.
func (Transfer) Name() string { return "transfer" }