Skip to content

Commit

Permalink
feat(DoH): implement DNS-over-HTTPS, fixes #2 (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
cottand authored Nov 6, 2023
1 parent f6ee20f commit 033f4f6
Show file tree
Hide file tree
Showing 14 changed files with 630 additions and 23 deletions.
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@

Forked from [looterz/grimd](https://github.com/looterz/grimd)

# Features
- [x] DNS over UTP
- [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] Blocklist fetching
- [x] Hardcoded blocklist config
- [x] Hardcoded whitelist config
- [x] Fast startup _(so it can be used with templating for service discovery)_
- [x] Small memory footprint (~50MBs with metrics and DoH enabled)

# Installation
```
go install github.com/cottand/grimd@latest
Expand Down Expand Up @@ -46,7 +58,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
```shell
env GOOS=linux GOARCH=amd64 go build -v github.com/looterz/grimd
env GOOS=linux GOARCH=amd64 go build -v github.com/cottand/grimd
```

# Building Docker
Expand Down Expand Up @@ -78,8 +90,8 @@ These are some of the things I would like to contribute in this fork:
- [x] ~~Fix multi-record responses issue#5~~
- [ ] DNS record flattening issue#1
- [ ] Service discovery integrations? issue#4
- [ ] Prometheus metrics exporter issue#3
- [ ] DNS over HTTPS #2
- [x] Prometheus metrics exporter issue#3
- [x] DNS over HTTPS #2
- [ ] Add lots of docs

## Non-objectives
Expand Down
85 changes: 78 additions & 7 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package main

import (
cTls "crypto/tls"
"errors"
"fmt"
"github.com/cottand/grimd/tls"
"github.com/jonboulle/clockwork"
"github.com/pelletier/go-toml/v2"
"log"
"os"
"path/filepath"
"strings"
)

// BuildVersion returns the build version of grimd, this should be incremented every new release
var BuildVersion = "2.2.1"
var BuildVersion = "1.3.0"

// ConfigVersion returns the version of grimd, this should be incremented every time the config changes so grimd presents a warning
var ConfigVersion = "2.2.1"
var ConfigVersion = "1.3.0"

// Config holds the configuration parameters
type Config struct {
Expand Down Expand Up @@ -40,13 +45,34 @@ type Config struct {
APIDebug bool
DoH string
Metrics Metrics `toml:"metrics"`
DnsOverHttpServer DnsOverHttpServer
}

type Metrics struct {
Enabled bool
Path string
}

type DnsOverHttpServer struct {
Enabled bool
Bind string
TimeoutMs int64
TLS TlsConfig
parsedTls *cTls.Config
}

type TlsConfig struct {
certPath, keyPath, caPath string
enabled bool
}

func (c TlsConfig) parsedConfig() (*cTls.Config, error) {
if !c.enabled {
return nil, nil
}
return tls.NewTLSConfig(c.certPath, c.keyPath, c.caPath)
}

var defaultConfig = `
# version this config was generated from
version = "%s"
Expand Down Expand Up @@ -133,21 +159,34 @@ togglename = ""
# having been turned off.
reactivationdelay = 300
#Dns over HTTPS provider to use.
# Dns over HTTPS upstream provider to use
DoH = "https://cloudflare-dns.com/dns-query"
# Prometheus metrics - enable
# Prometheus metrics - disabled by default
[Metrics]
enabled = false
path = "/metrics"
[DnsOverHttpServer]
enabled = false
bind = "0.0.0.0:80"
timeoutMs = 5000
# TLS config is not required for DoH if you have some proxy (ie, caddy, nginx, traefik...) manage HTTPS for you
[DnsOverHttpServer.TLS]
enabled = false
certPath = ""
keyPath = ""
# if empty, system CAs will be used
caPath = ""
`

func parseDefaultConfig() Config {
var config Config

err := toml.Unmarshal([]byte(defaultConfig), &config)
if err != nil {
logger.Fatalf("There was an error parsing the default config %v", err)
logger.Fatalf("There was an error parsing the default config: %v", err)
}
config.Version = ConfigVersion

Expand All @@ -157,6 +196,19 @@ func parseDefaultConfig() Config {
// WallClock is the wall clock
var WallClock = clockwork.NewRealClock()

func contextualisedParsingErrorFrom(err error) error {
errString := strings.Builder{}
var derr *toml.DecodeError
_, _ = fmt.Fprint(&errString, "could not load config:", err)
if errors.As(err, &derr) {
errString.WriteByte('\n')
_, _ = fmt.Fprintln(&errString, derr.String())
row, col := derr.Position()
_, _ = fmt.Fprintln(&errString, "error occurred at row", row, "column", col)
}
return errors.New(errString.String())
}

// LoadConfig loads the given config file
func LoadConfig(path string) (*Config, error) {

Expand All @@ -167,9 +219,28 @@ func LoadConfig(path string) (*Config, error) {
return &config, nil
}

if err := toml.Unmarshal([]byte(path), &config); err != nil {
return nil, fmt.Errorf("could not load config: %s", err)
path = filepath.Clean(path)
file, err := os.Open(path)
if err != nil {
log.Printf("warning, failed to open config (%v) - using defaults", err)
return &config, nil
}

defer func(file *os.File) {
_ = file.Close()
}(file)

d := toml.NewDecoder(file)

if err := d.Decode(&config); err != nil {
return nil, contextualisedParsingErrorFrom(err)
}

dohTls, err := config.DnsOverHttpServer.TLS.parsedConfig()
if err != nil {
return nil, fmt.Errorf("could not load TLS config: %s", err)
}
config.DnsOverHttpServer.parsedTls = dohTls

if config.Version != ConfigVersion {
if config.Version == "" {
Expand Down
20 changes: 20 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert"
"strings"
"testing"
)

Expand Down Expand Up @@ -35,3 +36,22 @@ path = "/voo"
}
assert.Equal(t, true, config.Metrics.Enabled, "expected overridden value for config.bind")
}

func TestFriendlyErrors(t *testing.T) {
config := parseDefaultConfig()

badConfig := `
[metrics]
enabled = 3
`

err := toml.Unmarshal([]byte(badConfig), &config)
if err == nil {
t.Fatalf("expected an error!")
}
err = contextualisedParsingErrorFrom(err)

if !(strings.Contains(err.Error(), "enabled = 3") && strings.Contains(err.Error(), "row 3 column 11")) {
t.Fatalf("expected error string to contain contextual info, but was %v", err.Error())
}
}
Loading

0 comments on commit 033f4f6

Please sign in to comment.