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: Support defining records by dns zone format #1360

Merged
merged 22 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
654518e
feat: Support zonefile configuration for custom dns mapping
BenMcH Jan 31, 2024
19a5a62
docs: Update configuration.md
BenMcH Jan 31, 2024
ee1ad7f
Rename var to ok
BenMcH Jan 31, 2024
ea9077d
Linter fixes
BenMcH Jan 31, 2024
3cf218f
Remove hashes in test describe description
BenMcH Feb 1, 2024
68a1ba9
Implement PR comments; zoneFileMapping -> zone, initialize with prope…
BenMcH Feb 1, 2024
21f395e
Remove custom CNAME parsing
BenMcH Feb 1, 2024
b64d658
Utilize TTL defined in zone file
BenMcH Feb 1, 2024
c6fc0dd
Link to wikipedia's example file
BenMcH Feb 1, 2024
1ddaa60
Test to confirm that a relative zone entry without an $ORIGIN returns…
BenMcH Feb 1, 2024
463ac47
Write a test covering the $INCLUDE directive
BenMcH Feb 1, 2024
1bf1383
Write a test confirming that a dns zone can result in more than 1 RR
BenMcH Feb 1, 2024
bb67203
Linting
BenMcH Feb 1, 2024
026d513
Merge branch 'main' into zonefile
BenMcH Feb 1, 2024
36676c6
fix: Use proper matchers in CustomDNS Zone tests; Update configuratio…
BenMcH Feb 3, 2024
f86b199
Pull in config directory to support relative $INCLUDE
BenMcH Feb 3, 2024
37a39f2
Added tests to ensure the ability to use both bare filenames as well …
BenMcH Feb 3, 2024
9f9cfa1
Shorten test description (Linting error)
BenMcH Feb 3, 2024
44ed91c
Move Assignment of z.RRs to the end of the UnmarshallYAML function
BenMcH Feb 3, 2024
cab953a
Moved tests for relative $INCLUDE zones to config_test. Added test ca…
BenMcH Feb 3, 2024
7cd3eb0
Corrected test case to _actually_ test againt bare file names
BenMcH Feb 3, 2024
efa5283
Merge branch 'main' into zonefile
BenMcH Feb 9, 2024
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
42 changes: 42 additions & 0 deletions config/custom_dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,56 @@ type CustomDNS struct {
RewriterConfig `yaml:",inline"`
CustomTTL Duration `yaml:"customTTL" default:"1h"`
Mapping CustomDNSMapping `yaml:"mapping"`
ZoneFileMapping ZoneFileDNS `yaml:"zoneFileMapping" default:""`
FilterUnmappedTypes bool `yaml:"filterUnmappedTypes" default:"true"`
}

type (
CustomDNSMapping map[string]CustomDNSEntries
CustomDNSEntries []dns.RR

ZoneFileDNS CustomDNSMapping
)

func (z *ZoneFileDNS) UnmarshalYAML(unmarshal func(interface{}) error) error {
var input string
if err := unmarshal(&input); err != nil {
return err
}

result := make(ZoneFileDNS, len(input))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor copy-paste typo (1 seems sensible since you're probably at least going to have a domain if you specify the option):

Suggested change
result := make(ZoneFileDNS, len(input))
result := make(ZoneFileDNS, 1)


if input != "" {
zoneParser := dns.NewZoneParser(strings.NewReader(input), "", "")
ThinkChaos marked this conversation as resolved.
Show resolved Hide resolved
zoneParser.SetIncludeAllowed(true)

for {
zoneRR, ok := zoneParser.Next()

if !ok {
if zoneParser.Err() != nil {
return zoneParser.Err()
}

// Done
break
}

domain := zoneRR.Header().Name

if _, ok := result[domain]; !ok {
result[domain] = make(CustomDNSEntries, 0)
ThinkChaos marked this conversation as resolved.
Show resolved Hide resolved
}

result[domain] = append(result[domain], zoneRR)
}
}

*z = result

return nil
}

func (c *CustomDNSEntries) UnmarshalYAML(unmarshal func(interface{}) error) error {
var input string
if err := unmarshal(&input); err != nil {
Expand Down
71 changes: 70 additions & 1 deletion config/custom_dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package config

import (
"errors"
"fmt"
"net"
"strings"

"github.com/creasty/defaults"
"github.com/miekg/dns"
Expand Down Expand Up @@ -67,7 +69,7 @@ var _ = Describe("CustomDNSConfig", func() {
})
})

Describe("UnmarshalYAML", func() {
Describe("#CustomDNSEntries UnmarshalYAML", func() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does # do anything here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope! I will remove it when I work on this next

It("Should parse config as map", func() {
c := CustomDNSEntries{}
err := c.UnmarshalYAML(func(i interface{}) error {
Expand Down Expand Up @@ -102,4 +104,71 @@ var _ = Describe("CustomDNSConfig", func() {
Expect(err).Should(MatchError("some err"))
})
})

Describe("#ZoneFileDNS UnmarshalYAML", func() {
It("Should parse config as map", func() {
z := ZoneFileDNS{}
err := z.UnmarshalYAML(func(i interface{}) error {
*i.(*string) = strings.TrimSpace(`
$ORIGIN example.com.
www 3600 A 1.2.3.4
www6 3600 AAAA 2001:0db8:85a3:0000:0000:8a2e:0370:7334
cname 3600 CNAME www
`)

return nil
})
Expect(err).Should(Succeed())
Expect(z).Should(HaveLen(3))

for url, records := range z {
if url == "www.example.com." {
Expect(records).Should(HaveLen(1))

record, isA := records[0].(*dns.A)

Expect(isA).Should(BeTrue())
Expect(record.A).Should(Equal(net.ParseIP("1.2.3.4")))
} else if url == "www6.example.com." {
Expect(records).Should(HaveLen(1))

record, isAAAA := records[0].(*dns.AAAA)

Expect(isAAAA).Should(BeTrue())
Expect(record.AAAA).Should(Equal(net.ParseIP("2001:db8:85a3::8a2e:370:7334")))
} else if url == "cname.example.com." {
Expect(records).Should(HaveLen(1))

record, isCNAME := records[0].(*dns.CNAME)

Expect(isCNAME).Should(BeTrue())
Expect(record.Target).Should(Equal("www.example.com."))
} else {
Fail("unexpected record")
}
}
})

It("Should return an error if the zone file is malformed", func() {
z := ZoneFileDNS{}
err := z.UnmarshalYAML(func(i interface{}) error {
*i.(*string) = strings.TrimSpace(`
$ORIGIN example.com.
www A 1.2.3.4
`)

return nil
})
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("dns: missing TTL with no previous value"))
})
It("Should return an error if the unmarshall function returns an error", func() {
z := ZoneFileDNS{}
err := z.UnmarshalYAML(func(i interface{}) error {
return fmt.Errorf("Failed to unmarshal")
})
Expect(err).Should(HaveOccurred())
Expect(err).Should(MatchError("Failed to unmarshal"))
})
})
})
5 changes: 5 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ domain must be separated by a comma.
| customTTL | duration (no unit is minutes) | no | 1h |
| rewrite | string: string (domain: domain) | no | |
| mapping | string: string (hostname: address or CNAME) | no | |
| zoneFileMapping | multiline string containing a DNS Zone File | no | |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it supports already more than the simple mapping and is no file by itself I would opt for zone as config key. 🤔

| filterUnmappedTypes | boolean | no | true |

!!! example
Expand All @@ -279,6 +280,10 @@ domain must be separated by a comma.
printer.lan: 192.168.178.3
otherdevice.lan: 192.168.178.15,2001:0db8:85a3:08d3:1319:8a2e:0370:7344
anothername.lan: CNAME(otherdevice.lan)
zoneFileMapping: |
$ORIGIN example.com.
www 3600 A 1.2.3.4
@ 3600 CNAME www
```

This configuration will also resolve any subdomain of the defined domain, recursively. For example querying any of
Expand Down
17 changes: 13 additions & 4 deletions resolver/custom_dns_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,21 @@ type CustomDNSResolver struct {

// NewCustomDNSResolver creates new resolver instance
func NewCustomDNSResolver(cfg config.CustomDNS) *CustomDNSResolver {
m := make(config.CustomDNSMapping, len(cfg.Mapping))
reverse := make(map[string][]string, len(cfg.Mapping))
dnsRecords := make(config.CustomDNSMapping, len(cfg.Mapping)+len(cfg.ZoneFileMapping))

for url, entries := range cfg.Mapping {
m[strings.ToLower(url)] = entries
url = util.ExtractDomainOnly(url)
dnsRecords[url] = entries
}

for url, entries := range cfg.ZoneFileMapping {
url = util.ExtractDomainOnly(url)
dnsRecords[url] = entries
}

reverse := make(map[string][]string, len(dnsRecords))

for url, entries := range dnsRecords {
for _, entry := range entries {
a, isA := entry.(*dns.A)

Expand All @@ -59,7 +68,7 @@ func NewCustomDNSResolver(cfg config.CustomDNS) *CustomDNSResolver {
typed: withType("custom_dns"),

createAnswerFromQuestion: util.CreateAnswerFromQuestion,
mapping: m,
mapping: dnsRecords,
reverseAddresses: reverse,
}
}
Expand Down
16 changes: 16 additions & 0 deletions resolver/custom_dns_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ var _ = Describe("CustomDNSResolver", func() {
"cname.recursive": {&dns.CNAME{Target: "cname.recursive"}},
"mx.domain": {&dns.MX{Mx: "mx.domain"}},
},
ZoneFileMapping: config.ZoneFileDNS{
"example.zone.": {&dns.A{A: net.ParseIP("1.2.3.4")}},
},
CustomTTL: config.Duration(time.Duration(TTL) * time.Second),
FilterUnmappedTypes: true,
}
Expand Down Expand Up @@ -136,6 +139,19 @@ var _ = Describe("CustomDNSResolver", func() {
When("Ip 4 mapping is defined for custom domain and", func() {
Context("filterUnmappedTypes is true", func() {
BeforeEach(func() { cfg.FilterUnmappedTypes = true })
It("defined ip4 query should be resolved from zone mappings", func() {
Expect(sut.Resolve(ctx, newRequest("example.zone.", A))).
Should(
SatisfyAll(
BeDNSRecord("example.zone.", A, "1.2.3.4"),
HaveTTL(BeNumerically("==", TTL)),
HaveResponseType(ResponseTypeCUSTOMDNS),
HaveReason("CUSTOM DNS"),
HaveReturnCode(dns.RcodeSuccess),
))
// will not delegate to next resolver
m.AssertNotCalled(GinkgoT(), "Resolve", mock.Anything)
})
It("defined ip4 query should be resolved", func() {
Expect(sut.Resolve(ctx, newRequest("custom.domain.", A))).
Should(
Expand Down
Loading