Skip to content

Commit

Permalink
feat(RFC-1034): implement CNAME following, fixes #1 (#26)
Browse files Browse the repository at this point in the history
* refactor handler into EventLoop and doRequest()

* delete reaper subrepo

* refactor handler to be able to resolve from req to response

* implement CNAME-following for external domains

* feat(RFC-1034): implement CNAME following

* fix(CI): use Go 1.21 to test in CI

* update README
  • Loading branch information
cottand authored Nov 8, 2023
1 parent 4e84f83 commit 340459a
Show file tree
Hide file tree
Showing 12 changed files with 428 additions and 242 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
go-version: 1.21

- name: Build
run: go build -v ./...
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Forked from [looterz/grimd](https://github.com/looterz/grimd)
- [x] DNS over TCP
- [x] DNS over HTTP(S) (DoH as per [RFC-8484](https://datatracker.ietf.org/doc/html/rfc8484))
- [x] Prometheus metrics API
- [x] Custom DNS records supports
- [x] Custom DNS records support
- [x] Blocklist fetching
- [x] Hardcoded blocklist config
- [x] Hardcoded whitelist config
Expand Down Expand Up @@ -56,7 +56,7 @@ Usage of grimd:
```

# Building
Requires golang 1.7 or higher, you build grimd like any other golang application, for example to build for linux x64
Requires golang 1.20 or higher, you build grimd like any other golang application, for example to build for linux x64
```shell
env GOOS=linux GOARCH=amd64 go build -v github.com/cottand/grimd
```
Expand All @@ -76,19 +76,19 @@ curl -H "Accept: application/json" http://127.0.0.1:55006/application/active
```

# Speed
Incoming requests spawn a goroutine and are served concurrently, and the block cache resides in-memory to allow for rapid lookups, while answered queries are cached allowing grimd to serve thousands of queries at once while maintaining a memory footprint of under 15mb for 100,000 blocked domains!
Incoming requests spawn a goroutine and are served concurrently, and the block cache resides in-memory to allow for rapid lookups, while answered queries are cached allowing grimd to serve thousands of queries at once while maintaining a memory footprint of under 30mb for 100,000 blocked domains!

# Daemonize
You can find examples of different daemon scripts for grimd on the [wiki](https://github.com/looterz/grimd/wiki/Daemon-Scripts).

# Objectives

These are some of the things I would like to contribute in this fork:
- [x] ~~ARM64 Docker builds~~
- [ ] Better custom DNS support
- [x] ~~Dynamic config reload for custom DNS issue#16~~
- [x] ~~Fix multi-record responses issue#5~~
- [ ] DNS record flattening issue#1
- [x] ~~DNS record CNAME following issue#1~~
- [ ] DNS record CNAME flattening a la cloudflare issue#27
- [ ] Service discovery integrations? issue#4
- [x] Prometheus metrics exporter issue#3
- [x] DNS over HTTPS #2
Expand Down
7 changes: 7 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Config struct {
DoH string
Metrics Metrics `toml:"metrics"`
DnsOverHttpServer DnsOverHttpServer
FollowCnameDepth uint32
}

type Metrics struct {
Expand Down Expand Up @@ -162,6 +163,12 @@ reactivationdelay = 300
# Dns over HTTPS upstream provider to use
DoH = "https://cloudflare-dns.com/dns-query"
# How deep to follow chains of CNAME records
# set to 0 to disable CNAME-following entirely
# (anything more than 10 should be more than plenty)
# see https://github.com/Cottand/grimd/wiki/CNAME%E2%80%90following-DNS
followCnameDepth = 12
# Prometheus metrics - disabled by default
[Metrics]
enabled = false
Expand Down
1 change: 0 additions & 1 deletion dashboard/reaper
Submodule reaper deleted from 79c906
4 changes: 2 additions & 2 deletions doh.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (s *ServerHTTPS) Stop() error {
return nil
}

// ServeHTTP is the handler that gets the HTTP request and converts to the dns format, calls the resolver,
// ServeHTTP is the eventLoop that gets the HTTP request and converts to the dns format, calls the resolver,
// converts it back and write it to the client.
func (s *ServerHTTPS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !(r.URL.Path == pathDOH) {
Expand Down Expand Up @@ -195,7 +195,7 @@ type DohResponseWriter struct {

// See section 4.2.1 of RFC 8484.
// We are using code 500 to indicate an unexpected situation when the chain
// handler has not provided any response message.
// eventLoop has not provided any response message.
func (w *DohResponseWriter) handleErr(err error) {
logger.Warningf("error when replying to DoH: %v", err)
http.Error(w.delegate, "No response", http.StatusInternalServerError)
Expand Down
4 changes: 2 additions & 2 deletions doh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func dnsAQuestion(question string) (msg *dns.Msg) {
func TestDohHappyPath(t *testing.T) {
handler := dns.NewServeMux()
custom := NewCustomDNSRecordsFromText([]string{"example.com. IN A 10.0.0.0 "})
handler.HandleFunc("example.com", custom[0].serve(nil))
handler.HandleFunc("example.com", custom[0].asHandler())

dohTest(t, handler, func(r Resolver, bind string) {
response, err := r.DoHLookup("http://"+bind+"/dns-query", 1, dnsAQuestion("example.com."))
Expand All @@ -36,7 +36,7 @@ func TestDohHappyPath(t *testing.T) {
func TestDoh404(t *testing.T) {
handler := dns.NewServeMux()
custom := NewCustomDNSRecordsFromText([]string{"example.com A 10.0.0.0"})
handler.HandleFunc("example.com", custom[0].serve(nil))
handler.HandleFunc("example.com", custom[0].asHandler())

dohTest(t, handler, func(r Resolver, bind string) {
resp, err := http.Get("http://" + bind + "/unknown-path")
Expand Down
71 changes: 71 additions & 0 deletions grimd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/pelletier/go-toml/v2"
"io"
"net/http"
"slices"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -40,6 +41,7 @@ func integrationTest(changeConfig func(c *Config), test func(client *dns.Client,

go startActivation(actChannel, quitActivation, config.ReactivationDelay)
grimdActivation = <-actChannel
grimdActive = true
close(actChannel)

server := &Server{
Expand All @@ -51,6 +53,9 @@ func integrationTest(changeConfig func(c *Config), test func(client *dns.Client,

// BlockCache contains all blocked domains
blockCache := &MemoryBlockCache{Backend: make(map[string]bool)}
for _, blocked := range config.Blocklist {
_ = blockCache.Set(blocked, true)
}
// QuestionCache contains all queries to the dns server
questionCache := makeQuestionCache(config.QuestionCacheCap)

Expand Down Expand Up @@ -150,6 +155,72 @@ func Test2in3DifferentARecords(t *testing.T) {
)
}

func contains(str string) func(rr dns.RR) bool {
return func(rr dns.RR) bool {
return strings.Contains(rr.String(), str)
}
}

func TestCnameFollowHappyPath(t *testing.T) {
integrationTest(
func(c *Config) {
c.CustomDNSRecords = []string{
"first.com IN CNAME second.com ",
"second.com IN CNAME third.com ",
"third.com IN A 10.10.0.42 ",
}
c.Timeout = 10000
},
func(client *dns.Client, target string) {
c := new(dns.Client)

m := new(dns.Msg)

m.SetQuestion(dns.Fqdn("first.com"), dns.TypeA)
reply, _, err := c.Exchange(m, target)
if err != nil {
t.Fatalf("failed to exchange %v", err)
}
if l := len(reply.Answer); l != 3 {
t.Fatalf("Expected 3 returned records but had %v: %v", l, reply.Answer)
}

if !slices.ContainsFunc(reply.Answer, contains("10.10.0.42")) ||
!slices.ContainsFunc(reply.Answer, contains("A")) {
t.Fatalf("Expected the right A address to be returned, but got %v", reply.Answer[0])
}
},
)
}

func TestCnameFollowWithBlocked(t *testing.T) {
integrationTest(
func(c *Config) {
c.CustomDNSRecords = []string{
"first.com IN CNAME second.com ",
"second.com IN CNAME example.com ",
}
c.Blocklist = []string{"example.com"}

},
func(client *dns.Client, target string) {
c := new(dns.Client)

m := new(dns.Msg)

m.SetQuestion(dns.Fqdn("first.com"), dns.TypeA)
reply, _, err := c.Exchange(m, target)
if err != nil {
t.Error(err)
t.FailNow()
}
if !slices.ContainsFunc(reply.Answer, contains("0.0.0.0")) {
t.Fatalf("Expected right A address to be blocked, but got \n%v", reply.String())
}
},
)
}

func TestDohIntegration(t *testing.T) {
dohBind := "localhost:8181"
integrationTest(func(c *Config) {
Expand Down
Loading

0 comments on commit 340459a

Please sign in to comment.