Skip to content

Commit

Permalink
Merge pull request #4324 from PseudoResonance/master
Browse files Browse the repository at this point in the history
Add IPv6 AAAA record support to PiHole provider
  • Loading branch information
k8s-ci-robot committed May 10, 2024
2 parents ffbd06a + b4d76a9 commit fac5b44
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 30 deletions.
4 changes: 2 additions & 2 deletions docs/tutorials/pihole.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Setting up ExternalDNS for Pi-hole

This tutorial describes how to setup ExternalDNS to sync records with Pi-hole's Custom DNS.
Pi-hole has an internal list it checks last when resolving requests. This list can contain any number of arbitrary A or CNAME records.
Pi-hole has an internal list it checks last when resolving requests. This list can contain any number of arbitrary A, AAAA or CNAME records.
There is a pseudo-API exposed that ExternalDNS is able to use to manage these records.

__NOTE:__ Your Pi-hole must be running [version 5.9 or newer](https://pi-hole.net/blog/2022/02/12/pi-hole-ftl-v5-14-web-v5-11-and-core-v5-9-released).
Expand Down Expand Up @@ -91,7 +91,7 @@ spec:
args:
- --source=service
- --source=ingress
# Pihole only supports A/CNAME records so there is no mechanism to track ownership.
# Pihole only supports A/AAAA/CNAME records so there is no mechanism to track ownership.
# You don't need to set this flag, but if you leave it unset, you will receive warning
# logs when ExternalDNS attempts to create TXT records.
- --registry=noop
Expand Down
15 changes: 13 additions & 2 deletions provider/pihole/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,24 @@ func (p *piholeClient) listRecords(ctx context.Context, rtype string) ([]*endpoi
if !ok {
return out, nil
}
loop:
for _, rec := range data {
name := rec[0]
target := rec[1]
if !p.cfg.DomainFilter.Match(name) {
log.Debugf("Skipping %s that does not match domain filter", name)
continue
}
switch rtype {
case endpoint.RecordTypeA:
if strings.Contains(target, ":") {
continue loop
}
case endpoint.RecordTypeAAAA:
if strings.Contains(target, ".") {
continue loop
}
}
out = append(out, &endpoint.Endpoint{
DNSName: name,
Targets: []string{target},
Expand Down Expand Up @@ -180,7 +191,7 @@ func (p *piholeClient) cnameRecordsScript() string {

func (p *piholeClient) urlForRecordType(rtype string) (string, error) {
switch rtype {
case endpoint.RecordTypeA:
case endpoint.RecordTypeA, endpoint.RecordTypeAAAA:
return p.aRecordsScript(), nil
case endpoint.RecordTypeCNAME:
return p.cnameRecordsScript(), nil
Expand Down Expand Up @@ -287,7 +298,7 @@ func (p *piholeClient) newDNSActionForm(action string, ep *endpoint.Endpoint) *u
form.Add("action", action)
form.Add("domain", ep.DNSName)
switch ep.RecordType {
case endpoint.RecordTypeA:
case endpoint.RecordTypeA, endpoint.RecordTypeAAAA:
form.Add("ip", ep.Targets[0])
case endpoint.RecordTypeCNAME:
form.Add("target", ep.Targets[0])
Expand Down
80 changes: 79 additions & 1 deletion provider/pihole/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,16 @@ func TestListRecords(t *testing.T) {
`))
return
}
// Pihole makes no distinction between A and AAAA records
w.Write([]byte(`
{
"data": [
["test1.example.com", "192.168.1.1"],
["test2.example.com", "192.168.1.2"],
["test3.match.com", "192.168.1.3"]
["test3.match.com", "192.168.1.3"],
["test1.example.com", "fc00::1:192:168:1:1"],
["test2.example.com", "fc00::1:192:168:1:2"],
["test3.match.com", "fc00::1:192:168:1:3"]
]
}
`))
Expand Down Expand Up @@ -157,6 +161,29 @@ func TestListRecords(t *testing.T) {
}
}

// Test retrieve AAAA records unfiltered
arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeAAAA)
if err != nil {
t.Fatal(err)
}
if len(arecs) != 3 {
t.Fatal("Expected 3 AAAA records returned, got:", len(arecs))
}
// Ensure records were parsed correctly
expected = [][]string{
{"test1.example.com", "fc00::1:192:168:1:1"},
{"test2.example.com", "fc00::1:192:168:1:2"},
{"test3.match.com", "fc00::1:192:168:1:3"},
}
for idx, rec := range arecs {
if rec.DNSName != expected[idx][0] {
t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
}
if rec.Targets[0] != expected[idx][1] {
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
}
}

// Test retrieve CNAME records unfiltered
cnamerecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeCNAME)
if err != nil {
Expand Down Expand Up @@ -209,6 +236,27 @@ func TestListRecords(t *testing.T) {
}
}

// Test retrieve AAAA records filtered
arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeAAAA)
if err != nil {
t.Fatal(err)
}
if len(arecs) != 1 {
t.Fatal("Expected 1 AAAA record returned, got:", len(arecs))
}
// Ensure records were parsed correctly
expected = [][]string{
{"test3.match.com", "fc00::1:192:168:1:3"},
}
for idx, rec := range arecs {
if rec.DNSName != expected[idx][0] {
t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
}
if rec.Targets[0] != expected[idx][1] {
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
}
}

// Test retrieve CNAME records filtered
cnamerecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeCNAME)
if err != nil {
Expand Down Expand Up @@ -246,6 +294,11 @@ func TestCreateRecord(t *testing.T) {
if r.Form.Get("ip") != ep.Targets[0] {
t.Error("Invalid ip in form:", r.Form.Get("ip"), "Expected:", ep.Targets[0])
}
// Pihole makes no distinction between A and AAAA records
case endpoint.RecordTypeAAAA:
if r.Form.Get("ip") != ep.Targets[0] {
t.Error("Invalid ip in form:", r.Form.Get("ip"), "Expected:", ep.Targets[0])
}
case endpoint.RecordTypeCNAME:
if r.Form.Get("target") != ep.Targets[0] {
t.Error("Invalid target in form:", r.Form.Get("target"), "Expected:", ep.Targets[0])
Expand Down Expand Up @@ -281,6 +334,16 @@ func TestCreateRecord(t *testing.T) {
t.Fatal(err)
}

// Test create AAAA record
ep = &endpoint.Endpoint{
DNSName: "test.example.com",
Targets: []string{"fc00::1:192:168:1:1"},
RecordType: endpoint.RecordTypeAAAA,
}
if err := cl.createRecord(context.Background(), ep); err != nil {
t.Fatal(err)
}

// Test create CNAME record
ep = &endpoint.Endpoint{
DNSName: "test.example.com",
Expand All @@ -307,6 +370,11 @@ func TestDeleteRecord(t *testing.T) {
if r.Form.Get("ip") != ep.Targets[0] {
t.Error("Invalid ip in form:", r.Form.Get("ip"), "Expected:", ep.Targets[0])
}
// Pihole makes no distinction between A and AAAA records
case endpoint.RecordTypeAAAA:
if r.Form.Get("ip") != ep.Targets[0] {
t.Error("Invalid ip in form:", r.Form.Get("ip"), "Expected:", ep.Targets[0])
}
case endpoint.RecordTypeCNAME:
if r.Form.Get("target") != ep.Targets[0] {
t.Error("Invalid target in form:", r.Form.Get("target"), "Expected:", ep.Targets[0])
Expand Down Expand Up @@ -342,6 +410,16 @@ func TestDeleteRecord(t *testing.T) {
t.Fatal(err)
}

// Test delete AAAA record
ep = &endpoint.Endpoint{
DNSName: "test.example.com",
Targets: []string{"fc00::1:192:168:1:1"},
RecordType: endpoint.RecordTypeAAAA,
}
if err := cl.deleteRecord(context.Background(), ep); err != nil {
t.Fatal(err)
}

// Test delete CNAME record
ep = &endpoint.Endpoint{
DNSName: "test.example.com",
Expand Down
5 changes: 5 additions & 0 deletions provider/pihole/pihole.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,15 @@ func (p *PiholeProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err
if err != nil {
return nil, err
}
aaaaRecords, err := p.api.listRecords(ctx, endpoint.RecordTypeAAAA)
if err != nil {
return nil, err
}
cnameRecords, err := p.api.listRecords(ctx, endpoint.RecordTypeCNAME)
if err != nil {
return nil, err
}
aRecords = append(aRecords, aaaaRecords...)
return append(aRecords, cnameRecords...), nil
}

Expand Down
Loading

0 comments on commit fac5b44

Please sign in to comment.