14 "www.bamsoftware.com/git/dnstt.git/turbotunnel"
17 // A default Retry-After delay to use when there is no explicit Retry-After
18 // header in an HTTP response.
19 const defaultRetryAfter
= 10 * time
.Second
21 // HTTPPacketConn is an HTTP-based transport for DNS messages, used for DNS over
22 // HTTPS (DoH). Its WriteTo and ReadFrom methods exchange DNS messages over HTTP
23 // requests and responses.
25 // HTTPPacketConn deals only with already formatted DNS messages. It does not
26 // handle encoding information into the messages. That is rather the
27 // responsibility of DNSPacketConn.
29 // https://tools.ietf.org/html/rfc8484
30 type HTTPPacketConn
struct {
31 // client is the http.Client used to make requests. We use this instead
32 // of http.DefaultClient in order to support setting a timeout and a
36 // urlString is the URL to which HTTP requests will be sent, for example
37 // "https://doh.example/dns-query".
40 // notBefore, if not zero, is a time before which we may not send any
41 // queries; queries are buffered or dropped until that time. notBefore
42 // is set when we get a 429 Too Many Requests HTTP response or other
43 // unexpected status code that causes us to need to slow down. It is set
44 // according to the Retry-After header if available, otherwise it is set
45 // to defaultRetryAfter in the future. notBeforeLock controls access to
48 notBeforeLock sync
.RWMutex
50 // QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
51 // sendLoop, via send, removes messages from the outgoing queue that
52 // were placed there by WriteTo, and inserts messages into the incoming
53 // queue to be returned from ReadFrom.
54 *turbotunnel
.QueuePacketConn
57 // NewHTTPPacketConn creates a new HTTPPacketConn configured to use the HTTP
58 // server at urlString as a DNS over HTTP resolver. client is the http.Client
59 // that will be used to make requests. urlString should include any necessary
60 // path components; e.g., "/dns-query". numSenders is the number of concurrent
61 // sender-receiver goroutines to run.
62 func NewHTTPPacketConn(rt http
.RoundTripper
, urlString
string, numSenders
int) (*HTTPPacketConn
, error
) {
66 Timeout
: 1 * time
.Minute
,
69 QueuePacketConn
: turbotunnel
.NewQueuePacketConn(turbotunnel
.DummyAddr
{}, 0),
71 for i
:= 0; i
< numSenders
; i
++ {
77 // send sends a message in an HTTP request, and queues the body HTTP response to
78 // be returned from a future call to ReadFrom.
79 func (c
*HTTPPacketConn
) send(p
[]byte) error
{
80 req
, err
:= http
.NewRequest("POST", c
.urlString
, bytes
.NewReader(p
))
84 req
.Header
.Set("Accept", "application/dns-message")
85 req
.Header
.Set("Content-Type", "application/dns-message")
86 req
.Header
.Set("User-Agent", "") // Disable default "Go-http-client/1.1".
87 resp
, err
:= c
.client
.Do(req
)
91 defer resp
.Body
.Close()
93 switch resp
.StatusCode
{
95 if ct
:= resp
.Header
.Get("Content-Type"); ct
!= "application/dns-message" {
96 return fmt
.Errorf("unknown HTTP response Content-Type %+q", ct
)
98 body
, err
:= ioutil
.ReadAll(io
.LimitReader(resp
.Body
, 64000))
100 c
.QueuePacketConn
.QueueIncoming(body
, turbotunnel
.DummyAddr
{})
102 // Ignore err != nil; don't report an error if we at least
105 // We primarily are thinking of 429 Too Many Requests here, but
106 // any other unexpected response codes will also cause us to
107 // rate-limit ourselves and emit a log message.
108 // https://developers.google.com/speed/public-dns/docs/doh/#errors
110 var retryAfter time
.Time
111 if value
:= resp
.Header
.Get("Retry-After"); value
!= "" {
113 retryAfter
, err
= parseRetryAfter(value
, now
)
115 log
.Printf("cannot parse Retry-After value %+q", value
)
118 if retryAfter
.IsZero() {
120 retryAfter
= now
.Add(defaultRetryAfter
)
122 if retryAfter
.Before(now
) {
123 log
.Printf("got %+q, but Retry-After is %v in the past",
124 resp
.Status
, now
.Sub(retryAfter
))
126 c
.notBeforeLock
.Lock()
127 if retryAfter
.Before(c
.notBefore
) {
128 log
.Printf("got %+q, but Retry-After is %v earlier than already received Retry-After",
129 resp
.Status
, c
.notBefore
.Sub(retryAfter
))
131 log
.Printf("got %+q; ceasing sending for %v",
132 resp
.Status
, retryAfter
.Sub(now
))
133 c
.notBefore
= retryAfter
135 c
.notBeforeLock
.Unlock()
142 // sendLoop loops over the contents of the outgoing queue and passes them to
143 // send. It drops packets while c.notBefore is in the future.
144 func (c
*HTTPPacketConn
) sendLoop() {
145 for p
:= range c
.QueuePacketConn
.OutgoingQueue(turbotunnel
.DummyAddr
{}) {
146 // Stop sending while we are rate-limiting ourselves (as a
147 // result of a Retry-After response header, for example).
148 c
.notBeforeLock
.RLock()
149 notBefore
:= c
.notBefore
150 c
.notBeforeLock
.RUnlock()
151 if wait
:= notBefore
.Sub(time
.Now()); wait
> 0 {
158 log
.Printf("sendLoop: %v", err
)
163 // parseRetryAfter parses the value of a Retry-After header as an absolute
165 func parseRetryAfter(value
string, now time
.Time
) (time
.Time
, error
) {
166 // May be a date string or an integer number of seconds.
167 // https://tools.ietf.org/html/rfc7231#section-7.1.3
168 if t
, err
:= http
.ParseTime(value
); err
== nil {
171 i
, err
:= strconv
.ParseUint(value
, 10, 32)
173 return time
.Time
{}, err
175 return now
.Add(time
.Duration(i
) * time
.Second
), nil