Skip to content

Commit

Permalink
DoH and DoT stub resolvers
Browse files Browse the repository at this point in the history
A split-listener listens on incoming tcp connections and either proxies it to
approp backend depending on SNI or HTTP Host header, or if the incoming SNI
matches that of the front-end, DoH (on 443) / DoT (on 853) is served instead.
  • Loading branch information
ignoramous committed Apr 14, 2022
1 parent 5532a05 commit 39f7ff9
Show file tree
Hide file tree
Showing 14 changed files with 813 additions and 186 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ DoH endpoint to a stub resolver that returns midway's IP for all DNS queries.

Try midway with curl, like so:

```
```bash
# TLS with SNI on port 443
curl https://www.example.com --resolve 'www.example.com:443:<midway-ip>' -v
* Added www.example.com:443:<midway-ip> to DNS cache
* Uses proxy env variable no_proxy == 'localhost,127.0.0.0/8,::1'
* Hostname www.example.com was found in DNS cache
# this line from curl is a confirmation that the traffic routed to midway:
#
# this next log line is a confirmation that the traffic was routd to midway:
#
* Trying <midway-ip>:443...
* TCP_NODELAY set
* Connected to www.example.com (<midway-ip>) port 443 (#0)
Expand All @@ -30,6 +32,24 @@ curl https://www.example.com --resolve 'www.example.com:443:<midway-ip>' -v
curl abcxyz.neverssl.com --resolve 'abcxyz.neverssl.com:80:<midway-ip>' -v
```
### DNS
midway runs DoT and DoH stub resolver on ports 443 and 853 (or 8443 and 8853 in
non-previledge mode), forwarding queries to `UPSTREAM_DOH` env var (The Google
Public Resolver is the default). `TLS_CN` env var must also be set matching
the SNI (server name identification) of the DoH / DoT endpoint's TLS cert.
Cert can be ethier supplied through the filesystem by setting env vars,
`TLS_CERT_PATH` and `TLS_KEY_PATH`, or by base64 encoding the contents of
key and cert into env var with the same name as `TLS_CN` but in uppercase and
periods (`.`) replaced by underscores (`_`), like so:
Test certs for DNS over TLS and DNS over HTTPS is in `/test/certs/` generated
via openssl ([ref](https://github.com/denji/golang-tls))
```bash
TLS_CN = "example.domain.tld"
EXAMPLE_DOMAIN_TLD = "KEY=b64(key-contents)\nCRT=b64(cert-contents)"
```
### A note on HTTP/3 and QUIC
QUIC takes the stakes even higher with [Connection IDs](https://www.rfc-editor.org/rfc/rfc9000.html#connections)
facilitating routes between peers (a client source and a server destination),
Expand Down
192 changes: 192 additions & 0 deletions dns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright (c) 2022 RethinkDNS and its authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main

import (
"bytes"
"encoding/base64"
"io/ioutil"
"log"
"net/http"

"github.com/miekg/dns"
)

// Adopted from: github.com/folbricht/routedns

func dnsHandler(doh *http.Client) dns.HandlerFunc {
return func(w dns.ResponseWriter, msg *dns.Msg) {
ans := refused(msg)
defer func() {
_ = w.WriteMsg(ans)
w.Close()
}()

q, err := msg.Pack()
if err != nil {
return
}

req, err := http.NewRequest("POST", upstreamdoh, bytes.NewReader(q))
if err != nil {
return
}

ans = servfail(msg)
req.Header.Add("accept", "application/dns-message")
req.Header.Add("content-type", "application/dns-message")

// TODO: rm and restore query-id
res, err := doh.Do(req)

if err != nil {
return
}
defer res.Body.Close()

if res.StatusCode < 200 || res.StatusCode > 299 {
return
}

ansb, err := ioutil.ReadAll(res.Body)
if err != nil {
return
}

p := new(dns.Msg)
if err = p.Unpack(ansb); err == nil {
ans = p
}
return
}
}

func servfail(q *dns.Msg) *dns.Msg {
return responseWithCode(q, dns.RcodeServerFailure)
}

func refused(q *dns.Msg) *dns.Msg {
return responseWithCode(q, dns.RcodeRefused)
}

func responseWithCode(q *dns.Msg, rcode int) *dns.Msg {
a := new(dns.Msg)
a.SetRcode(q, rcode)
return a
}

func dohHandler(resolver *http.Client) func(http.ResponseWriter, *http.Request) {
// ref: github.com/folbricht/routedns/blob/5932594/dohlistener.go#L153
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
getHandler(resolver, w, r)
case "POST":
postHandler(resolver, w, r)
default:
http.Error(w, "GET or POST only", http.StatusMethodNotAllowed)
}
}
}

func getHandler(resolver *http.Client, w http.ResponseWriter, r *http.Request) {
b64, ok := r.URL.Query()["dns"]
if !ok {
http.Error(w, "query missing", http.StatusBadRequest)
return
}
if len(b64) < 1 {
http.Error(w, "query empty", http.StatusBadRequest)
return
}
b, err := base64.RawURLEncoding.DecodeString(b64[0])
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
upstreamDNS(resolver, b, w, r)
}

func postHandler(resolver *http.Client, w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
upstreamDNS(resolver, b, w, r)
}

func upstreamDNS(resolver *http.Client, b []byte, w http.ResponseWriter, r *http.Request) {
q := new(dns.Msg)
if err := q.Unpack(b); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

var err error
a := dodoh(resolver, b)

// A nil response from the resolvers means "drop", return blank response
if a == nil {
w.WriteHeader(http.StatusForbidden)
return
}

querystr := q.Question[0].String()
ansstr := a.Answer[0].String()
log.Printf("dns: q0 %s => ans0 %s | len(ans): %d", querystr, ansstr, len(a.Answer))

// Pad the packet according to rfc8467 and rfc7830
// TODO: padAnswer(q, a)

out, err := a.Pack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("content-type", "application/dns-message")
w.Header().Set("content-type", "application/dns-message")
_, _ = w.Write(out)
}

// TODO: rm query-id before request and restore after response
func dodoh(resolver *http.Client, b []byte) *dns.Msg {
req, err := http.NewRequest("POST", upstreamdoh, bytes.NewReader(b))
if err != nil {
return nil
}

req.Header.Add("accept", "application/dns-message")
req.Header.Add("content-type", "application/dns-message")

res, err := resolver.Do(req)

if err != nil {
return nil
}
defer res.Body.Close()

if res.StatusCode < 200 || res.StatusCode > 299 {
return nil
}

ans, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil
}

x := new(dns.Msg)
if err = x.Unpack(ans); err == nil {
querystr := x.Question[0].String()
ansstr := x.Answer[0].String()
log.Printf("dns: q0 %s => ans0 %s | len(ans): %d", querystr, ansstr, len(x.Answer))

return x
}

return nil
}
7 changes: 5 additions & 2 deletions echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

// mtu on fly is 1420
const mtu = 1420

// runtime.NumCPU() instead?
const udproutines = 4

Expand Down Expand Up @@ -69,6 +70,8 @@ func echoTCP(tcp net.Listener, wg *sync.WaitGroup) {
return
}

defer tcp.Close()

for {
if conn, err := tcp.Accept(); err == nil {
go processtcp(conn)
Expand All @@ -90,6 +93,8 @@ func echoPP(pp *proxyproto.Listener, wg *sync.WaitGroup) {
return
}

defer pp.Close()

for {
if conn, err := pp.Accept(); err == nil {
go processtcp(conn)
Expand All @@ -103,7 +108,6 @@ func echoPP(pp *proxyproto.Listener, wg *sync.WaitGroup) {
}
}


func processtcp(c net.Conn) {
defer c.Close()

Expand All @@ -113,4 +117,3 @@ func processtcp(c net.Conn) {
fmt.Fprint(c, line)
fmt.Fprint(c, c.RemoteAddr())
}

Loading

0 comments on commit 39f7ff9

Please sign in to comment.