mirror of
https://github.com/coredns/coredns.git
synced 2025-11-02 02:03:13 -05:00
plugin/transfer: Zone transfer plugin (#3223)
* transfer plugin Signed-off-by: Chris O'Haver <cohaver@infoblox.com>
This commit is contained in:
181
plugin/transfer/transfer.go
Normal file
181
plugin/transfer/transfer.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"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 // the list of plugins that implement Transferer
|
||||
xfrs []*xfr
|
||||
Next plugin.Handler // the next plugin in the chain
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// If serial is not 0, handle 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.
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Find the first transfer instance for which the queried zone is a subdomain.
|
||||
var x *xfr
|
||||
for _, xfr := range t.xfrs {
|
||||
zone := plugin.Zones(xfr.Zones).Matches(state.Name())
|
||||
if zone == "" {
|
||||
continue
|
||||
}
|
||||
x = xfr
|
||||
}
|
||||
if x == nil {
|
||||
// Requested zone did not match any transfer instance zones.
|
||||
// Pass request down chain in case later plugins are capable of handling transfer requests themselves.
|
||||
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
if !x.allowed(state) {
|
||||
return dns.RcodeRefused, nil
|
||||
}
|
||||
|
||||
// Get serial from request if this is an IXFR
|
||||
var serial uint32
|
||||
if state.QType() == dns.TypeIXFR {
|
||||
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 fromPlugin <-chan []dns.RR
|
||||
for _, p := range t.Transferers {
|
||||
var err error
|
||||
fromPlugin, err = p.Transfer(state.QName(), serial)
|
||||
if err == ErrNotAuthoritative {
|
||||
// plugin was not authoritative for the zone, try next plugin
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if fromPlugin == nil {
|
||||
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
// Send response to client
|
||||
ch := make(chan *dns.Envelope)
|
||||
tr := new(dns.Transfer)
|
||||
wg := new(sync.WaitGroup)
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
tr.Out(w, r, ch)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
var soa *dns.SOA
|
||||
rrs := []dns.RR{}
|
||||
l := 0
|
||||
|
||||
receive:
|
||||
for records := range fromPlugin {
|
||||
for _, record := range records {
|
||||
if soa == nil {
|
||||
if soa = record.(*dns.SOA); soa == nil {
|
||||
break receive
|
||||
}
|
||||
serial = soa.Serial
|
||||
}
|
||||
rrs = append(rrs, record)
|
||||
if len(rrs) > 500 {
|
||||
ch <- &dns.Envelope{RR: rrs}
|
||||
l += len(rrs)
|
||||
rrs = []dns.RR{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(rrs) > 0 {
|
||||
ch <- &dns.Envelope{RR: rrs}
|
||||
l += len(rrs)
|
||||
rrs = []dns.RR{}
|
||||
}
|
||||
|
||||
if soa != nil {
|
||||
ch <- &dns.Envelope{RR: []dns.RR{soa}} // closing SOA.
|
||||
l++
|
||||
}
|
||||
|
||||
close(ch) // Even though we close the channel here, we still have
|
||||
wg.Wait() // to wait before we can return and close the connection.
|
||||
|
||||
if soa == nil {
|
||||
return dns.RcodeServerFailure, fmt.Errorf("first record in zone %s is not SOA", state.QName())
|
||||
}
|
||||
|
||||
log.Infof("Outgoing transfer of %d records of zone %s to %s with %d SOA serial", l, state.QName(), state.IP(), serial)
|
||||
return dns.RcodeSuccess, 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.
|
||||
remote := state.IP()
|
||||
if to == remote {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (Transfer) Name() string { return "transfer" }
|
||||
Reference in New Issue
Block a user