author | Alberto Bertogli
<albertito@blitiri.com.ar> 2016-05-17 22:12:47 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2016-05-19 09:35:21 UTC |
parent | 313bcf21cf8861017011009ed68978a6ed8749e2 |
README.md | +28 | -7 |
dnss.go | +37 | -7 |
dnstox/resolver.go | +176 | -0 |
diff --git a/README.md b/README.md index 4f1a4f9..1fe676a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,39 @@ -# dnss - Encapsulate DNS over GRPC +# dnss -dnss encapsulates DNS over GRPC. +dnss is a tool for encapsulating DNS over more secure protocols. + + +## DNS over HTTPS + +dnss can act as a DNS-over-HTTPS proxy, using https://dns.google.com as a +server. + +``` ++--------+ +----------------+ +----------------+ +| | | dnss | | | +| client +-------> (dns-to-https) +--------> dns.google.com | +| | DNS | | | | ++--------+ UDP +----------------+ HTTP +----------------+ + SSL + TCP +``` + + +## DNS over GRPC + +dnss can encapsulate DNS over GRPC. It can be useful when you want to use a particular DNS server, but don't want some parts of the network in between to be able to see your traffic. ``` -+--------+ +---------------+ +---------------+ +------------+ -| | | dnss | | dnss | | | -| client +-------> (dns-to-grpc) +--------> (grpc-to-dns) +-------> DNS server | -| | DNS | | DNS | | DNS | | -+--------+ UDP +---------------+ GRPC +---------------+ UDP +------------+ ++--------+ +---------------+ +---------------+ +------------+ +| | | dnss | | dnss | | | +| client +-------> (dns-to-grpc) +--------> (grpc-to-dns) +------> DNS server | +| | DNS | | DNS | | DNS | | ++--------+ UDP +---------------+ GRPC +---------------+ UDP +------------+ SSL TCP ``` diff --git a/dnss.go b/dnss.go index 54c4c0d..4dd6cc1 100644 --- a/dnss.go +++ b/dnss.go @@ -20,16 +20,17 @@ import ( ) var ( - enableDNStoGRPC = flag.Bool("enable_dns_to_grpc", false, - "enable DNS-to-GRPC server") dnsListenAddr = flag.String("dns_listen_addr", ":53", "address to listen on for DNS") + dnsUnqualifiedUpstream = flag.String("dns_unqualified_upstream", "", + "DNS server to forward unqualified requests to") + + enableDNStoGRPC = flag.Bool("enable_dns_to_grpc", false, + "enable DNS-to-GRPC server") grpcUpstream = flag.String("grpc_upstream", "localhost:9953", "address of the upstream GRPC server") grpcClientCAFile = flag.String("grpc_client_cafile", "", "CA file to use for the GRPC client") - dnsUnqualifiedUpstream = flag.String("dns_unqualified_upstream", "", - "DNS server to forward unqualified requests to") enableGRPCtoDNS = flag.Bool("enable_grpc_to_dns", false, "enable GRPC-to-DNS server") @@ -38,6 +39,14 @@ var ( dnsUpstream = flag.String("dns_upstream", "8.8.8.8:53", "address of the upstream DNS server") + enableDNStoHTTPS = flag.Bool("enable_dns_to_https", false, + "enable DNS-to-HTTPS proxy") + httpsUpstream = flag.String("https_upstream", + "https://dns.google.com/resolve", + "URL of upstream DNS-to-HTTP server") + httpsClientCAFile = flag.String("https_client_cafile", "", + "CA file to use for the HTTPS client") + grpcCert = flag.String("grpc_cert", "", "certificate file for the GRPC server") grpcKey = flag.String("grpc_key", "", @@ -71,9 +80,18 @@ func main() { go http.ListenAndServe(*monitoringListenAddr, nil) } - if !*enableDNStoGRPC && !*enableGRPCtoDNS { - glog.Fatal( - "Error: pass --enable_dns_to_grpc or --enable_grpc_to_dns") + if !*enableDNStoGRPC && !*enableGRPCtoDNS && !*enableDNStoHTTPS { + glog.Error("Need to set one of the following:") + glog.Error(" --enable_dns_to_https") + glog.Error(" --enable_dns_to_grpc") + glog.Error(" --enable_grpc_to_dns") + glog.Fatal("") + } + + if *enableDNStoGRPC && *enableDNStoHTTPS { + glog.Error("The following options cannot be set at the same time:") + glog.Error(" --enable_dns_to_grpc and --enable_dns_to_https") + glog.Fatal("") } var wg sync.WaitGroup @@ -105,5 +123,17 @@ func main() { }() } + // DNS to HTTPS. + if *enableDNStoHTTPS { + r := dnstox.NewHTTPSResolver(*httpsUpstream, *httpsClientCAFile) + cr := dnstox.NewCachingResolver(r) + dth := dnstox.New(*dnsListenAddr, cr, *dnsUnqualifiedUpstream) + wg.Add(1) + go func() { + defer wg.Done() + dth.ListenAndServe() + }() + } + wg.Wait() } diff --git a/dnstox/resolver.go b/dnstox/resolver.go index 2c8c6dd..3db5e0f 100644 --- a/dnstox/resolver.go +++ b/dnstox/resolver.go @@ -1,9 +1,14 @@ package dnstox import ( + "crypto/tls" + "crypto/x509" + "encoding/json" "expvar" "fmt" + "io/ioutil" "net/http" + "net/url" "sync" "time" @@ -32,6 +37,9 @@ type Resolver interface { Query(r *dns.Msg, tr trace.Trace) (*dns.Msg, error) } +/////////////////////////////////////////////////////////////////////////// +// GRPC resolver. + // grpcResolver implements the Resolver interface by querying a server via // GRPC. type grpcResolver struct { @@ -92,6 +100,174 @@ func (g *grpcResolver) Query(r *dns.Msg, tr trace.Trace) (*dns.Msg, error) { return m, err } +/////////////////////////////////////////////////////////////////////////// +// HTTPS resolver. + +// httpsResolver implements the Resolver interface by querying a server via +// DNS-over-HTTPS (like https://dns.google.com). +type httpsResolver struct { + Upstream string + CAFile string + client *http.Client +} + +func loadCertPool(caFile string) (*x509.CertPool, error) { + pemData, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pemData) { + return nil, fmt.Errorf("Error appending certificates") + } + + return pool, nil +} + +func NewHTTPSResolver(upstream, caFile string) *httpsResolver { + return &httpsResolver{ + Upstream: upstream, + CAFile: caFile, + } +} + +func (r *httpsResolver) Init() error { + r.client = &http.Client{ + // Give our HTTP requests 4 second timeouts: DNS usually doesn't wait + // that long anyway, but this helps with slow connections. + Timeout: 4 * time.Second, + } + + if r.CAFile == "" { + return nil + } + + pool, err := loadCertPool(r.CAFile) + if err != nil { + return err + } + + r.client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + ClientCAs: pool, + }, + } + + return nil +} + +func (r *httpsResolver) Maintain() { +} + +// Structure for parsing JSON responses. +type jsonResponse struct { + Status int + TC bool + RD bool + RA bool + AD bool + CD bool + Question []jsonRR + Answer []jsonRR +} + +type jsonRR struct { + Name string `json:name` + Type uint16 `json:type` + TTL uint32 `json:TTL` + Data string `json:data` +} + +func (r *httpsResolver) Query(req *dns.Msg, tr trace.Trace) (*dns.Msg, error) { + // Only answer single-question queries. + // In practice, these are all we get, and almost no server supports + // multi-question requests anyway. + if len(req.Question) != 1 { + return nil, fmt.Errorf("multi-question query") + } + + question := req.Question[0] + // Only answer IN-class queries, which are the ones used in practice. + if question.Qclass != dns.ClassINET { + return nil, fmt.Errorf("query class != IN") + } + + // Build the query and send the request. + v := url.Values{} + v.Set("name", question.Name) + v.Set("type", dns.TypeToString[question.Qtype]) + // TODO: add random_padding. + + url := r.Upstream + "?" + v.Encode() + if glog.V(3) { + tr.LazyPrintf("GET %q", url) + } + + hr, err := r.client.Get(url) + if err != nil { + return nil, fmt.Errorf("GET failed: %v", err) + } + tr.LazyPrintf("%s %s", hr.Proto, hr.Status) + defer hr.Body.Close() + + if hr.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Response status: %s", hr.Status) + } + + // Read the HTTPS response, and parse the JSON. + body, err := ioutil.ReadAll(hr.Body) + if err != nil { + return nil, fmt.Errorf("Failed to read body: %v", err) + } + + jr := &jsonResponse{} + err = json.Unmarshal(body, jr) + if err != nil { + return nil, fmt.Errorf("Failed to unmarshall: %v", err) + } + + // Build the DNS response. + resp := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: req.Id, + Response: true, + Opcode: req.Opcode, + Rcode: jr.Status, + + Truncated: jr.TC, + RecursionDesired: jr.RD, + RecursionAvailable: jr.RA, + AuthenticatedData: jr.AD, + CheckingDisabled: jr.CD, + }, + } + + if len(jr.Question) != 1 { + return nil, fmt.Errorf("Wrong number of questions in the response") + } + resp.SetQuestion(jr.Question[0].Name, jr.Question[0].Type) + + for _, answer := range jr.Answer { + // TODO: This "works" but is quite hacky. Is there a better way, + // without doing lots of data parsing? + s := fmt.Sprintf("%s %d IN %s %s", + answer.Name, answer.TTL, + dns.TypeToString[answer.Type], answer.Data) + rr, err := dns.NewRR(s) + if err != nil { + return nil, fmt.Errorf("Error parsing answer: %v", err) + } + + resp.Answer = append(resp.Answer, rr) + } + + return resp, nil +} + +/////////////////////////////////////////////////////////////////////////// +// Caching resolver. + // cachingResolver implements a caching Resolver. // It is backed by another Resolver, but will cache results. type cachingResolver struct {