Skip to content

Commit

Permalink
Adds internal dns responses for some common record types
Browse files Browse the repository at this point in the history
  • Loading branch information
jc21 committed Oct 27, 2021
1 parent 2191d0b commit 8abe3db
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.4
0.0.6
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand All @@ -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

Expand All @@ -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`*
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 22 additions & 5 deletions internal/config/router_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down Expand Up @@ -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
Expand Down
115 changes: 93 additions & 22 deletions internal/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package server
import (
"fmt"
"net"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down Expand Up @@ -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
}

0 comments on commit 8abe3db

Please sign in to comment.