aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2018-09-19 05:25:07 +0700
committerShulhan <ms@kilabit.info>2018-09-25 08:28:07 +0700
commitc78c938b264e1232df0fe5e2b75d282fff696b7a (patch)
treeddea649b62fd3af324fdb72dbb01f9c2868051c7
parent449678df29018f845a0eadeba3948bbc8eed116d (diff)
downloadpakakeh.go-c78c938b264e1232df0fe5e2b75d282fff696b7a.tar.xz
lib/dns: implement client and server for DNS over HTTPS
The implementation is based on latest draft [1]. [1] https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-14
-rw-r--r--lib/dns/dns.go4
-rw-r--r--lib/dns/dns_test.go13
-rw-r--r--lib/dns/dohclient.go188
-rw-r--r--lib/dns/dohclient_test.go366
-rw-r--r--lib/dns/example_server_test.go3
-rw-r--r--lib/dns/request.go8
-rw-r--r--lib/dns/server.go138
-rw-r--r--lib/dns/testdata/domain.crt24
-rw-r--r--lib/dns/testdata/domain.key28
9 files changed, 757 insertions, 15 deletions
diff --git a/lib/dns/dns.go b/lib/dns/dns.go
index c1680f72..52211ef5 100644
--- a/lib/dns/dns.go
+++ b/lib/dns/dns.go
@@ -36,6 +36,10 @@ const (
rdataIPv6Size = 16
// sectionHeaderSize define the size of section header in DNS message.
sectionHeaderSize = 12
+
+ dohHeaderKeyAccept = "accept"
+ dohHeaderKeyContentType = "content-type"
+ dohHeaderValDNSMessage = "application/dns-message"
)
//
diff --git a/lib/dns/dns_test.go b/lib/dns/dns_test.go
index 01185428..cdaa875b 100644
--- a/lib/dns/dns_test.go
+++ b/lib/dns/dns_test.go
@@ -167,9 +167,13 @@ func (h *serverHandler) ServeDNS(req *Request) {
res.SetID(req.Message.Header.ID)
}
- _, err = req.Sender.Send(res, req.UDPAddr)
- if err != nil {
- log.Println("ServeDNS: ", err)
+ if req.Sender != nil {
+ _, err = req.Sender.Send(res, req.UDPAddr)
+ if err != nil {
+ log.Println("ServeDNS: ", err)
+ }
+ } else if req.ChanMessage != nil {
+ req.ChanMessage <- res
}
_testServer.FreeRequest(req)
@@ -188,7 +192,8 @@ func TestMain(m *testing.M) {
}
go func() {
- err := _testServer.ListenAndServe(testServerAddress)
+ err := _testServer.ListenAndServe(testServerAddress,
+ "testdata/domain.crt", "testdata/domain.key", true)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
diff --git a/lib/dns/dohclient.go b/lib/dns/dohclient.go
new file mode 100644
index 00000000..98ebac2f
--- /dev/null
+++ b/lib/dns/dohclient.go
@@ -0,0 +1,188 @@
+// Copyright 2018, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dns
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/base64"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+//
+// DoHClient client for DNS over HTTPS.
+//
+type DoHClient struct {
+ addr *url.URL
+ headers http.Header
+ req *http.Request
+ query url.Values
+ conn *http.Client
+}
+
+//
+// NewDoHClient will create new DNS client with HTTP connection.
+//
+func NewDoHClient(nameserver string, allowInsecure bool) (*DoHClient, error) {
+ nsURL, err := url.Parse(nameserver)
+ if err != nil {
+ return nil, err
+ }
+
+ if nsURL.Scheme != "https" {
+ err = fmt.Errorf("DoH name server must be HTTPS")
+ return nil, err
+ }
+
+ tr := &http.Transport{
+ MaxIdleConns: 1,
+ IdleConnTimeout: 30 * time.Second,
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: allowInsecure,
+ },
+ }
+
+ cl := &DoHClient{
+ addr: nsURL,
+ headers: http.Header{
+ "accept": []string{
+ "application/dns-message",
+ },
+ },
+ query: nsURL.Query(),
+ conn: &http.Client{
+ Transport: tr,
+ Timeout: clientTimeout,
+ },
+ }
+
+ cl.req = &http.Request{
+ Method: http.MethodGet,
+ URL: nsURL,
+ Proto: "HTTP/2",
+ ProtoMajor: 2,
+ ProtoMinor: 0,
+ Header: cl.headers,
+ Body: nil,
+ Host: nsURL.Hostname(),
+ }
+
+ return cl, nil
+}
+
+func (cl *DoHClient) Lookup(qtype, qclass uint16, qname []byte) (*Message, error) {
+ if len(qname) == 0 {
+ return nil, nil
+ }
+ if qtype == 0 {
+ qtype = QueryTypeA
+ }
+ if qclass == 0 {
+ qclass = QueryClassIN
+ }
+
+ msg := NewMessage()
+
+ msg.Question.Type = qtype
+ msg.Question.Class = qclass
+ msg.Question.Name = append(msg.Question.Name, qname...)
+
+ _, err := msg.Pack()
+ if err != nil {
+ return nil, err
+ }
+
+ res, err := cl.Get(msg)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, err
+}
+
+//
+// Post send query to name server using HTTP POST and return the response
+// as unpacked message.
+//
+func (cl *DoHClient) Post(msg *Message) (*Message, error) {
+ cl.req.Method = http.MethodPost
+ cl.req.Body = ioutil.NopCloser(bytes.NewReader(msg.Packet))
+ cl.req.URL.RawQuery = ""
+
+ httpRes, err := cl.conn.Do(cl.req)
+ if err != nil {
+ cl.req.Body.Close()
+ return nil, err
+ }
+ cl.req.Body.Close()
+
+ res := NewMessage()
+
+ packet, err := ioutil.ReadAll(httpRes.Body)
+ if err != nil {
+ httpRes.Body.Close()
+ return nil, err
+ }
+
+ res.Packet = append(res.Packet[:0], packet...)
+
+ httpRes.Body.Close()
+
+ err = res.Unpack()
+
+ return res, err
+}
+
+//
+// Get send query to name server using HTTP GET and return the response as
+// unpacked message.
+//
+func (cl *DoHClient) Get(msg *Message) (*Message, error) {
+ q := base64.RawURLEncoding.EncodeToString(msg.Packet)
+
+ cl.query.Set("dns", q)
+ cl.req.Method = http.MethodGet
+ cl.req.Body = nil
+ cl.req.URL.RawQuery = cl.query.Encode()
+
+ httpRes, err := cl.conn.Do(cl.req)
+ if err != nil {
+ return nil, err
+ }
+
+ if httpRes.StatusCode != 200 {
+ body, err := ioutil.ReadAll(httpRes.Body)
+ if err != nil {
+ return nil, err
+ }
+ err = fmt.Errorf("%s", string(body))
+ return nil, err
+ }
+
+ res := NewMessage()
+
+ packet, err := ioutil.ReadAll(httpRes.Body)
+ if err != nil {
+ httpRes.Body.Close()
+ return nil, err
+ }
+
+ res.Packet = append(res.Packet[:0], packet...)
+
+ httpRes.Body.Close()
+
+ if len(res.Packet) > 20 {
+ err = res.Unpack()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return res, err
+}
diff --git a/lib/dns/dohclient_test.go b/lib/dns/dohclient_test.go
new file mode 100644
index 00000000..a94ac4c9
--- /dev/null
+++ b/lib/dns/dohclient_test.go
@@ -0,0 +1,366 @@
+// Copyright 2018, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dns
+
+import (
+ "testing"
+
+ "github.com/shuLhan/share/lib/test"
+)
+
+func TestDoHClient_Lookup(t *testing.T) {
+ nameserver := "https://127.0.0.1:8443/dns-query"
+
+ cl, err := NewDoHClient(nameserver, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cases := []struct {
+ desc string
+ qtype uint16
+ qclass uint16
+ qname []byte
+ exp *Message
+ }{{
+ desc: "QType:A QClass:IN QName:kilabit.info",
+ qtype: QueryTypeA,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ exp: &Message{
+ Header: &SectionHeader{
+ ID: 0,
+ QDCount: 1,
+ ANCount: 1,
+ },
+ Question: &SectionQuestion{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeA,
+ Class: QueryClassIN,
+ },
+ Answer: []*ResourceRecord{{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeA,
+ Class: QueryClassIN,
+ TTL: 3600,
+ rdlen: 4,
+ Text: &RDataText{
+ Value: []byte("127.0.0.1"),
+ },
+ }},
+ Authority: []*ResourceRecord{},
+ Additional: []*ResourceRecord{},
+ },
+ }, {
+ desc: "QType:SOA QClass:IN QName:kilabit.info",
+ qtype: QueryTypeSOA,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ exp: &Message{
+ Header: &SectionHeader{
+ ID: 0,
+ QDCount: 1,
+ ANCount: 1,
+ },
+ Question: &SectionQuestion{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeSOA,
+ Class: QueryClassIN,
+ },
+ Answer: []*ResourceRecord{{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeSOA,
+ Class: QueryClassIN,
+ TTL: 3600,
+ SOA: &RDataSOA{
+ MName: []byte("kilabit.info"),
+ RName: []byte("admin.kilabit.info"),
+ Serial: 20180832,
+ Refresh: 3600,
+ Retry: 60,
+ Expire: 3600,
+ Minimum: 3600,
+ },
+ }},
+ Authority: []*ResourceRecord{},
+ Additional: []*ResourceRecord{},
+ },
+ }, {
+ desc: "QType:TXT QClass:IN QName:kilabit.info",
+ qtype: QueryTypeTXT,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ exp: &Message{
+ Header: &SectionHeader{
+ ID: 0,
+ QDCount: 1,
+ ANCount: 1,
+ },
+ Question: &SectionQuestion{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeTXT,
+ Class: QueryClassIN,
+ },
+ Answer: []*ResourceRecord{{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeTXT,
+ Class: QueryClassIN,
+ TTL: 3600,
+ Text: &RDataText{
+ Value: []byte("This is a test server"),
+ },
+ }},
+ Authority: []*ResourceRecord{},
+ Additional: []*ResourceRecord{},
+ },
+ }, {
+ desc: "QType:AAAA QClass:IN QName:kilabit.info",
+ qtype: QueryTypeAAAA,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ exp: &Message{
+ Header: &SectionHeader{
+ ID: 0,
+ QDCount: 1,
+ },
+ Question: &SectionQuestion{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeAAAA,
+ Class: QueryClassIN,
+ },
+ Answer: []*ResourceRecord{},
+ Authority: []*ResourceRecord{},
+ Additional: []*ResourceRecord{},
+ },
+ }}
+
+ for _, c := range cases {
+ t.Log(c.desc)
+
+ got, err := cl.Lookup(c.qtype, c.qclass, c.qname)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = c.exp.Pack()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ test.Assert(t, "Packet", c.exp.Packet, got.Packet, true)
+ }
+}
+
+func TestDoHClient_Post(t *testing.T) {
+ nameserver := "https://127.0.0.1:8443/dns-query"
+
+ cl, err := NewDoHClient(nameserver, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cases := []struct {
+ desc string
+ qtype uint16
+ qclass uint16
+ qname []byte
+ exp *Message
+ }{{
+ desc: "QType:A QClass:IN QName:kilabit.info",
+ qtype: QueryTypeA,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ exp: &Message{
+ Header: &SectionHeader{
+ ID: 0,
+ QDCount: 1,
+ ANCount: 1,
+ },
+ Question: &SectionQuestion{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeA,
+ Class: QueryClassIN,
+ },
+ Answer: []*ResourceRecord{{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeA,
+ Class: QueryClassIN,
+ TTL: 3600,
+ rdlen: 4,
+ Text: &RDataText{
+ Value: []byte("127.0.0.1"),
+ },
+ }},
+ Authority: []*ResourceRecord{},
+ Additional: []*ResourceRecord{},
+ },
+ }, {
+ desc: "QType:SOA QClass:IN QName:kilabit.info",
+ qtype: QueryTypeSOA,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ exp: &Message{
+ Header: &SectionHeader{
+ ID: 0,
+ QDCount: 1,
+ ANCount: 1,
+ },
+ Question: &SectionQuestion{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeSOA,
+ Class: QueryClassIN,
+ },
+ Answer: []*ResourceRecord{{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeSOA,
+ Class: QueryClassIN,
+ TTL: 3600,
+ SOA: &RDataSOA{
+ MName: []byte("kilabit.info"),
+ RName: []byte("admin.kilabit.info"),
+ Serial: 20180832,
+ Refresh: 3600,
+ Retry: 60,
+ Expire: 3600,
+ Minimum: 3600,
+ },
+ }},
+ Authority: []*ResourceRecord{},
+ Additional: []*ResourceRecord{},
+ },
+ }, {
+ desc: "QType:TXT QClass:IN QName:kilabit.info",
+ qtype: QueryTypeTXT,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ exp: &Message{
+ Header: &SectionHeader{
+ ID: 0,
+ QDCount: 1,
+ ANCount: 1,
+ },
+ Question: &SectionQuestion{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeTXT,
+ Class: QueryClassIN,
+ },
+ Answer: []*ResourceRecord{{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeTXT,
+ Class: QueryClassIN,
+ TTL: 3600,
+ Text: &RDataText{
+ Value: []byte("This is a test server"),
+ },
+ }},
+ Authority: []*ResourceRecord{},
+ Additional: []*ResourceRecord{},
+ },
+ }, {
+ desc: "QType:AAAA QClass:IN QName:kilabit.info",
+ qtype: QueryTypeAAAA,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ exp: &Message{
+ Header: &SectionHeader{
+ ID: 0,
+ QDCount: 1,
+ },
+ Question: &SectionQuestion{
+ Name: []byte("kilabit.info"),
+ Type: QueryTypeAAAA,
+ Class: QueryClassIN,
+ },
+ Answer: []*ResourceRecord{},
+ Authority: []*ResourceRecord{},
+ Additional: []*ResourceRecord{},
+ },
+ }}
+
+ for _, c := range cases {
+ t.Log(c.desc)
+
+ msg := NewMessage()
+
+ msg.Question.Type = c.qtype
+ msg.Question.Class = c.qclass
+ msg.Question.Name = append(msg.Question.Name, c.qname...)
+
+ _, err := msg.Pack()
+ if err != nil {
+ t.Fatal("msg.Pack:", err)
+ }
+
+ got, err := cl.Post(msg)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = c.exp.Pack()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ test.Assert(t, "Packet", c.exp.Packet, got.Packet, true)
+ }
+}
+
+func TestDoHClient_Get(t *testing.T) {
+ nameserver := "https://127.0.0.1:8443/dns-invalid"
+
+ cl, err := NewDoHClient(nameserver, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cases := []struct {
+ desc string
+ qtype uint16
+ qclass uint16
+ qname []byte
+ exp *Message
+ expErr string
+ }{{
+ desc: "QType:A QClass:IN QName:kilabit.info",
+ qtype: QueryTypeA,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ expErr: "404 page not found\n",
+ }, {
+ desc: "QType:A QClass:IN QName:kilabit.info",
+ qtype: QueryTypeA,
+ qclass: QueryClassIN,
+ qname: []byte("kilabit.info"),
+ expErr: "404 page not found\n",
+ }}
+
+ for _, c := range cases {
+ t.Log(c.desc)
+
+ msg := NewMessage()
+
+ msg.Question.Type = c.qtype
+ msg.Question.Class = c.qclass
+ msg.Question.Name = append(msg.Question.Name, c.qname...)
+
+ _, err := msg.Pack()
+ if err != nil {
+ t.Fatal("msg.Pack:", err)
+ }
+
+ got, err := cl.Get(msg)
+ if err != nil {
+ test.Assert(t, "error", c.expErr, err.Error(), true)
+ continue
+ }
+
+ _, err = c.exp.Pack()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ test.Assert(t, "Packet", c.exp.Packet, got.Packet, true)
+ }
+}
diff --git a/lib/dns/example_server_test.go b/lib/dns/example_server_test.go
index a6970bfe..020e7907 100644
--- a/lib/dns/example_server_test.go
+++ b/lib/dns/example_server_test.go
@@ -195,7 +195,8 @@ func ExampleServer() {
}
go func() {
- err := server.ListenAndServe(serverAddress)
+ err := server.ListenAndServe(serverAddress,
+ "testdata/domain.crt", "testdata/domain.key", true)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
diff --git a/lib/dns/request.go b/lib/dns/request.go
index 052fdc22..54a84d82 100644
--- a/lib/dns/request.go
+++ b/lib/dns/request.go
@@ -22,9 +22,10 @@ var _requestPool = sync.Pool{
// Request contains UDP address and DNS query message from client.
//
type Request struct {
- Message *Message
- UDPAddr *net.UDPAddr
- Sender Sender
+ Message *Message
+ UDPAddr *net.UDPAddr
+ Sender Sender
+ ChanMessage chan *Message
}
//
@@ -34,4 +35,5 @@ func (req *Request) Reset() {
req.Message.Reset()
req.UDPAddr = nil
req.Sender = nil
+ req.ChanMessage = nil
}
diff --git a/lib/dns/server.go b/lib/dns/server.go
index 06274e09..99a93cb8 100644
--- a/lib/dns/server.go
+++ b/lib/dns/server.go
@@ -5,9 +5,15 @@
package dns
import (
+ "crypto/tls"
+ "encoding/base64"
"io"
+ "io/ioutil"
"log"
"net"
+ "net/http"
+ "strings"
+ "time"
libnet "github.com/shuLhan/share/lib/net"
)
@@ -19,34 +25,40 @@ type Server struct {
Handler Handler
udp *net.UDPConn
tcp *net.TCPListener
+ doh *http.Server
}
-func parseAddress(address string) (*net.UDPAddr, *net.TCPAddr, error) {
+func parseAddress(address string) (udp *net.UDPAddr, tcp, doh *net.TCPAddr, err error) {
ip, port, err := libnet.ParseIPPort(address, DefaultPort)
if err != nil {
- return nil, nil, err
+ return
}
- udpAddr := &net.UDPAddr{
+ udp = &net.UDPAddr{
IP: ip,
Port: int(port),
}
- tcpAddr := &net.TCPAddr{
+ tcp = &net.TCPAddr{
IP: ip,
Port: int(port),
}
- return udpAddr, tcpAddr, nil
+ doh = &net.TCPAddr{
+ IP: ip,
+ Port: 8443,
+ }
+
+ return
}
//
// ListenAndServe run DNS server, listening on UDP and TCP connection.
//
-func (srv *Server) ListenAndServe(address string) error {
+func (srv *Server) ListenAndServe(address, certFile, keyFile string, allowInsecure bool) error {
var err error
- udpAddr, tcpAddr, err := parseAddress(address)
+ udpAddr, tcpAddr, dohAddr, err := parseAddress(address)
if err != nil {
return err
}
@@ -65,6 +77,14 @@ func (srv *Server) ListenAndServe(address string) error {
cherr <- err
}
}()
+ if len(certFile) > 0 && len(keyFile) > 0 {
+ go func() {
+ err = srv.ListenAndServeDoH(dohAddr, certFile, keyFile, allowInsecure)
+ if err != nil {
+ cherr <- err
+ }
+ }()
+ }
err = <-cherr
@@ -72,6 +92,110 @@ func (srv *Server) ListenAndServe(address string) error {
}
//
+// ListenAndServeDoH listen for request over HTTPS using certificate and key
+// file in parameter. The path to request is static "/dns-query".
+//
+func (srv *Server) ListenAndServeDoH(address *net.TCPAddr, certFile, keyFile string, allowInsecure bool) error {
+ srv.doh = &http.Server{
+ Addr: address.String(),
+ IdleTimeout: 120 * time.Second,
+ TLSConfig: &tls.Config{
+ InsecureSkipVerify: allowInsecure,
+ },
+ }
+
+ http.Handle("/dns-query", srv)
+
+ err := srv.doh.ListenAndServeTLS(certFile, keyFile)
+
+ return err
+}
+
+func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ hdr := w.Header()
+ hdr.Set(dohHeaderKeyContentType, dohHeaderValDNSMessage)
+
+ hdrAcceptValue := r.Header.Get(dohHeaderKeyAccept)
+ if len(hdrAcceptValue) == 0 {
+ w.WriteHeader(http.StatusUnsupportedMediaType)
+ return
+ }
+
+ hdrAcceptValue = strings.ToLower(hdrAcceptValue)
+ if hdrAcceptValue != dohHeaderValDNSMessage {
+ w.WriteHeader(http.StatusUnsupportedMediaType)
+ return
+ }
+
+ if r.Method == http.MethodGet {
+ srv.handleDoHGet(w, r)
+ return
+ }
+ if r.Method == http.MethodPost {
+ srv.handleDoHPost(w, r)
+ return
+ }
+
+ w.WriteHeader(http.StatusMethodNotAllowed)
+}
+
+func (srv *Server) handleDoHGet(w http.ResponseWriter, r *http.Request) {
+ q := r.URL.Query()
+ msgBase64 := q.Get("dns")
+
+ if len(msgBase64) == 0 {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ raw, err := base64.RawURLEncoding.DecodeString(msgBase64)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ srv.handleDoHRequest(raw, w)
+}
+
+func (srv *Server) handleDoHPost(w http.ResponseWriter, r *http.Request) {
+ raw, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ srv.handleDoHRequest(raw, w)
+}
+
+func (srv *Server) handleDoHRequest(raw []byte, w http.ResponseWriter) {
+ req := _requestPool.Get().(*Request)
+ req.Reset()
+ req.ChanMessage = make(chan *Message, 1)
+ req.Message.Packet = append(req.Message.Packet[:0], raw...)
+ req.Message.UnpackHeaderQuestion()
+
+ srv.Handler.ServeDNS(req)
+
+ timeout := time.NewTicker(clientTimeout)
+ for {
+ select {
+ case res := <-req.ChanMessage:
+ _, err := w.Write(res.Packet)
+ if err != nil {
+ log.Printf("! handleDoHRequest: %s\n", err)
+ }
+ goto out
+
+ case <-timeout.C:
+ w.WriteHeader(http.StatusGatewayTimeout)
+ goto out
+ }
+ }
+out:
+ timeout.Stop()
+}
+
+//
// ListenAndServeTCP listen for request with TCP socket.
//
func (srv *Server) ListenAndServeTCP(tcpAddr *net.TCPAddr) error {
diff --git a/lib/dns/testdata/domain.crt b/lib/dns/testdata/domain.crt
new file mode 100644
index 00000000..01501086
--- /dev/null
+++ b/lib/dns/testdata/domain.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEBzCCAu+gAwIBAgIUJYHUkLhtYbIcfjnslpHyKqOCxY0wDQYJKoZIhvcNAQEL
+BQAwgZIxCzAJBgNVBAYTAklEMREwDwYDVQQIDAhXRVNUSkFWQTEQMA4GA1UEBwwH
+QkFORFVORzEVMBMGA1UECgwMS0lMQUJJVC5JTkZPMRUwEwYDVQQLDAxLSUxBQklU
+LklORk8xEDAOBgNVBAMMB1NIVUxIQU4xHjAcBgkqhkiG9w0BCQEWD21zQGtpbGFi
+aXQuaW5mbzAeFw0xODA5MTgyMTEzMjNaFw0yODA5MTUyMTEzMjNaMIGSMQswCQYD
+VQQGEwJJRDERMA8GA1UECAwIV0VTVEpBVkExEDAOBgNVBAcMB0JBTkRVTkcxFTAT
+BgNVBAoMDEtJTEFCSVQuSU5GTzEVMBMGA1UECwwMS0lMQUJJVC5JTkZPMRAwDgYD
+VQQDDAdTSFVMSEFOMR4wHAYJKoZIhvcNAQkBFg9tc0BraWxhYml0LmluZm8wggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCckWPz7mzmx8/TYm3tg2W/cVMc
+clraj+Gkf6KPEeZp310iWnQNV/DVNe+a1W8qrCgKT48xpWu/vqwmsUqpPe/sSGl2
+FJPyJORwVq4aKZkv1UAqk/5t/2bH69Meg+m0SznrVw7egvw/syUInT0Z27KGWmp9
+duMibgGSSHoBbw4y+EPVXQnMI0kDsB9hRxddu/gTxSoia6vJf/IFwqAWvECzzUjJ
+YbI2Lq6I3U11ejc7XIGXB23uKnas0/aOlWMV7OHnDKqEZ7FEohe1IXwOQU0cLJ94
+P82u1flzdh9ntjIH0LOfNrpYk5Kvll4Cm1u9HPLCC9lfUoMhDb2M3x24Q78XAgMB
+AAGjUzBRMB0GA1UdDgQWBBTk99WkqIL8oDVQwtP46WXeXtJ4KzAfBgNVHSMEGDAW
+gBTk99WkqIL8oDVQwtP46WXeXtJ4KzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
+DQEBCwUAA4IBAQAWYPRf2F9eE4EPuNBd6bEEbLEceL56b1C11Sivf4DW2PJwYbvd
+0Uky7Hc0FkBEVL1a0GA+sJ0mNsTVTs/O8cU5VLkKCmoEuewlDF2YliH/mY6A5NDV
+sumblp0JrpH4L6DEX2ktZ+9tTzguvg6HGZOR+HgBP6xaSpm8Tb06iYfgYtehTNEW
+s6Ws67+E59qQFMKTMgqudkixNsr2CkdjtBCdPRQ8Y/Mj3yl57npnLQi/ltpq1BBH
+i6wfhTGQX4uo0XU7hSZw9Nx1BQ4TreO3kirnN7kNxxPBwZyg4+I7YAJ3SKDCEQ7O
+g6zg8CQQHszwsso4BMwZfIIJtpocu8Qba81g
+-----END CERTIFICATE-----
diff --git a/lib/dns/testdata/domain.key b/lib/dns/testdata/domain.key
new file mode 100644
index 00000000..4660b947
--- /dev/null
+++ b/lib/dns/testdata/domain.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCckWPz7mzmx8/T
+Ym3tg2W/cVMcclraj+Gkf6KPEeZp310iWnQNV/DVNe+a1W8qrCgKT48xpWu/vqwm
+sUqpPe/sSGl2FJPyJORwVq4aKZkv1UAqk/5t/2bH69Meg+m0SznrVw7egvw/syUI
+nT0Z27KGWmp9duMibgGSSHoBbw4y+EPVXQnMI0kDsB9hRxddu/gTxSoia6vJf/IF
+wqAWvECzzUjJYbI2Lq6I3U11ejc7XIGXB23uKnas0/aOlWMV7OHnDKqEZ7FEohe1
+IXwOQU0cLJ94P82u1flzdh9ntjIH0LOfNrpYk5Kvll4Cm1u9HPLCC9lfUoMhDb2M
+3x24Q78XAgMBAAECggEAbiKkEhKVDp5d5k+mDl7Q8yNvmGIk4Pw3ePTD0CqCT9Vs
++V5xpnVHF2RSgTNEeNsTa3VdwEmiCwbAqJMsdvL309l4PjTpgXtMKm3/GK5McOZs
+tcbXQl9X2KheIWgfvNDyFEdwUTwI33JQScf6FDeEVJhDsrAvxKdneZR8JogNj/IU
+Z88jI1Qfc4DamfVi3cEAfXn2i0Vo1nvfHFbxUZqQkmw4RjYum+keDKg0fLa907zH
+yV6zcqxsqDktwX9QW176M9WZ7yB6H0XNjSAkSrOFgVgOd6tSbYeGWlVf5Axw0B5I
+5C2iV0CmVMSWjnCwEe0D16H/DgT9/VXSbvEboTJ6YQKBgQDL1ScVc7+6PQLXsuLe
+I80cQBQ7rSE/J0DPG5gIFZgGrJi4zM7lo6Dnyb0W8AvFPpSHuEsYO38p/NOuBiBi
+uvqFxpiGQudxZtYFEWwDr1Ay0NOu89UQbxo/48Bgz/KRfYJ6JIgMUJ9xeB7YjZE2
+Mrxmu5DugY+5MFS9UFouxNL50QKBgQDEo4RDdlF5BXraNymeLfS10cGUTx6WkiC4
+G04Hjl9LEJ0BY1oSBg19l6AxVzfHPYgXVWVDq7TTo5okH6eHd7h7qdeXj3UlyQSl
+47K1QU5s+sfwoqjuvm/awhU7qrQlY0GTHn/MXMEBX5DTHEuemT9m+jyTS4K3zWU2
+bf/QKGB8ZwKBgQC1NLtYMNyjnpWmWFujjERN9xGFs/Y4hJbzB97yYPAUDuB+eWT9
+dagYJ5q4h5KPOYEl3sqzskDsfN1aegvUedE5mEIEKfpDMF7XhpN1+yba5hcqE464
+22yEm95ssrE8ck3KdCuWdx4n69fQQJp1ikk/M0Q3JGs3ASZ0Xritl0DP4QKBgHRM
+pchkrTEfrZZsc7/rPEVhBtXZqaSyTom1FIRhjzjNXZ7ZjQcF72qtiABGrmW3ncr3
+JcpNPsjBhUQCOMplY4Y4YJtyLH4pkwcuUZ7kPic0d5Z6DeIOXgeLLJW6k4tdVgZW
+To2m+jv+sqA5pvvpdVdJfxQ639gnscnsaxVJHC/XAoGBAIx5chZZesECe7ij+2c5
+3sPM8ZcNzxUmOF2mZ3yLy+x42suairfXlAQpLTAnjW+Nx3QslC4eV89kjU/a5iyc
+fAiwWSQGk972CY9h0CwO4ykj3oOyYi6ZfrkRaDf6A9QD5EMUBP4oPePEJbCmDIsJ
+VITYrHkjJsDULGIq4JX+zxdd
+-----END PRIVATE KEY-----