Skip to content

Commit

Permalink
Allow specifying of DNS resolvers, endpoint IP addresses (#6)
Browse files Browse the repository at this point in the history
* progress

* remove test SOA work

* prep work for new requester

* all lookup options working

* some cmd util tests

* fix tests, add log lines, test cmd on CI

* CSVtoIPs test cases

* README updates

* change pad flag to `no-pad`, since we default to padding

* threadsafe dnscache

* haha

* endpoint parser tests

* dns cache tests

* LookupIP tests

* improve lookup tests
  • Loading branch information
fardog authored Apr 12, 2017
1 parent f817b8a commit 9e687d2
Show file tree
Hide file tree
Showing 11 changed files with 742 additions and 39 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go:
- tip
install:
- go get -u -v github.com/fardog/secureoperator
- go get -u -v github.com/fardog/secureoperator/cmd
- go get -u -v github.com/fardog/secureoperator/cmd/secure-operator
before_deploy: "make release"
deploy:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ release:
GOOS=windows GOARCH=386 go build -o release/secure-operator_windows-386.exe $(cmd_package)

test:
go test -v ./
go test -v ./ ./cmd
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The `latest` tag will always be the build from the `master` branch. If you wish
to use one of the stable releases, use its version tag when pulling, e.g.:

```
docker pull fardog/secureoperator:v1.0.3
docker pull fardog/secureoperator:v2.0.0
```

## Version Compatibility
Expand All @@ -50,7 +50,7 @@ always considered stable, but may break API compatibility. If you require API
stability, either use the tagged releases or mirror on gopkg.in:

```
go get -u gopkg.in/fardog/secureoperator.v1
go get -u gopkg.in/fardog/secureoperator.v2
```

## Security
Expand All @@ -60,17 +60,26 @@ consider the following:

* You must trust Google with your requests, see
[their privacy statement][googlednspriv] for further details.
* The initial lookup for the Google DNS endpoint happens over plain DNS using
your locally configured DNS resolver; there are plans to mitigate this in the
future, but at least _one_ DNS request will be sent unsecured.
* The lookup for the Google DNS endpoint must happen in _some_ regard, although
how this is handled is up to you:
* The system DNS resolver is used to look up the endpoint (default)
* You provide a list of DNS servers to use for the endpoint lookup
* You provide the IP address(es) to the endpoint; and no unencrypted DNS
lookup will be performed. However if the addresses change while the
service is running, you will need to restart the service to provide new
addresses.

Information on the usage of these options is available with
`secure-operator --help`.

## Caveats/TODO

* Currently only the following records are supported: `A, AAAA, CNAME, MX`
* More thorough tests should be written
* No caching is implemented, and probably never will. If you need caching, put
your `secure-operator` server behind another DNS server which provides
caching.
caching. (TODO: write instructions on setup, or provide a docker-compose
configuration.)

## Acknowledgments

Expand Down
62 changes: 49 additions & 13 deletions cmd/secure-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,28 @@ package main

import (
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"

log "github.com/Sirupsen/logrus"
"github.com/miekg/dns"

secop "github.com/fardog/secureoperator"
"github.com/fardog/secureoperator/cmd"
)

var (
listenAddress = flag.String(
"listen", ":53", "listen address, as `[host]:port`",
)

endpoint = flag.String(
"endpoint",
"https://dns.google.com/resolve",
"Google DNS-over-HTTPS endpoint url",
)
pad = flag.Bool(
"pad",
true,
"Pad Google DNS-over-HTTPS requests to identical length",
noPad = flag.Bool(
"no-pad",
false,
"Disable padding of Google DNS-over-HTTPS requests to identical length",
)

logLevel = flag.String(
Expand All @@ -34,6 +32,29 @@ var (
"Log level, one of: debug, info, warn, error, fatal, panic",
)

// resolution of the Google DNS endpoint; the interaction of these values is
// somewhat complex, and is further explained in the help message.
endpoint = flag.String(
"endpoint",
"https://dns.google.com/resolve",
"Google DNS-over-HTTPS endpoint url",
)
endpointIPs = flag.String(
"endpoint-ips",
"",
`IPs of the Google DNS-over-HTTPS endpoint; if provided, endpoint lookup is
skipped, and the host value in "endpoint" is sent as the Host header. Comma
separated with no spaces; e.g. "74.125.28.139,74.125.28.102". One server is
randomly chosen for each request, failed requests are not retried.`,
)
dnsServers = flag.String(
"dns-servers",
"",
`DNS Servers used to look up the endpoint; system default is used if absent.
Ignored if "endpoint-ips" is set. Comma separated, e.g. "8.8.8.8,8.8.4.4:53".
The port section is optional, and 53 will be used by default.`,
)

enableTCP = flag.Bool("tcp", true, "Listen on TCP")
enableUDP = flag.Bool("udp", true, "Listen on UDP")
)
Expand Down Expand Up @@ -61,6 +82,9 @@ func serve(net string) {

func main() {
flag.Usage = func() {
_, exe := filepath.Split(os.Args[0])
fmt.Fprint(os.Stderr, "A DNS-protocol proxy for Google's DNS-over-HTTPS service.\n\n")
fmt.Fprintf(os.Stderr, "Usage:\n\n %s [options]\n\nOptions:\n\n", exe)
flag.PrintDefaults()
}
flag.Parse()
Expand All @@ -69,13 +93,25 @@ func main() {
level, err := log.ParseLevel(*logLevel)
if err != nil {
log.Fatalf("invalid log level: %s", err.Error())
return
}
log.SetLevel(level)

provider := secop.GDNSProvider{
Endpoint: *endpoint,
Pad: *pad,
eips, err := cmd.CSVtoIPs(*endpointIPs)
if err != nil {
log.Fatalf("error parsing endpoint-ips: %v", err)
}
dips, err := cmd.CSVtoEndpoints(*dnsServers)
if err != nil {
log.Fatalf("error parsing dns-servers: %v", err)
}

provider, err := secop.NewGDNSProvider(*endpoint, &secop.GDNSOptions{
Pad: !*noPad,
EndpointIPs: eips,
DNSServers: dips,
})
if err != nil {
log.Fatal(err)
}
options := &secop.HandlerOptions{}
handler := secop.NewHandler(provider, options)
Expand Down
48 changes: 48 additions & 0 deletions cmd/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cmd

import (
"fmt"
"net"
"strings"

secop "github.com/fardog/secureoperator"
)

// CSVtoEndpoints takes a comma-separated string of endpoints, and parses to a
// []secop.Endpoint
func CSVtoEndpoints(csv string) (eps []secop.Endpoint, err error) {
reps := strings.Split(csv, ",")
for _, r := range reps {
if r == "" {
continue
}

ep, err := secop.ParseEndpoint(r, 53)
if err != nil {
return eps, err
}

eps = append(eps, ep)
}

return eps, err
}

// CSVtoIPs takes a comma-separated string of IPs, and parses to a []net.IP
func CSVtoIPs(csv string) (ips []net.IP, err error) {
rs := strings.Split(csv, ",")

for _, r := range rs {
if r == "" {
continue
}

ip := net.ParseIP(r)
if ip == nil {
return ips, fmt.Errorf("unable to parse IP from string %s", r)
}
ips = append(ips, ip)
}

return
}
115 changes: 115 additions & 0 deletions cmd/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package cmd

import "testing"

func TestCSVtoEndpoints(t *testing.T) {
type Case struct {
csv string
err bool
expected []string
}

cs := []Case{
Case{
"8.8.8.8:53,8.8.4.4:8053",
false,
[]string{"8.8.8.8:53", "8.8.4.4:8053"},
},
Case{
"8.8.8.8,8.8.4.4:8053",
false,
[]string{"8.8.8.8:53", "8.8.4.4:8053"},
},
Case{
"8.8.8.8",
false,
[]string{"8.8.8.8:53"},
},
Case{
"",
false,
[]string{},
},
Case{
"8.8.8.8:53:54",
true,
[]string{},
},
}

for i, c := range cs {
results, err := CSVtoEndpoints(c.csv)
if c.err && err == nil {
t.Errorf("%v: expected err, got none", i)
} else if !c.err && err != nil {
t.Errorf("%v: did not expect error, got: %v", i, err)
}

if e, r := len(c.expected), len(results); e != r {
t.Errorf("%v: expected %v results, got %v", i, e, r)
continue
}

for j, r := range results {
if r.String() != c.expected[j] {
t.Errorf("%v,%v: expected %v, got %v", i, j, r, c.expected[j])
}
}
}
}

func TestCSVtoIPs(t *testing.T) {
type Case struct {
csv string
err bool
expected []string
}

cs := []Case{
Case{
"8.8.8.8,8.8.4.4",
false,
[]string{"8.8.8.8", "8.8.4.4"},
},
Case{
"8.8.8.8,8.8.4.4",
false,
[]string{"8.8.8.8", "8.8.4.4"},
},
Case{
"8.8.8.8",
false,
[]string{"8.8.8.8"},
},
Case{
"",
false,
[]string{},
},
Case{
"8.8.8.8:53",
true,
[]string{},
},
}

for i, c := range cs {
results, err := CSVtoIPs(c.csv)
if c.err && err == nil {
t.Errorf("%v: expected err, got none", i)
} else if !c.err && err != nil {
t.Errorf("%v: did not expect error, got: %v", i, err)
}

if e, r := len(c.expected), len(results); e != r {
t.Errorf("%v: expected %v results, got %v", i, e, r)
continue
}

for j, r := range results {
if r.String() != c.expected[j] {
t.Errorf("%v,%v: expected %v, got %v", i, j, r, c.expected[j])
}
}
}
}
Loading

0 comments on commit 9e687d2

Please sign in to comment.