2018-01-30 15:51:07 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2018-02-05 11:30:10 +01:00
|
|
|
"encoding/base64"
|
2018-01-30 15:51:07 +01:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/jedisct1/dlog"
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
)
|
|
|
|
|
2018-03-22 02:43:03 +01:00
|
|
|
const DefaultFallbackResolver = "9.9.9.9:53"
|
2018-01-30 15:51:07 +01:00
|
|
|
|
|
|
|
type CachedIPs struct {
|
|
|
|
sync.RWMutex
|
|
|
|
cache map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
type XTransport struct {
|
|
|
|
transport *http.Transport
|
|
|
|
timeout time.Duration
|
|
|
|
cachedIPs CachedIPs
|
|
|
|
fallbackResolver string
|
2018-01-30 17:37:35 +01:00
|
|
|
ignoreSystemDNS bool
|
2018-02-26 14:50:01 +01:00
|
|
|
useIPv4 bool
|
|
|
|
useIPv6 bool
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
|
|
|
|
2018-02-05 19:03:04 +01:00
|
|
|
var IdleConnTimeout = 5 * time.Second
|
|
|
|
|
2018-02-26 14:50:01 +01:00
|
|
|
func NewXTransport(timeout time.Duration, useIPv4 bool, useIPv6 bool) *XTransport {
|
2018-01-30 15:51:07 +01:00
|
|
|
xTransport := XTransport{
|
|
|
|
cachedIPs: CachedIPs{cache: make(map[string]string)},
|
|
|
|
timeout: timeout,
|
|
|
|
fallbackResolver: DefaultFallbackResolver,
|
2018-01-30 17:37:35 +01:00
|
|
|
ignoreSystemDNS: false,
|
2018-02-26 14:50:01 +01:00
|
|
|
useIPv4: useIPv4,
|
|
|
|
useIPv6: useIPv6,
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
2018-02-05 19:03:04 +01:00
|
|
|
xTransport.rebuildTransport()
|
|
|
|
return &xTransport
|
|
|
|
}
|
|
|
|
|
2018-03-21 09:32:35 +01:00
|
|
|
func (xTransport *XTransport) clearCache() {
|
|
|
|
xTransport.cachedIPs.Lock()
|
|
|
|
xTransport.cachedIPs.cache = make(map[string]string)
|
|
|
|
xTransport.cachedIPs.Unlock()
|
|
|
|
dlog.Info("IP cache cleared")
|
|
|
|
}
|
|
|
|
|
2018-02-05 19:03:04 +01:00
|
|
|
func (xTransport *XTransport) rebuildTransport() {
|
|
|
|
dlog.Debug("Rebuilding transport")
|
|
|
|
if xTransport.transport != nil {
|
|
|
|
(*xTransport.transport).CloseIdleConnections()
|
|
|
|
}
|
|
|
|
timeout := xTransport.timeout
|
2018-01-30 15:51:07 +01:00
|
|
|
dialer := &net.Dialer{Timeout: timeout, KeepAlive: timeout, DualStack: true}
|
|
|
|
transport := &http.Transport{
|
|
|
|
DisableKeepAlives: false,
|
|
|
|
DisableCompression: true,
|
|
|
|
MaxIdleConns: 1,
|
2018-02-05 19:03:04 +01:00
|
|
|
IdleConnTimeout: IdleConnTimeout,
|
2018-01-30 15:51:07 +01:00
|
|
|
ResponseHeaderTimeout: timeout,
|
|
|
|
ExpectContinueTimeout: timeout,
|
|
|
|
MaxResponseHeaderBytes: 4096,
|
|
|
|
DialContext: func(ctx context.Context, network, addrStr string) (net.Conn, error) {
|
2018-03-28 12:08:05 +02:00
|
|
|
host, port := ExtractHostAndPort(addrStr, DefaultPort)
|
2018-01-30 15:51:07 +01:00
|
|
|
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)
|
|
|
|
}
|
2018-03-28 12:08:05 +02:00
|
|
|
addrStr = ipOnly + ":" + string(port)
|
2018-01-30 15:51:07 +01:00
|
|
|
return dialer.DialContext(ctx, network, addrStr)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
xTransport.transport = transport
|
|
|
|
}
|
|
|
|
|
2018-03-21 10:03:05 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-02-27 09:30:09 +01:00
|
|
|
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) {
|
2018-01-30 15:51:07 +01:00
|
|
|
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}
|
|
|
|
}
|
2018-02-27 09:30:09 +01:00
|
|
|
if padding != nil {
|
|
|
|
header["X-Pad"] = []string{*padding}
|
|
|
|
}
|
2018-01-30 15:51:07 +01:00
|
|
|
req := &http.Request{
|
|
|
|
Method: method,
|
|
|
|
URL: url,
|
|
|
|
Header: header,
|
|
|
|
Close: false,
|
|
|
|
}
|
|
|
|
if body != nil {
|
|
|
|
req.Body = *body
|
|
|
|
}
|
2018-01-30 17:37:35 +01:00
|
|
|
var err error
|
2018-01-30 15:51:07 +01:00
|
|
|
host := url.Host
|
|
|
|
xTransport.cachedIPs.RLock()
|
|
|
|
cachedIP := xTransport.cachedIPs.cache[host]
|
|
|
|
xTransport.cachedIPs.RUnlock()
|
2018-01-30 17:37:35 +01:00
|
|
|
if !xTransport.ignoreSystemDNS || len(cachedIP) > 0 {
|
2018-02-05 19:03:04 +01:00
|
|
|
var resp *http.Response
|
2018-01-30 17:37:35 +01:00
|
|
|
start := time.Now()
|
2018-02-05 19:03:04 +01:00
|
|
|
resp, err = client.Do(req)
|
2018-01-30 17:37:35 +01:00
|
|
|
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
|
|
|
|
}
|
2018-03-17 22:47:17 +01:00
|
|
|
(*xTransport.transport).CloseIdleConnections()
|
2018-01-30 17:37:35 +01:00
|
|
|
dlog.Debugf("[%s]: [%s]", req.URL, err)
|
|
|
|
} else {
|
|
|
|
dlog.Debug("Ignoring system DNS")
|
|
|
|
}
|
|
|
|
if len(cachedIP) > 0 && err != nil {
|
2018-01-30 15:51:07 +01:00
|
|
|
dlog.Debugf("IP for [%s] was cached to [%s], but connection failed: [%s]", host, cachedIP, err)
|
2018-01-30 17:37:35 +01:00
|
|
|
return nil, 0, err
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
2018-02-03 10:25:41 +01:00
|
|
|
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)
|
|
|
|
}
|
2018-03-21 09:05:30 +01:00
|
|
|
dnsClient := new(dns.Client)
|
2018-03-21 10:03:05 +01:00
|
|
|
foundIP, err := xTransport.resolve(dnsClient, host, xTransport.fallbackResolver)
|
2018-03-21 09:05:30 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
2018-02-26 14:50:01 +01:00
|
|
|
if foundIP == nil {
|
2018-01-30 17:37:35 +01:00
|
|
|
return nil, 0, fmt.Errorf("No IP found for [%s]", host)
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
|
|
|
xTransport.cachedIPs.Lock()
|
2018-02-26 14:50:01 +01:00
|
|
|
xTransport.cachedIPs.cache[host] = *foundIP
|
2018-01-30 15:51:07 +01:00
|
|
|
xTransport.cachedIPs.Unlock()
|
2018-03-20 15:10:12 +01:00
|
|
|
dlog.Debugf("[%s] IP address [%s] added to the cache", host, *foundIP)
|
2018-01-30 15:51:07 +01:00
|
|
|
|
2018-01-30 17:37:35 +01:00
|
|
|
start := time.Now()
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
rtt := time.Since(start)
|
2018-01-30 15:51:07 +01:00
|
|
|
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)
|
|
|
|
}
|
2018-02-05 19:03:04 +01:00
|
|
|
} else {
|
|
|
|
(*xTransport.transport).CloseIdleConnections()
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
2018-01-30 17:37:35 +01:00
|
|
|
if err != nil {
|
|
|
|
dlog.Debugf("[%s]: [%s]", req.URL, err)
|
|
|
|
}
|
2018-01-30 15:51:07 +01:00
|
|
|
return resp, rtt, err
|
|
|
|
}
|
|
|
|
|
2018-02-05 11:30:10 +01:00
|
|
|
func (xTransport *XTransport) Get(url *url.URL, accept string, timeout time.Duration) (*http.Response, time.Duration, error) {
|
2018-02-27 09:30:09 +01:00
|
|
|
return xTransport.Fetch("GET", url, "", "", nil, timeout, nil)
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
|
|
|
|
2018-02-27 09:30:09 +01:00
|
|
|
func (xTransport *XTransport) Post(url *url.URL, accept string, contentType string, body []byte, timeout time.Duration, padding *string) (*http.Response, time.Duration, error) {
|
2018-01-30 15:51:07 +01:00
|
|
|
bc := ioutil.NopCloser(bytes.NewReader(body))
|
2018-02-27 09:30:09 +01:00
|
|
|
return xTransport.Fetch("POST", url, accept, contentType, &bc, timeout, padding)
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
2018-02-27 09:30:09 +01:00
|
|
|
|
2018-02-05 11:30:10 +01:00
|
|
|
func (xTransport *XTransport) DoHQuery(useGet bool, url *url.URL, body []byte, timeout time.Duration) (*http.Response, time.Duration, error) {
|
2018-02-27 09:30:09 +01:00
|
|
|
padLen := 63 - (len(body)+63)&63
|
|
|
|
padding := xTransport.makePad(padLen)
|
2018-02-05 11:30:10 +01:00
|
|
|
dataType := "application/dns-udpwireformat"
|
|
|
|
if useGet {
|
|
|
|
qs := url.Query()
|
|
|
|
qs.Add("ct", "")
|
2018-02-05 11:36:15 +01:00
|
|
|
encBody := base64.RawURLEncoding.EncodeToString(body)
|
|
|
|
qs.Add("body", encBody)
|
|
|
|
qs.Add("dns", encBody)
|
2018-02-27 09:30:09 +01:00
|
|
|
qs.Add("random_padding", *padding)
|
2018-02-05 11:30:10 +01:00
|
|
|
url2 := *url
|
|
|
|
url2.RawQuery = qs.Encode()
|
|
|
|
return xTransport.Get(&url2, dataType, timeout)
|
|
|
|
}
|
2018-02-27 09:30:09 +01:00
|
|
|
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
|
2018-02-05 11:30:10 +01:00
|
|
|
}
|