xtransport.go 8.52 KB
Newer Older
Frank Denis's avatar
Frank Denis committed
1 2 3 4 5
package main

import (
	"bytes"
	"context"
6
	"crypto/tls"
7
	"encoding/base64"
Frank Denis's avatar
Frank Denis committed
8 9 10 11 12 13 14
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
15
	"strconv"
Frank Denis's avatar
Frank Denis committed
16 17 18
	"strings"
	"sync"
	"time"
Frank Denis's avatar
Frank Denis committed
19

Frank Denis's avatar
Frank Denis committed
20
	"github.com/jedisct1/dlog"
Frank Denis's avatar
Frank Denis committed
21
	stamps "github.com/jedisct1/go-dnsstamps"
Frank Denis's avatar
Frank Denis committed
22
	"github.com/miekg/dns"
23
	"golang.org/x/net/http2"
24
	netproxy "golang.org/x/net/proxy"
Frank Denis's avatar
Frank Denis committed
25 26
)

27
const DefaultFallbackResolver = "9.9.9.9:53"
Frank Denis's avatar
Frank Denis committed
28 29 30 31 32 33 34

type CachedIPs struct {
	sync.RWMutex
	cache map[string]string
}

type XTransport struct {
35 36 37 38 39 40 41 42 43 44
	transport                *http.Transport
	keepAlive                time.Duration
	timeout                  time.Duration
	cachedIPs                CachedIPs
	fallbackResolver         string
	ignoreSystemDNS          bool
	useIPv4                  bool
	useIPv6                  bool
	tlsDisableSessionTickets bool
	tlsCipherSuite           []uint16
45
	proxyDialer              *netproxy.Dialer
Frank Denis's avatar
Frank Denis committed
46 47
}

48
var DefaultKeepAlive = 5 * time.Second
49
var DefaultTimeout = 30 * time.Second
50

51
func NewXTransport() *XTransport {
Frank Denis's avatar
Frank Denis committed
52
	xTransport := XTransport{
53 54
		cachedIPs:                CachedIPs{cache: make(map[string]string)},
		keepAlive:                DefaultKeepAlive,
55
		timeout:                  DefaultTimeout,
56 57
		fallbackResolver:         DefaultFallbackResolver,
		ignoreSystemDNS:          false,
58 59
		useIPv4:                  true,
		useIPv6:                  false,
60 61
		tlsDisableSessionTickets: false,
		tlsCipherSuite:           nil,
Frank Denis's avatar
Frank Denis committed
62
	}
63 64 65
	return &xTransport
}

66 67 68 69 70 71 72
func (xTransport *XTransport) clearCache() {
	xTransport.cachedIPs.Lock()
	xTransport.cachedIPs.cache = make(map[string]string)
	xTransport.cachedIPs.Unlock()
	dlog.Info("IP cache cleared")
}

73 74 75 76 77 78
func (xTransport *XTransport) rebuildTransport() {
	dlog.Debug("Rebuilding transport")
	if xTransport.transport != nil {
		(*xTransport.transport).CloseIdleConnections()
	}
	timeout := xTransport.timeout
Frank Denis's avatar
Frank Denis committed
79 80 81 82
	transport := &http.Transport{
		DisableKeepAlives:      false,
		DisableCompression:     true,
		MaxIdleConns:           1,
83
		IdleConnTimeout:        xTransport.keepAlive,
Frank Denis's avatar
Frank Denis committed
84 85 86 87
		ResponseHeaderTimeout:  timeout,
		ExpectContinueTimeout:  timeout,
		MaxResponseHeaderBytes: 4096,
		DialContext: func(ctx context.Context, network, addrStr string) (net.Conn, error) {
gdm85's avatar
gdm85 committed
88
			host, port := ExtractHostAndPort(addrStr, stamps.DefaultPort)
Frank Denis's avatar
Frank Denis committed
89 90 91 92 93 94 95 96 97
			ipOnly := host
			xTransport.cachedIPs.RLock()
			cachedIP := xTransport.cachedIPs.cache[host]
			xTransport.cachedIPs.RUnlock()
			if len(cachedIP) > 0 {
				ipOnly = cachedIP
			} else {
				dlog.Debugf("[%s] IP address was not cached", host)
			}
98
			addrStr = ipOnly + ":" + strconv.Itoa(port)
99 100 101 102 103 104
			if xTransport.proxyDialer == nil {
				dialer := &net.Dialer{Timeout: timeout, KeepAlive: timeout, DualStack: true}
				return dialer.DialContext(ctx, network, addrStr)
			} else {
				return (*xTransport.proxyDialer).Dial(network, addrStr)
			}
Frank Denis's avatar
Frank Denis committed
105 106
		},
	}
107 108 109 110
	if xTransport.tlsDisableSessionTickets || xTransport.tlsCipherSuite != nil {
		tlsClientConfig := tls.Config{
			SessionTicketsDisabled: xTransport.tlsDisableSessionTickets,
		}
111 112 113
		if !xTransport.tlsDisableSessionTickets {
			tlsClientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
		}
114 115 116 117 118 119 120
		if xTransport.tlsCipherSuite != nil {
			tlsClientConfig.PreferServerCipherSuites = false
			tlsClientConfig.CipherSuites = xTransport.tlsCipherSuite
		}
		transport.TLSClientConfig = &tlsClientConfig
	}
	http2.ConfigureTransport(transport)
Frank Denis's avatar
Frank Denis committed
121 122 123
	xTransport.transport = transport
}

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
func (xTransport *XTransport) resolve(dnsClient *dns.Client, host string, resolver string) (*string, error) {
	var foundIP *string
	var err error
	if xTransport.useIPv4 {
		msg := new(dns.Msg)
		msg.SetQuestion(dns.Fqdn(host), dns.TypeA)
		msg.SetEdns0(4096, true)
		var in *dns.Msg
		in, _, err = dnsClient.Exchange(msg, resolver)
		if err == nil {
			for _, answer := range in.Answer {
				if answer.Header().Rrtype == dns.TypeA {
					foundIPx := answer.(*dns.A).A.String()
					foundIP = &foundIPx
					return foundIP, nil
				}
			}
		}
	}
	if xTransport.useIPv6 && foundIP == nil {
		msg := new(dns.Msg)
		msg.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)
		msg.SetEdns0(4096, true)
		var in *dns.Msg
		in, _, err = dnsClient.Exchange(msg, resolver)
		if err == nil {
			for _, answer := range in.Answer {
				if answer.Header().Rrtype == dns.TypeAAAA {
					foundIPx := "[" + answer.(*dns.AAAA).AAAA.String() + "]"
					foundIP = &foundIPx
					return foundIP, nil
				}
			}
		}
	}
	return nil, err
}

162
func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string, contentType string, body *io.ReadCloser, timeout time.Duration, padding *string) (*http.Response, time.Duration, error) {
Frank Denis's avatar
Frank Denis committed
163 164 165 166 167 168 169 170 171 172 173
	if timeout <= 0 {
		timeout = xTransport.timeout
	}
	client := http.Client{Transport: xTransport.transport, Timeout: timeout}
	header := map[string][]string{"User-Agent": {"dnscrypt-proxy"}}
	if len(accept) > 0 {
		header["Accept"] = []string{accept}
	}
	if len(contentType) > 0 {
		header["Content-Type"] = []string{contentType}
	}
174 175 176
	if padding != nil {
		header["X-Pad"] = []string{*padding}
	}
Frank Denis's avatar
Frank Denis committed
177 178 179 180 181 182 183 184 185
	req := &http.Request{
		Method: method,
		URL:    url,
		Header: header,
		Close:  false,
	}
	if body != nil {
		req.Body = *body
	}
186
	var err error
Frank Denis's avatar
Frank Denis committed
187 188 189 190
	host := url.Host
	xTransport.cachedIPs.RLock()
	cachedIP := xTransport.cachedIPs.cache[host]
	xTransport.cachedIPs.RUnlock()
191
	if !xTransport.ignoreSystemDNS || len(cachedIP) > 0 {
192
		var resp *http.Response
193
		start := time.Now()
194
		resp, err = client.Do(req)
195 196 197 198 199 200 201 202 203
		rtt := time.Since(start)
		if err == nil {
			if resp == nil {
				err = errors.New("Webserver returned an error")
			} else if resp.StatusCode < 200 || resp.StatusCode > 299 {
				err = fmt.Errorf("Webserver returned code %d", resp.StatusCode)
			}
			return resp, rtt, err
		}
Frank Denis's avatar
Frank Denis committed
204
		(*xTransport.transport).CloseIdleConnections()
205 206 207 208 209
		dlog.Debugf("[%s]: [%s]", req.URL, err)
	} else {
		dlog.Debug("Ignoring system DNS")
	}
	if len(cachedIP) > 0 && err != nil {
Frank Denis's avatar
Frank Denis committed
210
		dlog.Debugf("IP for [%s] was cached to [%s], but connection failed: [%s]", host, cachedIP, err)
211
		return nil, 0, err
Frank Denis's avatar
Frank Denis committed
212
	}
213 214 215 216 217
	if !xTransport.ignoreSystemDNS {
		dlog.Noticef("System DNS configuration not usable yet, exceptionally resolving [%s] using fallback resolver [%s]", host, xTransport.fallbackResolver)
	} else {
		dlog.Debugf("Resolving [%s] using fallback resolver [%s]", host, xTransport.fallbackResolver)
	}
218
	dnsClient := new(dns.Client)
219
	foundIP, err := xTransport.resolve(dnsClient, host, xTransport.fallbackResolver)
220 221 222
	if err != nil {
		return nil, 0, err
	}
223
	if foundIP == nil {
224
		return nil, 0, fmt.Errorf("No IP found for [%s]", host)
Frank Denis's avatar
Frank Denis committed
225 226
	}
	xTransport.cachedIPs.Lock()
227
	xTransport.cachedIPs.cache[host] = *foundIP
Frank Denis's avatar
Frank Denis committed
228
	xTransport.cachedIPs.Unlock()
229
	dlog.Debugf("[%s] IP address [%s] added to the cache", host, *foundIP)
Frank Denis's avatar
Frank Denis committed
230

231 232 233
	start := time.Now()
	resp, err := client.Do(req)
	rtt := time.Since(start)
Frank Denis's avatar
Frank Denis committed
234 235 236 237 238 239
	if err == nil {
		if resp == nil {
			err = errors.New("Webserver returned an error")
		} else if resp.StatusCode < 200 || resp.StatusCode > 299 {
			err = fmt.Errorf("Webserver returned code %d", resp.StatusCode)
		}
240 241
	} else {
		(*xTransport.transport).CloseIdleConnections()
Frank Denis's avatar
Frank Denis committed
242
	}
243 244
	if err != nil {
		dlog.Debugf("[%s]: [%s]", req.URL, err)
245 246 247 248 249
		if xTransport.tlsCipherSuite != nil && strings.Contains(err.Error(), "handshake failure") {
			dlog.Warnf("TLS handshake failure - Try changing or deleting the tls_cipher_suite value in the configuration file")
			xTransport.tlsCipherSuite = nil
			xTransport.rebuildTransport()
		}
250
	}
Frank Denis's avatar
Frank Denis committed
251 252 253
	return resp, rtt, err
}

254
func (xTransport *XTransport) Get(url *url.URL, accept string, timeout time.Duration) (*http.Response, time.Duration, error) {
255
	return xTransport.Fetch("GET", url, "", "", nil, timeout, nil)
Frank Denis's avatar
Frank Denis committed
256 257
}

258
func (xTransport *XTransport) Post(url *url.URL, accept string, contentType string, body []byte, timeout time.Duration, padding *string) (*http.Response, time.Duration, error) {
Frank Denis's avatar
Frank Denis committed
259
	bc := ioutil.NopCloser(bytes.NewReader(body))
260
	return xTransport.Fetch("POST", url, accept, contentType, &bc, timeout, padding)
Frank Denis's avatar
Frank Denis committed
261
}
262

263
func (xTransport *XTransport) DoHQuery(useGet bool, url *url.URL, body []byte, timeout time.Duration) (*http.Response, time.Duration, error) {
264 265
	padLen := 63 - (len(body)+63)&63
	padding := xTransport.makePad(padLen)
Frank Denis's avatar
Frank Denis committed
266
	dataType := "application/dns-message"
267 268 269
	if useGet {
		qs := url.Query()
		qs.Add("ct", "")
270 271 272
		encBody := base64.RawURLEncoding.EncodeToString(body)
		qs.Add("body", encBody)
		qs.Add("dns", encBody)
273 274 275
		if padding != nil {
			qs.Add("random_padding", *padding)
		}
276 277 278 279
		url2 := *url
		url2.RawQuery = qs.Encode()
		return xTransport.Get(&url2, dataType, timeout)
	}
280 281 282 283 284 285 286 287 288
	return xTransport.Post(url, dataType, dataType, body, timeout, padding)
}

func (xTransport *XTransport) makePad(padLen int) *string {
	if padLen <= 0 {
		return nil
	}
	padding := strings.Repeat("X", padLen)
	return &padding
289
}