Skip to content

Commit

Permalink
Merge branch 'modular' = dingo 0.12
Browse files Browse the repository at this point in the history
  • Loading branch information
pforemski committed Oct 16, 2016
2 parents 55900af + 118eba9 commit 966f6ee
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 89 deletions.
35 changes: 23 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

A DNS client in Go that supports the [Google
DNS-over-HTTPS](https://developers.google.com/speed/public-dns/docs/dns-over-https).
It effectively encrypts all your DNS traffic.
It effectively encrypts all your DNS traffic. It also supports
[OpenResolve](https://www.openresolve.com/) by OpenDNS.

The ultimate goal for the project is to provide a secure, caching DNS proxy that communicates with
recursive DNS resolvers over encrypted channels only. For now, it resolves DNS queries over
HTTPS/1.1, in a few independent threads. The plans for future include HTTP/2.0 and QUIC support,
better caching, and other resolvers (e.g. [OpenResolve](https://www.openresolve.com/) by OpenDNS).
The ultimate goal for the project is to provide a secure, caching DNS proxy that
communicates with recursive DNS resolvers over encrypted channels only. For now,
it resolves DNS queries over HTTPS/1.1 in independent threads. The plans for
future include better caching and support for HTTP/2.0 and QUIC.

## Quick start

Expand Down Expand Up @@ -43,34 +44,44 @@ $ sudo ./dingo-linux-amd64 -port=53 -gdns:server=[2a00:1450:401b:800::200e]

To see all options, run `dingo -h`:
```
Usage of dingo:
Usage of dingo-linux-amd64:
-bind string
IP address to bind to (default "0.0.0.0")
IP address to bind to (default "127.0.0.1")
-dbg int
debugging level (default 2)
-gdns:auto
Google DNS: try to lookup the closest IPv4 server
-gdns:edns string
Google DNS: EDNS client subnet (set 0.0.0.0/0 to disable)
-gdns:host string
Google DNS: HTTP 'Host' header (real FQDN, encrypted in TLS) (default "dns.google.com")
-gdns:nopad
Google DNS: disable random padding
-gdns:server string
Google DNS: web server address (default "216.58.209.174")
Google DNS: server address (default "216.58.195.78")
-gdns:sni string
Google DNS: SNI string to send (should match server certificate) (default "www.google.com")
-gdns:workers int
Google DNS: number of independent workers (default 10)
-odns:host string
OpenDNS: HTTP 'Host' header (real FQDN, encrypted in TLS) (default "api.openresolve.com")
-odns:server string
OpenDNS: web server address (default "67.215.70.81")
-odns:sni string
OpenDNS: TLS SNI string to send (unencrypted, must validate as server cert) (default "www.openresolve.com")
-odns:workers int
OpenDNS: number of independent workers
-port int
listen on port number (default 32000)
```
Note that by default dingo binds to all interfaces, which makes it open to the
world (unless you run a firewall). Consider binding it to `127.0.0.1` instead.
```

Finally, you will need to make dingo start in background each time you boot your machine. In Linux,
you might want to use the [GNU Screen](https://en.wikipedia.org/wiki/GNU_Screen), which can start
processes in background. For example, you might want to add the following line to your
`/etc/rc.local`:
```
screen -dmS dingo /path/to/bin/dingo -port=53 -bind=127.0.0.1 -gdns:server=[2a00:1450:401b:800::200e]
screen -dmS dingo /path/to/bin/dingo -port=53 -gdns:server=[2a00:1450:401b:800::200e]
```

## Author
Expand Down
11 changes: 7 additions & 4 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/bin/bash

[ -z "$1" ] && { echo "Usage: build.sh VERSION" >&1; exit 1; }

VERSION="$1"
DEST="$HOME/tmp/dingo-$VERSION"

###############################################

Expand All @@ -11,14 +13,15 @@ function build()

echo "Building dingo v. $VERSION for $TARGET"
GOOS="${TARGET%-*}" GOARCH="${TARGET##*-}" go build \
-o release/dingo-$VERSION/dingo-$TARGET \
./dingo.go ./gdns.go
-o $DEST/dingo-$TARGET \
./*.go
}

###############################################

rm -fr ./release/dingo-$VERSION
mkdir -p ./release/dingo-$VERSION
echo "Building in $DEST"
rm -fr $DEST
mkdir -p $DEST

for target in \
darwin-386 darwin-amd64 \
Expand Down
47 changes: 32 additions & 15 deletions dingo.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import "math/rand"

/* command-line arguments */
var (
bindip = flag.String("bind", "0.0.0.0", "IP address to bind to")
bindip = flag.String("bind", "127.0.0.1", "IP address to bind to")
port = flag.Int("port", 32000, "listen on port number")
dbglvl = flag.Int("dbg", 2, "debugging level")
)
Expand All @@ -33,7 +33,7 @@ var (
/* logging stuff */
func dbg(lvl int, fmt string, v ...interface{}) { if (*dbglvl >= lvl) { dbglog.Printf(fmt, v...) } }
func die(msg error) { dbglog.Fatalln("fatal error:", msg.Error()) }
var dbglog = log.New(os.Stderr, "", log.LstdFlags | log.Lshortfile | log.LUTC)
var dbglog *log.Logger

/* structures */
type GRR struct {
Expand Down Expand Up @@ -64,29 +64,46 @@ var qchan = make(chan Query, 100)
/* global reply cache */
var rcache *cache.Cache

/* module interface */
var Modules = make(map[string]Module)
type Module interface {
Init()
Start()
}
func register(name string, mod Module) *Module {
Modules[name] = mod
return &mod
}

/**********************************************************************/

/* UDP request handler */
func handle(buf []byte, addr *net.UDPAddr, uc *net.UDPConn) {
dbg(3, "new request from %s (%d bytes)", addr, len(buf))

/* try unpacking */
msg := new(dns.Msg)
if err := msg.Unpack(buf); err != nil { dbg(3, "Unpack failed: %s", err); return }
dbg(7, "unpacked: %s", msg)
if err := msg.Unpack(buf); err != nil {
dbg(3, "unpack failed: %s", err)
return
} else {
dbg(7, "unpacked message: %s", msg)
}

/* for each question */
/* any questions? */
if (len(msg.Question) < 1) { dbg(3, "no questions"); return }

qname := msg.Question[0].Name
qtype := msg.Question[0].Qtype
dbg(2, "resolving %s/%s", qname, dns.TypeToString[qtype])

/* check cache */
var r Reply
cid := fmt.Sprintf("%s/%d", msg.Question[0].Name, msg.Question[0].Qtype)
cid := fmt.Sprintf("%s/%d", qname, qtype)
if x, found := rcache.Get(cid); found {
// FIXME: update TTLs
r = x.(Reply)
} else {
/* pass to resolvers and block until the response comes */
r = resolve(msg.Question[0].Name, int(msg.Question[0].Qtype))
r = resolve(qname, int(qtype))
dbg(8, "got reply: %+v", r)

/* put to cache for 10 seconds (FIXME: use minimum TTL) */
Expand Down Expand Up @@ -139,11 +156,12 @@ func resolve(name string, qtype int) Reply {

/* main */
func main() {
rand.Seed(time.Now().UnixNano())
dbglog = log.New(os.Stderr, "", log.LstdFlags | log.LUTC)

/* prepare */
for _,mod := range Modules { mod.Init() }
flag.Parse()
// dbglog = log.New(os.Stderr, "", log.LstdFlags | log.Lshortfile | log.LUTC)
dbglog = log.New(os.Stderr, "", log.LstdFlags | log.LUTC)
rand.Seed(time.Now().UnixNano())
rcache = cache.New(24*time.Hour, 60*time.Second)

/* listen */
Expand All @@ -152,11 +170,10 @@ func main() {
if err != nil { die(err) }

/* start workers */
gdns_start()
// odns_start()
for _, mod := range Modules { mod.Start() }

/* accept new connections forever */
dbg(1, "dingo ver. 0.11 started on UDP port %d", laddr.Port)
dbg(1, "dingo ver. 0.12 listening on %s UDP port %d", *bindip, laddr.Port)
var buf []byte
for {
buf = make([]byte, 1500)
Expand Down
119 changes: 61 additions & 58 deletions gdns.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,85 +9,88 @@
package main

import "fmt"
import "net/http"
import "net/url"
import "time"
import "io/ioutil"
import "encoding/json"
import "crypto/tls"
import "math/rand"
import "strings"
import "flag"
//import "github.com/devsisters/goquic"

type Gdns struct {
workers *int
server *string
auto *bool
sni *string
host *string
edns *string
nopad *bool
}

/* command-line arguments */
var (
gdns_workers = flag.Int("gdns:workers", 10,
func (r *Gdns) Init() {
r.workers = flag.Int("gdns:workers", 10,
"Google DNS: number of independent workers")
gdns_server = flag.String("gdns:server", "216.58.209.174",
"Google DNS: web server address")
gdns_sni = flag.String("gdns:sni", "www.google.com",
r.server = flag.String("gdns:server", "216.58.195.78",
"Google DNS: server address")
r.auto = flag.Bool("gdns:auto", false,
"Google DNS: try to lookup the closest IPv4 server")
r.sni = flag.String("gdns:sni", "www.google.com",
"Google DNS: SNI string to send (should match server certificate)")
gdns_edns = flag.String("gdns:edns", "",
r.host = flag.String("gdns:host", "dns.google.com",
"Google DNS: HTTP 'Host' header (real FQDN, encrypted in TLS)")
r.edns = flag.String("gdns:edns", "",
"Google DNS: EDNS client subnet (set 0.0.0.0/0 to disable)")
gdns_nopad = flag.Bool("gdns:nopad", false,
r.nopad = flag.Bool("gdns:nopad", false,
"Google DNS: disable random padding")
)

/**********************************************************************/

func gdns_start() {
for i := 0; i < *gdns_workers; i++ { go gdns_resolver(*gdns_server) }
}

func gdns_resolver(server string) {
/* setup the HTTP client */
var httpTr = http.DefaultTransport.(*http.Transport)
// var httpTr = goquic.NewRoundTripper(true)
/**********************************************************************/

var tlsCfg = &tls.Config{ ServerName: *gdns_sni }
httpTr.TLSClientConfig = tlsCfg;
// req,_ := http.NewRequest("GET", "https://www.google.com/", nil)
// httpTr.RoundTrip(req)
func (R *Gdns) Start() {
if *R.workers <= 0 { return }

var httpClient = &http.Client{ Timeout: time.Second*10, Transport: httpTr }
if *R.auto {
dbg(1, "resolving dns.google.com...")
r4 := R.resolve(NewHttps(*R.sni), *R.server, "dns.google.com", 1)
if r4.Status == 0 && len(r4.Answer) > 0 {
R.server = &r4.Answer[0].Data
}
}

for q := range qchan {
/* make the new response object */
r := Reply{ Status: -1 }
dbg(1, "starting %d Google Public DNS client(s) querying server %s",
*R.workers, *R.server)
for i := 0; i < *R.workers; i++ { go R.worker(*R.server) }
}

/* prepare the query */
v := url.Values{}
v.Set("name", q.Name)
v.Set("type", fmt.Sprintf("%d", q.Type))
if len(*gdns_edns) > 0 {
v.Set("edns_client_subnet", *gdns_edns)
}
if !*gdns_nopad {
v.Set("random_padding", strings.Repeat(string(65+rand.Intn(26)), rand.Intn(500)))
}
func (R *Gdns) worker(server string) {
var https = NewHttps(*R.sni)
for q := range qchan { *q.rchan <- *R.resolve(https, server, q.Name, q.Type) }
}

/* prepare request, send proper HTTP 'Host:' header */
addr := fmt.Sprintf("https://%s/resolve?%s", server, v.Encode())
hreq,_ := http.NewRequest("GET", addr, nil)
hreq.Host = "dns.google.com"
func (R *Gdns) resolve(https *Https, server string, qname string, qtype int) *Reply {
r := Reply{ Status: -1 }
v := url.Values{}

/* send the query */
resp,err := httpClient.Do(hreq)
if (err == nil) {
dbg(2, "[%s/%d] %s %s", q.Name, q.Type, resp.Status, resp.Proto)
/* prepare */
v.Set("name", qname)
v.Set("type", fmt.Sprintf("%d", qtype))
if len(*R.edns) > 0 {
v.Set("edns_client_subnet", *R.edns)
}
if !*R.nopad {
v.Set("random_padding", strings.Repeat(string(65+rand.Intn(26)), rand.Intn(500)))
}

/* read */
buf,_ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
dbg(7, " reply: %s", buf)
/* query */
buf, err := https.Get(server, *R.host, "/resolve?" + v.Encode())
if err != nil { return &r }

/* parse JSON? */
if (resp.StatusCode == 200) { json.Unmarshal(buf, &r) }
r.Now = time.Now()
} else { dbg(1, "[%s/%d] error: %s", q.Name, q.Type, err.Error()) }
/* parse */
r.Now = time.Now()
json.Unmarshal(buf, &r)

/* write the reply */
*q.rchan <- r
}
return &r
}

/* register module */
var _ = register("gdns", new(Gdns))
Loading

0 comments on commit 966f6ee

Please sign in to comment.