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 18 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
9 changes: 8 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,14 @@ func loadConfig(logger *logrus.Entry, path string, mandatory bool) (rCfg *Config
return nil, fmt.Errorf("can't read config file(s): %w", err)
}

var data []byte
var (
data []byte
prettyPath string
)

if fs.IsDir() {
prettyPath = filepath.Join(path, "*")

data, err = readFromDir(path, data)

if err != nil {
Expand All @@ -470,6 +475,8 @@ func loadConfig(logger *logrus.Entry, path string, mandatory bool) (rCfg *Config
}
}

cfg.CustomDNS.Zone.configPath = prettyPath
ThinkChaos marked this conversation as resolved.
Show resolved Hide resolved

err = unmarshalConfig(logger, data, &cfg)
if err != nil {
return nil, err
Expand Down
94 changes: 51 additions & 43 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"`
Zone ZoneFileDNS `yaml:"zone" default:""`
FilterUnmappedTypes bool `yaml:"filterUnmappedTypes" default:"true"`
}

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

ZoneFileDNS struct {
RRs CustomDNSMapping
configPath string
}
)

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

z.RRs = make(CustomDNSMapping)
result := z.RRs
ThinkChaos marked this conversation as resolved.
Show resolved Hide resolved

zoneParser := dns.NewZoneParser(strings.NewReader(input), "", z.configPath)
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, 1)
}

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

return nil
}

func (c *CustomDNSEntries) UnmarshalYAML(unmarshal func(interface{}) error) error {
var input string
if err := unmarshal(&input); err != nil {
Expand All @@ -30,24 +72,16 @@ func (c *CustomDNSEntries) UnmarshalYAML(unmarshal func(interface{}) error) erro

parts := strings.Split(input, ",")
result := make(CustomDNSEntries, len(parts))
containsCNAME := false

for i, part := range parts {
rr, err := configToRR(part)
if err != nil {
return err
}

_, isCNAME := rr.(*dns.CNAME)
containsCNAME = containsCNAME || isCNAME

result[i] = rr
}

if containsCNAME && len(result) > 1 {
return fmt.Errorf("when a CNAME record is present, it must be the only record in the mapping")
}

*c = result

return nil
Expand All @@ -70,47 +104,21 @@ func (c *CustomDNS) LogConfig(logger *logrus.Entry) {
}
}

func removePrefixSuffix(in, prefix string) string {
in = strings.TrimPrefix(in, fmt.Sprintf("%s(", prefix))
in = strings.TrimSuffix(in, ")")

return strings.TrimSpace(in)
}

func configToRR(part string) (dns.RR, error) {
if strings.HasPrefix(part, "CNAME(") {
domain := removePrefixSuffix(part, "CNAME")
domain = dns.Fqdn(domain)
cname := &dns.CNAME{Target: domain}

return cname, nil
func configToRR(ipStr string) (dns.RR, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("invalid IP address '%s'", ipStr)
}

// Fall back to A/AAAA records to maintain backwards compatibility in config.yml
// We will still remove the A() or AAAA() if it exists
if strings.Contains(part, ".") { // IPV4 address
ipStr := removePrefixSuffix(part, "A")
ip := net.ParseIP(ipStr)

if ip == nil {
return nil, fmt.Errorf("invalid IP address '%s'", part)
}

if ip.To4() != nil {
a := new(dns.A)
a.A = ip

return a, nil
} else { // IPV6 address
ipStr := removePrefixSuffix(part, "AAAA")
ip := net.ParseIP(ipStr)

if ip == nil {
return nil, fmt.Errorf("invalid IP address '%s'", part)
}
}

aaaa := new(dns.AAAA)
aaaa.AAAA = ip
aaaa := new(dns.AAAA)
aaaa.AAAA = ip

return aaaa, nil
}
return aaaa, nil
}
185 changes: 173 additions & 12 deletions config/custom_dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package config

import (
"errors"
"fmt"
"net"
"path/filepath"
"strings"

. "github.com/0xERR0R/blocky/helpertest"
"github.com/creasty/defaults"
"github.com/miekg/dns"
. "github.com/onsi/ginkgo/v2"
Expand All @@ -25,7 +29,6 @@ var _ = Describe("CustomDNSConfig", func() {
&dns.A{A: net.ParseIP("192.168.143.125")},
&dns.AAAA{AAAA: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")},
},
"cname.domain": {&dns.CNAME{Target: "custom.domain"}},
},
}
})
Expand Down Expand Up @@ -62,12 +65,11 @@ var _ = Describe("CustomDNSConfig", func() {
ContainSubstring("custom.domain = "),
ContainSubstring("ip6.domain = "),
ContainSubstring("multiple.ips = "),
ContainSubstring("cname.domain = "),
))
})
})

Describe("UnmarshalYAML", func() {
Describe("CustomDNSEntries UnmarshalYAML", func() {
It("Should parse config as map", func() {
c := CustomDNSEntries{}
err := c.UnmarshalYAML(func(i interface{}) error {
Expand All @@ -82,24 +84,183 @@ var _ = Describe("CustomDNSConfig", func() {
Expect(aRecord.A).Should(Equal(net.ParseIP("1.2.3.4")))
})

It("Should return an error if a CNAME is accomanied by any other record", func() {
c := CustomDNSEntries{}
It("should fail if wrong YAML format", func() {
c := &CustomDNSEntries{}
err := c.UnmarshalYAML(func(i interface{}) error {
*i.(*string) = "CNAME(example.com),A(1.2.3.4)"
return errors.New("some err")
})
Expect(err).Should(HaveOccurred())
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
www 3600 AAAA 2001:0db8:85a3:0000:0000:8a2e:0370:7334
www6 3600 AAAA 2001:0db8:85a3:0000:0000:8a2e:0370:7334
cname 3600 CNAME www
`)

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

Expect(z.RRs["www.example.com."]).
Should(SatisfyAll(
HaveLen(2),
ContainElements(
SatisfyAll(
BeDNSRecord("www.example.com.", A, "1.2.3.4"),
HaveTTL(BeNumerically("==", 3600)),
),
SatisfyAll(
BeDNSRecord("www.example.com.", AAAA, "2001:db8:85a3::8a2e:370:7334"),
HaveTTL(BeNumerically("==", 3600)),
))))

Expect(z.RRs["www6.example.com."]).
Should(SatisfyAll(
HaveLen(1),
ContainElements(
SatisfyAll(
BeDNSRecord("www6.example.com.", AAAA, "2001:db8:85a3::8a2e:370:7334"),
HaveTTL(BeNumerically("==", 3600)),
))))

Expect(z.RRs["cname.example.com."]).
Should(SatisfyAll(
HaveLen(1),
ContainElements(
SatisfyAll(
BeDNSRecord("cname.example.com.", CNAME, "www.example.com."),
HaveTTL(BeNumerically("==", 3600)),
))))
})

It("Should support the $INCLUDE directive with a bare filename", func() {
folder := NewTmpFolder("zones")
folder.CreateStringFile("other.zone", `
www 3600 A 1.2.3.4
`)

z := ZoneFileDNS{configPath: filepath.Join(folder.Path, "*")}
err := z.UnmarshalYAML(func(i interface{}) error {
*i.(*string) = strings.TrimSpace(`
$ORIGIN example.com.
$INCLUDE other.zone`)

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

Expect(z.RRs["www.example.com."]).
Should(SatisfyAll(

HaveLen(1),
ContainElements(
SatisfyAll(
BeDNSRecord("www.example.com.", A, "1.2.3.4"),
HaveTTL(BeNumerically("==", 3600)),
)),
))
})
It("Should support the $INCLUDE directive with a relative filename", func() {
folder := NewTmpFolder("zones")
folder.CreateStringFile("other.zone", `
www 3600 A 1.2.3.4
`)

z := ZoneFileDNS{configPath: filepath.Join(folder.Path, "*")}
err := z.UnmarshalYAML(func(i interface{}) error {
*i.(*string) = strings.TrimSpace(`
$ORIGIN example.com.
$INCLUDE ./other.zone`)

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

Expect(z.RRs["www.example.com."]).
Should(SatisfyAll(

HaveLen(1),
ContainElements(
SatisfyAll(
BeDNSRecord("www.example.com.", A, "1.2.3.4"),
HaveTTL(BeNumerically("==", 3600)),
)),
))
})
It("Should support the $INCLUDE directive with an absolute path", func() {
folder := NewTmpFolder("zones")
file := folder.CreateStringFile("other.zone", `
www 3600 A 1.2.3.4
`)

z := ZoneFileDNS{}
err := z.UnmarshalYAML(func(i interface{}) error {
*i.(*string) = strings.TrimSpace(`
$ORIGIN example.com.
$INCLUDE ` + file.Path)

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

Expect(z.RRs["www.example.com."]).
Should(SatisfyAll(

HaveLen(1),
ContainElements(
SatisfyAll(
BeDNSRecord("www.example.com.", A, "1.2.3.4"),
HaveTTL(BeNumerically("==", 3600)),
)),
))
})

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).Should(MatchError("when a CNAME record is present, it must be the only record in the mapping"))
Expect(err.Error()).Should(ContainSubstring("dns: missing TTL with no previous value"))
})
It("Should return an error if a relative record is provided without an origin", func() {
z := ZoneFileDNS{}
err := z.UnmarshalYAML(func(i interface{}) error {
*i.(*string) = strings.TrimSpace(`
$TTL 3600
www A 1.2.3.4
`)

It("should fail if wrong YAML format", func() {
c := &CustomDNSEntries{}
err := c.UnmarshalYAML(func(i interface{}) error {
return errors.New("some err")
return nil
})
Expect(err).Should(HaveOccurred())
Expect(err).Should(MatchError("some err"))
Expect(err.Error()).Should(ContainSubstring("dns: bad owner name: \"www\""))
})
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"))
})
})
})
1 change: 0 additions & 1 deletion docs/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ customDNS:
example.com: printer.lan
mapping:
printer.lan: 192.168.178.3,2001:0db8:85a3:08d3:1319:8a2e:0370:7344
second-printer-address.lan: CNAME(printer.lan)

# optional: definition, which DNS resolver(s) should be used for queries to the domain (with all sub-domains). Multiple resolvers must be separated by a comma
# Example: Query client.fritz.box will ask DNS server 192.168.178.1. This is necessary for local network, to resolve clients by host name
Expand Down
Loading
Loading