Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rfc2136): add PTR optional support #4283

Merged
merged 9 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/tutorials/rfc2136.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,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:
```external-dns --txt-owner-id k8s --provider rfc2136 --rfc2136-host=192.168.0.1 --rfc2136-port=53 --rfc2136-zone=k8s.example.org --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= --rfc2136-tsig-secret-alg=hmac-sha256 --rfc2136-tsig-keyname=externaldns-key --rfc2136-tsig-axfr --source ingress --once --domain-filter=k8s.example.org --dry-run```
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,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{
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ type Config struct {
RFC2136Zone []string
RFC2136Insecure bool
RFC2136GSSTSIG bool
RFC2136CreatePTR bool
RFC2136KerberosRealm string
RFC2136KerberosUsername string
RFC2136KerberosPassword string `secure:"yes"`
Expand Down Expand Up @@ -583,6 +584,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)
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/externaldns/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 46 additions & 1 deletion provider/rfc2136/rfc2136.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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),
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 38 additions & 4 deletions provider/rfc2136/rfc2136_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion source/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
},
Expand Down
Loading