sigs.k8s.io/external-dns@v0.14.1/provider/rfc2136/rfc2136.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package rfc2136
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"fmt"
    23  	"net"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/bodgit/tsig"
    30  	"github.com/bodgit/tsig/gss"
    31  	"github.com/miekg/dns"
    32  
    33  	"github.com/pkg/errors"
    34  	log "github.com/sirupsen/logrus"
    35  
    36  	"sigs.k8s.io/external-dns/endpoint"
    37  	"sigs.k8s.io/external-dns/pkg/tlsutils"
    38  	"sigs.k8s.io/external-dns/plan"
    39  	"sigs.k8s.io/external-dns/provider"
    40  )
    41  
    42  const (
    43  	// maximum time DNS client can be off from server for an update to succeed
    44  	clockSkew = 300
    45  )
    46  
    47  // rfc2136 provider type
    48  type rfc2136Provider struct {
    49  	provider.BaseProvider
    50  	nameserver      string
    51  	zoneNames       []string
    52  	tsigKeyName     string
    53  	tsigSecret      string
    54  	tsigSecretAlg   string
    55  	insecure        bool
    56  	axfr            bool
    57  	minTTL          time.Duration
    58  	batchChangeSize int
    59  	tlsConfig       TLSConfig
    60  
    61  	// options specific to rfc3645 gss-tsig support
    62  	gssTsig      bool
    63  	krb5Username string
    64  	krb5Password string
    65  	krb5Realm    string
    66  
    67  	// only consider hosted zones managing domains ending in this suffix
    68  	domainFilter endpoint.DomainFilter
    69  	dryRun       bool
    70  	actions      rfc2136Actions
    71  }
    72  
    73  // TLSConfig is comprised of the TLS-related fields necessary if we are using DNS over TLS
    74  type TLSConfig struct {
    75  	UseTLS                bool
    76  	SkipTLSVerify         bool
    77  	CAFilePath            string
    78  	ClientCertFilePath    string
    79  	ClientCertKeyFilePath string
    80  	ServerName            string
    81  }
    82  
    83  // Map of supported TSIG algorithms
    84  var tsigAlgs = map[string]string{
    85  	"hmac-sha1":   dns.HmacSHA1,
    86  	"hmac-sha224": dns.HmacSHA224,
    87  	"hmac-sha256": dns.HmacSHA256,
    88  	"hmac-sha384": dns.HmacSHA384,
    89  	"hmac-sha512": dns.HmacSHA512,
    90  }
    91  
    92  type rfc2136Actions interface {
    93  	SendMessage(msg *dns.Msg) error
    94  	IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error)
    95  }
    96  
    97  // NewRfc2136Provider is a factory function for OpenStack rfc2136 providers
    98  func NewRfc2136Provider(host string, port int, zoneNames []string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, gssTsig bool, krb5Username string, krb5Password string, krb5Realm string, batchChangeSize int, tlsConfig TLSConfig, actions rfc2136Actions) (provider.Provider, error) {
    99  	secretAlgChecked, ok := tsigAlgs[secretAlg]
   100  	if !ok && !insecure && !gssTsig {
   101  		return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg)
   102  	}
   103  
   104  	// Set zone to root if no set
   105  	if len(zoneNames) == 0 {
   106  		zoneNames = append(zoneNames, ".")
   107  	}
   108  
   109  	// Sort zones
   110  	sort.Slice(zoneNames, func(i, j int) bool {
   111  		return len(strings.Split(zoneNames[i], ".")) > len(strings.Split(zoneNames[j], "."))
   112  	})
   113  
   114  	if tlsConfig.UseTLS {
   115  		tlsConfig.ServerName = host
   116  	}
   117  
   118  	r := &rfc2136Provider{
   119  		nameserver:      net.JoinHostPort(host, strconv.Itoa(port)),
   120  		zoneNames:       zoneNames,
   121  		insecure:        insecure,
   122  		gssTsig:         gssTsig,
   123  		krb5Username:    krb5Username,
   124  		krb5Password:    krb5Password,
   125  		krb5Realm:       strings.ToUpper(krb5Realm),
   126  		domainFilter:    domainFilter,
   127  		dryRun:          dryRun,
   128  		axfr:            axfr,
   129  		minTTL:          minTTL,
   130  		batchChangeSize: batchChangeSize,
   131  		tlsConfig:       tlsConfig,
   132  	}
   133  	if actions != nil {
   134  		r.actions = actions
   135  	} else {
   136  		r.actions = r
   137  	}
   138  
   139  	if !insecure {
   140  		r.tsigKeyName = dns.Fqdn(keyName)
   141  		r.tsigSecret = secret
   142  		r.tsigSecretAlg = secretAlgChecked
   143  	}
   144  
   145  	log.Infof("Configured RFC2136 with zone '%s' and nameserver '%s'", r.zoneNames, r.nameserver)
   146  	return r, nil
   147  }
   148  
   149  // KeyName will return TKEY name and TSIG handle to use for followon actions with a secure connection
   150  func (r rfc2136Provider) KeyData() (keyName string, handle *gss.Client, err error) {
   151  	handle, err = gss.NewClient(new(dns.Client))
   152  	if err != nil {
   153  		return keyName, handle, err
   154  	}
   155  
   156  	keyName, _, err = handle.NegotiateContextWithCredentials(r.nameserver, r.krb5Realm, r.krb5Username, r.krb5Password)
   157  
   158  	return keyName, handle, err
   159  }
   160  
   161  // Records returns the list of records.
   162  func (r rfc2136Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
   163  	rrs, err := r.List()
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	var eps []*endpoint.Endpoint
   169  
   170  OuterLoop:
   171  	for _, rr := range rrs {
   172  		log.Debugf("Record=%s", rr)
   173  
   174  		if rr.Header().Class != dns.ClassINET {
   175  			continue
   176  		}
   177  
   178  		rrFqdn := rr.Header().Name
   179  		rrTTL := endpoint.TTL(rr.Header().Ttl)
   180  		var rrType string
   181  		var rrValues []string
   182  		switch rr.Header().Rrtype {
   183  		case dns.TypeCNAME:
   184  			rrValues = []string{rr.(*dns.CNAME).Target}
   185  			rrType = "CNAME"
   186  		case dns.TypeA:
   187  			rrValues = []string{rr.(*dns.A).A.String()}
   188  			rrType = "A"
   189  		case dns.TypeAAAA:
   190  			rrValues = []string{rr.(*dns.AAAA).AAAA.String()}
   191  			rrType = "AAAA"
   192  		case dns.TypeTXT:
   193  			rrValues = (rr.(*dns.TXT).Txt)
   194  			rrType = "TXT"
   195  		case dns.TypeNS:
   196  			rrValues = []string{rr.(*dns.NS).Ns}
   197  			rrType = "NS"
   198  		default:
   199  			continue // Unhandled record type
   200  		}
   201  
   202  		for idx, existingEndpoint := range eps {
   203  			if existingEndpoint.DNSName == strings.TrimSuffix(rrFqdn, ".") && existingEndpoint.RecordType == rrType {
   204  				eps[idx].Targets = append(eps[idx].Targets, rrValues...)
   205  				continue OuterLoop
   206  			}
   207  		}
   208  
   209  		ep := endpoint.NewEndpointWithTTL(
   210  			rrFqdn,
   211  			rrType,
   212  			rrTTL,
   213  			rrValues...,
   214  		)
   215  
   216  		eps = append(eps, ep)
   217  	}
   218  
   219  	return eps, nil
   220  }
   221  
   222  func (r rfc2136Provider) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error) {
   223  	t := new(dns.Transfer)
   224  	if !r.insecure && !r.gssTsig {
   225  		t.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret}
   226  	}
   227  
   228  	c, err := makeClient(r)
   229  	if err != nil {
   230  		return nil, fmt.Errorf("error setting up TLS: %w", err)
   231  	}
   232  	conn, err := c.Dial(a)
   233  	if err != nil {
   234  		return nil, fmt.Errorf("failed to connect for transfer: %w", err)
   235  	}
   236  	t.Conn = conn
   237  	return t.In(m, r.nameserver)
   238  }
   239  
   240  func (r rfc2136Provider) List() ([]dns.RR, error) {
   241  	if !r.axfr {
   242  		log.Debug("axfr is disabled")
   243  		return make([]dns.RR, 0), nil
   244  	}
   245  
   246  	records := make([]dns.RR, 0)
   247  	for _, zone := range r.zoneNames {
   248  		log.Debugf("Fetching records for '%q'", zone)
   249  
   250  		m := new(dns.Msg)
   251  		m.SetAxfr(dns.Fqdn(zone))
   252  		if !r.insecure && !r.gssTsig {
   253  			m.SetTsig(r.tsigKeyName, r.tsigSecretAlg, clockSkew, time.Now().Unix())
   254  		}
   255  
   256  		env, err := r.actions.IncomeTransfer(m, r.nameserver)
   257  		if err != nil {
   258  			return nil, fmt.Errorf("failed to fetch records via AXFR: %w", err)
   259  		}
   260  
   261  		for e := range env {
   262  			if e.Error != nil {
   263  				if e.Error == dns.ErrSoa {
   264  					log.Error("AXFR error: unexpected response received from the server")
   265  				} else {
   266  					log.Errorf("AXFR error: %v", e.Error)
   267  				}
   268  				continue
   269  			}
   270  			records = append(records, e.RR...)
   271  		}
   272  	}
   273  
   274  	return records, nil
   275  }
   276  
   277  // ApplyChanges applies a given set of changes in a given zone.
   278  func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   279  	log.Debugf("ApplyChanges (Create: %d, UpdateOld: %d, UpdateNew: %d, Delete: %d)", len(changes.Create), len(changes.UpdateOld), len(changes.UpdateNew), len(changes.Delete))
   280  
   281  	var errors []error
   282  
   283  	for c, chunk := range chunkBy(changes.Create, r.batchChangeSize) {
   284  		log.Debugf("Processing batch %d of create changes", c)
   285  
   286  		m := make(map[string]*dns.Msg)
   287  		m["."] = new(dns.Msg) // Add the root zone
   288  		for _, z := range r.zoneNames {
   289  			z = dns.Fqdn(z)
   290  			m[z] = new(dns.Msg)
   291  		}
   292  		for _, ep := range chunk {
   293  			if !r.domainFilter.Match(ep.DNSName) {
   294  				log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
   295  				continue
   296  			}
   297  
   298  			zone := findMsgZone(ep, r.zoneNames)
   299  			r.krb5Realm = strings.ToUpper(zone)
   300  			m[zone].SetUpdate(zone)
   301  
   302  			r.AddRecord(m[zone], ep)
   303  		}
   304  
   305  		// only send if there are records available
   306  		for _, z := range m {
   307  			if len(z.Ns) > 0 {
   308  				if err := r.actions.SendMessage(z); err != nil {
   309  					log.Errorf("RFC2136 create record failed: %v", err)
   310  					errors = append(errors, err)
   311  					continue
   312  				}
   313  			}
   314  		}
   315  	}
   316  
   317  	for c, chunk := range chunkBy(changes.UpdateNew, r.batchChangeSize) {
   318  		log.Debugf("Processing batch %d of update changes", c)
   319  
   320  		m := make(map[string]*dns.Msg)
   321  		m["."] = new(dns.Msg) // Add the root zone
   322  		for _, z := range r.zoneNames {
   323  			z = dns.Fqdn(z)
   324  			m[z] = new(dns.Msg)
   325  		}
   326  
   327  		for i, ep := range chunk {
   328  			if !r.domainFilter.Match(ep.DNSName) {
   329  				log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
   330  				continue
   331  			}
   332  
   333  			zone := findMsgZone(ep, r.zoneNames)
   334  			r.krb5Realm = strings.ToUpper(zone)
   335  			m[zone].SetUpdate(zone)
   336  
   337  			r.UpdateRecord(m[zone], changes.UpdateOld[i], ep)
   338  		}
   339  
   340  		// only send if there are records available
   341  		for _, z := range m {
   342  			if len(z.Ns) > 0 {
   343  				if err := r.actions.SendMessage(z); err != nil {
   344  					log.Errorf("RFC2136 update record failed: %v", err)
   345  					errors = append(errors, err)
   346  					continue
   347  				}
   348  			}
   349  		}
   350  	}
   351  
   352  	for c, chunk := range chunkBy(changes.Delete, r.batchChangeSize) {
   353  		log.Debugf("Processing batch %d of delete changes", c)
   354  
   355  		m := make(map[string]*dns.Msg)
   356  		m["."] = new(dns.Msg) // Add the root zone
   357  		for _, z := range r.zoneNames {
   358  			z = dns.Fqdn(z)
   359  			m[z] = new(dns.Msg)
   360  		}
   361  		for _, ep := range chunk {
   362  			if !r.domainFilter.Match(ep.DNSName) {
   363  				log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
   364  				continue
   365  			}
   366  
   367  			zone := findMsgZone(ep, r.zoneNames)
   368  			r.krb5Realm = strings.ToUpper(zone)
   369  			m[zone].SetUpdate(zone)
   370  
   371  			r.RemoveRecord(m[zone], ep)
   372  		}
   373  
   374  		// only send if there are records available
   375  		for _, z := range m {
   376  			if len(z.Ns) > 0 {
   377  				if err := r.actions.SendMessage(z); err != nil {
   378  					log.Errorf("RFC2136 delete record failed: %v", err)
   379  					errors = append(errors, err)
   380  					continue
   381  				}
   382  			}
   383  		}
   384  	}
   385  
   386  	if len(errors) > 0 {
   387  		return fmt.Errorf("RFC2136 had errors in one or more of its batches: %v", errors)
   388  	}
   389  
   390  	return nil
   391  }
   392  
   393  func (r rfc2136Provider) UpdateRecord(m *dns.Msg, oldEp *endpoint.Endpoint, newEp *endpoint.Endpoint) error {
   394  	err := r.RemoveRecord(m, oldEp)
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	return r.AddRecord(m, newEp)
   400  }
   401  
   402  func (r rfc2136Provider) AddRecord(m *dns.Msg, ep *endpoint.Endpoint) error {
   403  	log.Debugf("AddRecord.ep=%s", ep)
   404  
   405  	ttl := int64(r.minTTL.Seconds())
   406  	if ep.RecordTTL.IsConfigured() && int64(ep.RecordTTL) > ttl {
   407  		ttl = int64(ep.RecordTTL)
   408  	}
   409  
   410  	for _, target := range ep.Targets {
   411  		newRR := fmt.Sprintf("%s %d %s %s", ep.DNSName, ttl, ep.RecordType, target)
   412  		log.Infof("Adding RR: %s", newRR)
   413  
   414  		rr, err := dns.NewRR(newRR)
   415  		if err != nil {
   416  			return fmt.Errorf("failed to build RR: %v", err)
   417  		}
   418  
   419  		m.Insert([]dns.RR{rr})
   420  	}
   421  
   422  	return nil
   423  }
   424  
   425  func (r rfc2136Provider) RemoveRecord(m *dns.Msg, ep *endpoint.Endpoint) error {
   426  	log.Debugf("RemoveRecord.ep=%s", ep)
   427  	for _, target := range ep.Targets {
   428  		newRR := fmt.Sprintf("%s %d %s %s", ep.DNSName, ep.RecordTTL, ep.RecordType, target)
   429  		log.Infof("Removing RR: %s", newRR)
   430  
   431  		rr, err := dns.NewRR(newRR)
   432  		if err != nil {
   433  			return fmt.Errorf("failed to build RR: %v", err)
   434  		}
   435  
   436  		m.Remove([]dns.RR{rr})
   437  	}
   438  
   439  	return nil
   440  }
   441  
   442  func (r rfc2136Provider) SendMessage(msg *dns.Msg) error {
   443  	if r.dryRun {
   444  		log.Debugf("SendMessage.skipped")
   445  		return nil
   446  	}
   447  	log.Debugf("SendMessage")
   448  
   449  	c, err := makeClient(r)
   450  	if err != nil {
   451  		return fmt.Errorf("error setting up TLS: %w", err)
   452  	}
   453  
   454  	if !r.insecure {
   455  		if r.gssTsig {
   456  			keyName, handle, err := r.KeyData()
   457  			if err != nil {
   458  				return err
   459  			}
   460  			defer handle.Close()
   461  			defer handle.DeleteContext(keyName)
   462  
   463  			c.TsigProvider = handle
   464  
   465  			msg.SetTsig(keyName, tsig.GSS, clockSkew, time.Now().Unix())
   466  		} else {
   467  			c.TsigProvider = tsig.HMAC{r.tsigKeyName: r.tsigSecret}
   468  			msg.SetTsig(r.tsigKeyName, r.tsigSecretAlg, clockSkew, time.Now().Unix())
   469  		}
   470  	}
   471  
   472  	resp, _, err := c.Exchange(msg, r.nameserver)
   473  	if err != nil {
   474  		if resp != nil && resp.Rcode != dns.RcodeSuccess {
   475  			log.Infof("error in dns.Client.Exchange: %s", err)
   476  			return err
   477  		}
   478  		log.Warnf("warn in dns.Client.Exchange: %s", err)
   479  	}
   480  	if resp != nil && resp.Rcode != dns.RcodeSuccess {
   481  		log.Infof("Bad dns.Client.Exchange response: %s", resp)
   482  		return fmt.Errorf("bad return code: %s", dns.RcodeToString[resp.Rcode])
   483  	}
   484  
   485  	log.Debugf("SendMessage.success")
   486  	return nil
   487  }
   488  
   489  func chunkBy(slice []*endpoint.Endpoint, chunkSize int) [][]*endpoint.Endpoint {
   490  	var chunks [][]*endpoint.Endpoint
   491  
   492  	for i := 0; i < len(slice); i += chunkSize {
   493  		end := i + chunkSize
   494  
   495  		if end > len(slice) {
   496  			end = len(slice)
   497  		}
   498  
   499  		chunks = append(chunks, slice[i:end])
   500  	}
   501  
   502  	return chunks
   503  }
   504  
   505  func findMsgZone(ep *endpoint.Endpoint, zoneNames []string) string {
   506  	for _, zone := range zoneNames {
   507  		if strings.HasSuffix(ep.DNSName, zone) {
   508  			return dns.Fqdn(zone)
   509  		}
   510  	}
   511  
   512  	log.Warnf("No available zone found for %s, set it to 'root'", ep.DNSName)
   513  	return dns.Fqdn(".")
   514  }
   515  
   516  func makeClient(r rfc2136Provider) (result *dns.Client, err error) {
   517  	c := new(dns.Client)
   518  
   519  	if r.tlsConfig.UseTLS {
   520  		log.Debug("RFC2136 Connecting via TLS")
   521  		c.Net = "tcp-tls"
   522  		tlsConfig, err := tlsutils.NewTLSConfig(
   523  			r.tlsConfig.ClientCertFilePath,
   524  			r.tlsConfig.ClientCertKeyFilePath,
   525  			r.tlsConfig.CAFilePath,
   526  			r.tlsConfig.ServerName,
   527  			r.tlsConfig.SkipTLSVerify,
   528  			// Per RFC9103
   529  			tls.VersionTLS13,
   530  		)
   531  		if err != nil {
   532  			return nil, err
   533  		}
   534  		if tlsConfig.NextProtos == nil {
   535  			// Per RFC9103
   536  			tlsConfig.NextProtos = []string{"dot"}
   537  		}
   538  		c.TLSConfig = tlsConfig
   539  	} else {
   540  		c.Net = "tcp"
   541  	}
   542  
   543  	return c, nil
   544  }