From 8abe3dbc632e79bb9ffa0b1b4e6545858c2e9ebb Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 27 Oct 2021 10:30:39 +1000 Subject: [PATCH] Adds internal dns responses for some common record types --- .version | 2 +- README.md | 22 +++++- config.json.example | 9 +++ internal/config/router_config.go | 27 ++++++-- internal/server/handler.go | 115 +++++++++++++++++++++++++------ 5 files changed, 145 insertions(+), 30 deletions(-) diff --git a/.version b/.version index 81340c7..1750564 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.0.4 +0.0.6 diff --git a/README.md b/README.md index 9a4a120..8059840 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ example `dnsmasq` will send DNS requests to all servers regardless of the rules and when an upstream DNS server is dropping connections it will hang the whole server. +This service can also answer with internal DNS entries, avoiding the +need for host file modifications. + ## Installation Only the following methods are available for installation. For all other systems, see Building below. @@ -80,6 +83,15 @@ Given the following configuration: "host": "127.0.0.1", "port": 53, "default_upstream": "1.1.1.1", + "internal": [ + { + "regex": "mail.example.com", + "A": "192.168.0.10", + "AAAA": "2001:db8::1234:5678", + "TXT": "omgyesitworked", + "MX": "10 mailserver1.example.com.\n20 mailserver2.example.com." + } + ], "upstreams": [ { "regex": "local", @@ -106,7 +118,7 @@ Given the following configuration: *Requesting DNS for `test.example.com`* 1. DNS client connects to `dnsrouter` and asks for `test.example.com` -2. `dnsrouter` matches with the 2nd rule +2. `dnsrouter` matches with the 2nd _upstream_ rule 3. `dnsrouter` forwards the DNS question to upstream DNS server `8.8.8.8` 4. `dnsrouter` returns the answer to the DNS client @@ -120,7 +132,7 @@ Given the following configuration: *Requesting DNS for `local`* 1. DNS client connects to `dnsrouter` and asks for `local` -2. `dnsrouter` matches with the 1st rule +2. `dnsrouter` matches with the 1st _upstream_ rule 3. `dnsrouter` returns an error to the client with NXDOMAIN *Requesting DNS for `myoffice.com`* @@ -132,6 +144,12 @@ Given the following configuration: _Note: This is a trick example. The domain matching regex will match `*.myoffice.com` but not `myoffice.com`_ +*Requesting DNS for `mail.example.com`* + +1. DNS client connects to `dnsrouter` and asks for `mail.example.com` +2. `dnsrouter` matches with the 1st _internal_ rule +3. `dnsrouter` returns the answer value to the DNS client with the A/AAAA/MX/TXT record as requested + ## Building ```bash diff --git a/config.json.example b/config.json.example index c76c0a2..8c74396 100644 --- a/config.json.example +++ b/config.json.example @@ -8,6 +8,15 @@ "host": "127.0.0.1", "port": 53, "default_upstream": "1.1.1.1", + "internal": [ + { + "regex": "mail.example.com", + "A": "192.168.0.10", + "AAAA": "2001:db8::1234:5678", + "TXT": "omgyesitworked", + "MX": "10 mailserver1.example.com.\n20 mailserver2.example.com." + } + ], "upstreams": [ { "regex": "local", diff --git a/internal/config/router_config.go b/internal/config/router_config.go index 213cdde..2efbf30 100644 --- a/internal/config/router_config.go +++ b/internal/config/router_config.go @@ -24,10 +24,11 @@ type ServerConfig struct { // RouterConfig is the settings that exist in the config file type RouterConfig struct { - Host string `json:"host"` - Port int `json:"port"` - Upstreams []UpstreamConfig `json:"upstreams"` - DefaultUpstream string `json:"default_upstream"` + Host string `json:"host"` + Port int `json:"port"` + Upstreams []UpstreamConfig `json:"upstreams"` + InternalRecords []InternalRecordConfig `json:"internal"` + DefaultUpstream string `json:"default_upstream"` } // LogConfig is self explanatatory @@ -44,6 +45,16 @@ type UpstreamConfig struct { CompiledRegex *regexp.Regexp `json:"-"` } +// InternalRecordConfig is a item for internal dns record configuration +type InternalRecordConfig struct { + HostRegex string `json:"regex"` + A string `json:"A"` + AAAA string `json:"AAAA"` + MX string `json:"MX"` + TXT string `json:"TXT"` + CompiledRegex *regexp.Regexp `json:"-"` +} + // NewServerConfig will create a new config instance and load it from the config file with defaults func NewServerConfig() ServerConfig { s := ServerConfig{ @@ -125,16 +136,22 @@ func (s *ServerConfig) Load() { // CompileRegexes prepares the regexes given ahead of their usage func (s *ServerConfig) CompileRegexes() { regexCount := 0 + iRegexCount := 0 if len(s.Servers) > 0 { for sIdx, server := range s.Servers { for rIdx, upstream := range server.Upstreams { s.Servers[sIdx].Upstreams[rIdx].CompiledRegex = regexp.MustCompile(fmt.Sprintf("^%s\\.$", upstream.HostRegex)) regexCount++ } + + for iIdx, internalRecord := range server.InternalRecords { + s.Servers[sIdx].InternalRecords[iIdx].CompiledRegex = regexp.MustCompile(fmt.Sprintf("^%s\\.$", internalRecord.HostRegex)) + iRegexCount++ + } } } - logger.Info("Compiled %d upstream regexes from %d servers", regexCount, len(s.Servers)) + logger.Info("Compiled %d upstream regexes and %d internal record regexes from %d servers", regexCount, iRegexCount, len(s.Servers)) } // Check will ensure that the servers defined are not duplicated diff --git a/internal/server/handler.go b/internal/server/handler.go index fe1d9bd..f935ac1 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -3,6 +3,7 @@ package server import ( "fmt" "net" + "strings" "sync" "time" @@ -45,32 +46,39 @@ func (h *DNSHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { if found { // Using our cached answer msg.Answer = cacheItem.([]dns.RR) - logger.Debug("[%d] DNSLookup %s -> cached", h.ServerIndex, domain) + logger.Debug("[%d] DNSLookup %s %s -> cached", h.ServerIndex, domain, getRecordTypeString(msg.Question[0].Qtype)) } else { - upstreamHost := getDNSServerFromLookup(h.RouterConf, domain) - logger.Debug("[%d] DNSLookup %s -> %s", h.ServerIndex, domain, upstreamHost) - - if upstreamHost == "nxdomain" { - // Return nxdomain asap - msg.SetRcode(r, dns.RcodeNameError) + // Look up from internal first + internalAnswer := getDNSAnswerFromInternal(h.RouterConf, msg, domain, h.ServerIndex) + if len(internalAnswer) > 0 { + msg.Answer = internalAnswer } else { - // Forward to the determined upstream dns server - m := new(dns.Msg) - m.SetQuestion(dns.Fqdn(domain), msg.Question[0].Qtype) - m.RecursionDesired = true - - upstreamResponse, _, err := c.Exchange(m, net.JoinHostPort(upstreamHost, "53")) - if upstreamResponse == nil { - logger.Error("UpstreamError", err) - return - } + // use upstream next + upstreamHost := getDNSServerFromLookup(h.RouterConf, domain) + logger.Debug("[%d] DNSLookup %s %s -> %s", h.ServerIndex, domain, getRecordTypeString(msg.Question[0].Qtype), upstreamHost) - if upstreamResponse.Rcode != dns.RcodeSuccess { - msg.SetRcode(r, upstreamResponse.Rcode) + if upstreamHost == "nxdomain" { + // Return nxdomain asap + msg.SetRcode(r, dns.RcodeNameError) } else { - msg.Answer = upstreamResponse.Answer - // Cache it - memCache.Set(cacheKey, upstreamResponse.Answer, cache.DefaultExpiration) + // Forward to the determined upstream dns server + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(domain), msg.Question[0].Qtype) + m.RecursionDesired = true + + upstreamResponse, _, err := c.Exchange(m, net.JoinHostPort(upstreamHost, "53")) + if upstreamResponse == nil { + logger.Error("UpstreamError", err) + return + } + + if upstreamResponse.Rcode != dns.RcodeSuccess { + msg.SetRcode(r, upstreamResponse.Rcode) + } else { + msg.Answer = upstreamResponse.Answer + // Cache it + memCache.Set(cacheKey, upstreamResponse.Answer, cache.DefaultExpiration) + } } } } @@ -98,3 +106,66 @@ func getDNSServerFromLookup(conf config.RouterConfig, domain string) string { return dnsServer } + +func getRecordTypeString(recordType uint16) string { + switch recordType { + case dns.TypeA: + return "A" + case dns.TypeAAAA: + return "AAAA" + case dns.TypeMX: + return "MX" + case dns.TypeTXT: + return "TXT" + default: + return fmt.Sprintf("%v", recordType) + } +} + +func getDNSAnswerFromInternal(conf config.RouterConfig, m dns.Msg, domain string, serverIdx int) []dns.RR { + if len(conf.InternalRecords) > 0 { + rr := make([]dns.RR, 0) + for _, internalRecord := range conf.InternalRecords { + if found := internalRecord.CompiledRegex.MatchString(domain); found { + switch m.Question[0].Qtype { + case dns.TypeA: + if internalRecord.A != "" { + ip := net.ParseIP(internalRecord.A) + rr = append(rr, &dns.A{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET}, A: ip}) + logger.Debug("[%d] DNSLookup %s %s -> %s", serverIdx, domain, getRecordTypeString(m.Question[0].Qtype), internalRecord.A) + } + case dns.TypeAAAA: + if internalRecord.AAAA != "" { + ip := net.ParseIP(internalRecord.AAAA) + rr = append(rr, &dns.AAAA{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET}, AAAA: ip}) + logger.Debug("[%d] DNSLookup %s %s -> %s", serverIdx, domain, getRecordTypeString(m.Question[0].Qtype), internalRecord.AAAA) + } + case dns.TypeTXT: + if internalRecord.TXT != "" { + rr = append(rr, &dns.TXT{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET}, Txt: []string{internalRecord.TXT}}) + logger.Debug("[%d] DNSLookup %s %s -> %s", serverIdx, domain, getRecordTypeString(m.Question[0].Qtype), internalRecord.TXT) + } + case dns.TypeMX: + if internalRecord.MX != "" { + lines := strings.Split(internalRecord.MX, "\n") + for _, line := range lines { + if line != "" { + d := fmt.Sprintf("%s 0 IN MX %s", m.Question[0].Name, line) + if mx, err := dns.NewRR(d); err == nil { + rr = append(rr, mx) + } + } + } + logger.Debug("[%d] DNSLookup %s %s -> %s", serverIdx, domain, getRecordTypeString(m.Question[0].Qtype), internalRecord.MX) + } + } + } + if len(rr) > 0 { + break + } + } + return rr + } + + return nil +}