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

Add IPv6 AAAA record support to PiHole provider #4324

Merged
merged 5 commits into from
May 10, 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
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
Loading