diff --git a/docs/tutorials/rfc2136.md b/docs/tutorials/rfc2136.md index 2c0a43fd31..0135e107e0 100644 --- a/docs/tutorials/rfc2136.md +++ b/docs/tutorials/rfc2136.md @@ -131,6 +131,16 @@ A default TTL for all records can be set using the the flag with a time in secon There are other annotation that can affect the generation of DNS records, but these are beyond the scope of this tutorial and are covered in the main documentation. +### Generate reverse DNS records + +If you want to generate reverse DNS records for your services, you have to enable the functionality using the `--rfc2136-create-ptr` +flag. You have also to add the zone to the list of zones managed by ExternalDNS via the `--rfc2136-zone` and `--domain-filter` flags. +An example of a valid configuration is the following: + +```--domain-filter=157.168.192.in-addr.arpa --rfc2136-zone=157.168.192.in-addr.arpa``` + +PTR record tracking is managed by the A/AAAA record so you can't create PTR records for already generated A/AAAA records. + ### Test with external-dns installed on local machine (optional) You may install external-dns and test on a local machine by running: diff --git a/main.go b/main.go index 05059a12d3..f05c46906f 100644 --- a/main.go +++ b/main.go @@ -349,7 +349,7 @@ func main() { ClientCertKeyFilePath: cfg.TLSClientCertKey, ServerName: "", } - p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, cfg.RFC2136KerberosRealm, cfg.RFC2136BatchChangeSize, tlsConfig, nil) + p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, cfg.RFC2136CreatePTR, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, cfg.RFC2136KerberosRealm, cfg.RFC2136BatchChangeSize, tlsConfig, nil) case "ns1": p, err = ns1.NewNS1Provider( ns1.NS1Config{ diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 21a3e9b42f..822993d093 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -173,6 +173,7 @@ type Config struct { RFC2136Zone []string RFC2136Insecure bool RFC2136GSSTSIG bool + RFC2136CreatePTR bool RFC2136KerberosRealm string RFC2136KerberosUsername string RFC2136KerberosPassword string `secure:"yes"` @@ -551,6 +552,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("rfc2136-host", "When using the RFC2136 provider, specify the host of the DNS server").Default(defaultConfig.RFC2136Host).StringVar(&cfg.RFC2136Host) app.Flag("rfc2136-port", "When using the RFC2136 provider, specify the port of the DNS server").Default(strconv.Itoa(defaultConfig.RFC2136Port)).IntVar(&cfg.RFC2136Port) app.Flag("rfc2136-zone", "When using the RFC2136 provider, specify zone entries of the DNS server to use").StringsVar(&cfg.RFC2136Zone) + app.Flag("rfc2136-create-ptr", "When using the RFC2136 provider, enable PTR management").Default(strconv.FormatBool(defaultConfig.RFC2136CreatePTR)).BoolVar(&cfg.RFC2136CreatePTR) app.Flag("rfc2136-insecure", "When using the RFC2136 provider, specify whether to attach TSIG or not (default: false, requires --rfc2136-tsig-keyname and rfc2136-tsig-secret)").Default(strconv.FormatBool(defaultConfig.RFC2136Insecure)).BoolVar(&cfg.RFC2136Insecure) app.Flag("rfc2136-tsig-keyname", "When using the RFC2136 provider, specify the TSIG key to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGKeyName).StringVar(&cfg.RFC2136TSIGKeyName) app.Flag("rfc2136-tsig-secret", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGSecret).StringVar(&cfg.RFC2136TSIGSecret) diff --git a/pkg/apis/externaldns/validation/validation_test.go b/pkg/apis/externaldns/validation/validation_test.go index 8573aa6cf1..b0efbf7d68 100644 --- a/pkg/apis/externaldns/validation/validation_test.go +++ b/pkg/apis/externaldns/validation/validation_test.go @@ -132,6 +132,7 @@ func TestValidateBadRfc2136Config(t *testing.T) { cfg.Sources = []string{"test-source"} cfg.Provider = "rfc2136" cfg.RFC2136MinTTL = -1 + cfg.RFC2136CreatePTR = false cfg.RFC2136BatchChangeSize = 50 err := ValidateConfig(cfg) diff --git a/provider/rfc2136/rfc2136.go b/provider/rfc2136/rfc2136.go index 06773f5eb8..ba58282126 100644 --- a/provider/rfc2136/rfc2136.go +++ b/provider/rfc2136/rfc2136.go @@ -57,6 +57,7 @@ type rfc2136Provider struct { minTTL time.Duration batchChangeSize int tlsConfig TLSConfig + createPTR bool // options specific to rfc3645 gss-tsig support gssTsig bool @@ -95,7 +96,7 @@ type rfc2136Actions interface { } // NewRfc2136Provider is a factory function for OpenStack rfc2136 providers -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) { +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, createPTR bool, gssTsig bool, krb5Username string, krb5Password string, krb5Realm string, batchChangeSize int, tlsConfig TLSConfig, actions rfc2136Actions) (provider.Provider, error) { secretAlgChecked, ok := tsigAlgs[secretAlg] if !ok && !insecure && !gssTsig { return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg) @@ -120,6 +121,7 @@ func NewRfc2136Provider(host string, port int, zoneNames []string, insecure bool zoneNames: zoneNames, insecure: insecure, gssTsig: gssTsig, + createPTR: createPTR, krb5Username: krb5Username, krb5Password: krb5Password, krb5Realm: strings.ToUpper(krb5Realm), @@ -195,6 +197,9 @@ OuterLoop: case dns.TypeNS: rrValues = []string{rr.(*dns.NS).Ns} rrType = "NS" + case dns.TypePTR: + rrValues = []string{rr.(*dns.PTR).Ptr} + rrType = "PTR" default: continue // Unhandled record type } @@ -274,6 +279,35 @@ func (r rfc2136Provider) List() ([]dns.RR, error) { return records, nil } +func (r rfc2136Provider) AddReverseRecord(ip string, hostname string) error { + changes := r.GenerateReverseRecord(ip, hostname) + return r.ApplyChanges(context.Background(), &plan.Changes{Create: changes}) +} + +func (r rfc2136Provider) RemoveReverseRecord(ip string, hostname string) error { + changes := r.GenerateReverseRecord(ip, hostname) + return r.ApplyChanges(context.Background(), &plan.Changes{Delete: changes}) +} + +func (r rfc2136Provider) GenerateReverseRecord(ip string, hostname string) []*endpoint.Endpoint { + // Find the zone for the PTR record + // zone := findMsgZone(&endpoint.Endpoint{DNSName: ip}, p.ptrZoneNames) + // Generate PTR notation record starting from the IP address + var records []*endpoint.Endpoint + + log.Debugf("Reverse zone is: %s %s", ip, dns.Fqdn(ip)) + reverseAddress, _ := dns.ReverseAddr(ip) + + // PTR + records = append(records, &endpoint.Endpoint{ + DNSName: reverseAddress[:len(reverseAddress)-1], + RecordType: "PTR", + Targets: endpoint.Targets{hostname}, + }) + + return records +} + // ApplyChanges applies a given set of changes in a given zone. func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { log.Debugf("ApplyChanges (Create: %d, UpdateOld: %d, UpdateNew: %d, Delete: %d)", len(changes.Create), len(changes.UpdateOld), len(changes.UpdateNew), len(changes.Delete)) @@ -300,6 +334,10 @@ func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes m[zone].SetUpdate(zone) r.AddRecord(m[zone], ep) + + if r.createPTR && (ep.RecordType == "A" || ep.RecordType == "AAAA") { + r.AddReverseRecord(ep.Targets[0], ep.DNSName) + } } // only send if there are records available @@ -335,6 +373,10 @@ func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes m[zone].SetUpdate(zone) r.UpdateRecord(m[zone], changes.UpdateOld[i], ep) + if r.createPTR && (ep.RecordType == "A" || ep.RecordType == "AAAA") { + r.RemoveReverseRecord(changes.UpdateOld[i].Targets[0], ep.DNSName) + r.AddReverseRecord(ep.Targets[0], ep.DNSName) + } } // only send if there are records available @@ -369,6 +411,9 @@ func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes m[zone].SetUpdate(zone) r.RemoveRecord(m[zone], ep) + if r.createPTR && (ep.RecordType == "A" || ep.RecordType == "AAAA") { + r.RemoveReverseRecord(ep.Targets[0], ep.DNSName) + } } // only send if there are records available diff --git a/provider/rfc2136/rfc2136_test.go b/provider/rfc2136/rfc2136_test.go index fe1b96bfe3..efaa79d183 100644 --- a/provider/rfc2136/rfc2136_test.go +++ b/provider/rfc2136/rfc2136_test.go @@ -127,11 +127,24 @@ func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) { ClientCertFilePath: "", ClientCertKeyFilePath: "", } - return NewRfc2136Provider("", 0, nil, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub) + return NewRfc2136Provider("", 0, nil, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", 50, tlsConfig, stub) } func createRfc2136TLSStubProvider(stub *rfc2136Stub, tlsConfig TLSConfig) (provider.Provider, error) { - return NewRfc2136Provider("rfc2136-host", 0, nil, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub) + return NewRfc2136Provider("rfc2136-host", 0, nil, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", 50, tlsConfig, stub) +} + +func createRfc2136StubProviderWithReverse(stub *rfc2136Stub) (provider.Provider, error) { + tlsConfig := TLSConfig{ + UseTLS: false, + SkipTLSVerify: false, + CAFilePath: "", + ClientCertFilePath: "", + ClientCertKeyFilePath: "", + } + + zones := []string{"foo.com", "3.2.1.in-addr.arpa"} + return NewRfc2136Provider("", 0, zones, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{Filters: zones}, false, 300*time.Second, true, false, "", "", "", 50, tlsConfig, stub) } func createRfc2136StubProviderWithZones(stub *rfc2136Stub) (provider.Provider, error) { @@ -143,7 +156,7 @@ func createRfc2136StubProviderWithZones(stub *rfc2136Stub) (provider.Provider, e ClientCertKeyFilePath: "", } zones := []string{"foo.com", "foobar.com"} - return NewRfc2136Provider("", 0, zones, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub) + return NewRfc2136Provider("", 0, zones, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", 50, tlsConfig, stub) } func createRfc2136StubProviderWithZonesFilters(stub *rfc2136Stub) (provider.Provider, error) { @@ -155,7 +168,7 @@ func createRfc2136StubProviderWithZonesFilters(stub *rfc2136Stub) (provider.Prov ClientCertKeyFilePath: "", } zones := []string{"foo.com", "foobar.com"} - return NewRfc2136Provider("", 0, zones, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{Filters: zones}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub) + return NewRfc2136Provider("", 0, zones, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{Filters: zones}, false, 300*time.Second, false, false, "", "", "", 50, tlsConfig, stub) } func extractUpdateSectionFromMessage(msg fmt.Stringer) []string { @@ -196,6 +209,27 @@ func TestRfc2136GetRecordsMultipleTargets(t *testing.T) { assert.Equal(t, 0, len(recs[0].ProviderSpecific), "expected no provider specific config") } +func TestRfc2136PTRCreation(t *testing.T) { + stub := newStub() + provider, err := createRfc2136StubProviderWithReverse(stub) + assert.NoError(t, err) + + err = provider.ApplyChanges(context.Background(), &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "demo.foo.com", + RecordType: "A", + Targets: []string{"1.2.3.4"}, + }, + }, + }) + assert.NoError(t, err) + assert.Equal(t, 2, len(stub.createMsgs), "expected two records, one A and one PTR") + createMsgs := getSortedChanges(stub.createMsgs) + assert.True(t, strings.Contains(strings.Join(strings.Fields(createMsgs[0]), " "), "4.3.2.1.in-addr.arpa. 300 IN PTR demo.foo.com."), "excpeted a PTR record") + assert.True(t, strings.Contains(strings.Join(strings.Fields(createMsgs[1]), " "), "demo.foo.com. 300 IN A 1.2.3.4"), "expected an A record") +} + func TestRfc2136TLSConfig(t *testing.T) { stub := newStub() diff --git a/source/ingress_test.go b/source/ingress_test.go index 6198f577b2..c1b58210fd 100644 --- a/source/ingress_test.go +++ b/source/ingress_test.go @@ -267,7 +267,7 @@ func testEndpointsFromIngress(t *testing.T) { { title: "invalid hostname does not generate endpoints", ingress: fakeIngress{ - dnsnames: []string{"this-is-an-exceedingly-long-label-that-external-dns-should-reject.example.org"}, + dnsnames: []string{"this-is-an-exceedingly-long-label-that-external-dns-should-reject.example.org"}, }, expected: []*endpoint.Endpoint{}, },