2018-01-30 15:51:07 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2018-11-22 18:09:27 +01:00
|
|
|
"crypto/sha512"
|
2018-04-07 22:23:29 +02:00
|
|
|
"crypto/tls"
|
2018-02-05 11:30:10 +01:00
|
|
|
"encoding/base64"
|
2018-11-22 18:09:27 +01:00
|
|
|
"encoding/hex"
|
2018-01-30 15:51:07 +01:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2018-03-28 12:38:17 +02:00
|
|
|
"strconv"
|
2018-01-30 15:51:07 +01:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
2018-04-18 18:47:10 +02:00
|
|
|
|
2018-01-30 15:51:07 +01:00
|
|
|
"github.com/jedisct1/dlog"
|
2018-04-18 18:58:39 +02:00
|
|
|
stamps "github.com/jedisct1/go-dnsstamps"
|
2018-01-30 15:51:07 +01:00
|
|
|
"github.com/miekg/dns"
|
2018-04-07 22:23:29 +02:00
|
|
|
"golang.org/x/net/http2"
|
2018-06-06 15:54:51 +02:00
|
|
|
netproxy "golang.org/x/net/proxy"
|
2018-01-30 15:51:07 +01:00
|
|
|
)
|
|
|
|
|
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 {
|
2018-04-07 22:23:29 +02:00
|
|
|
transport *http.Transport
|
|
|
|
keepAlive time.Duration
|
|
|
|
timeout time.Duration
|
|
|
|
cachedIPs CachedIPs
|
|
|
|
fallbackResolver string
|
2019-07-10 13:13:28 +02:00
|
|
|
mainProto string
|
2018-04-07 22:23:29 +02:00
|
|
|
ignoreSystemDNS bool
|
|
|
|
useIPv4 bool
|
|
|
|
useIPv6 bool
|
|
|
|
tlsDisableSessionTickets bool
|
|
|
|
tlsCipherSuite []uint16
|
2018-06-06 15:54:51 +02:00
|
|
|
proxyDialer *netproxy.Dialer
|
2018-11-15 18:47:33 +01:00
|
|
|
httpProxyFunction func(*http.Request) (*url.URL, error)
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
|
|
|
|
2018-04-02 01:49:09 +02:00
|
|
|
var DefaultKeepAlive = 5 * time.Second
|
2018-04-07 22:33:11 +02:00
|
|
|
var DefaultTimeout = 30 * time.Second
|
2018-02-05 19:03:04 +01:00
|
|
|
|
2018-04-07 22:33:11 +02:00
|
|
|
func NewXTransport() *XTransport {
|
2018-01-30 15:51:07 +01:00
|
|
|
xTransport := XTransport{
|
2018-04-07 22:23:29 +02:00
|
|
|
cachedIPs: CachedIPs{cache: make(map[string]string)},
|
|
|
|
keepAlive: DefaultKeepAlive,
|
2018-04-07 22:33:11 +02:00
|
|
|
timeout: DefaultTimeout,
|
2018-04-07 22:23:29 +02:00
|
|
|
fallbackResolver: DefaultFallbackResolver,
|
2019-07-10 13:13:28 +02:00
|
|
|
mainProto: "",
|
2018-04-07 22:23:29 +02:00
|
|
|
ignoreSystemDNS: false,
|
2018-04-07 22:33:11 +02:00
|
|
|
useIPv4: true,
|
|
|
|
useIPv6: false,
|
2018-04-07 22:23:29 +02:00
|
|
|
tlsDisableSessionTickets: false,
|
|
|
|
tlsCipherSuite: nil,
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
2018-02-05 19:03:04 +01:00
|
|
|
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
|
|
|
transport := &http.Transport{
|
|
|
|
DisableKeepAlives: false,
|
|
|
|
DisableCompression: true,
|
|
|
|
MaxIdleConns: 1,
|
2018-04-02 01:49:09 +02:00
|
|
|
IdleConnTimeout: xTransport.keepAlive,
|
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-04-14 15:03:21 +02:00
|
|
|
host, port := ExtractHostAndPort(addrStr, stamps.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:38:17 +02:00
|
|
|
addrStr = ipOnly + ":" + strconv.Itoa(port)
|
2018-06-06 15:54:51 +02:00
|
|
|
if xTransport.proxyDialer == nil {
|
|
|
|
dialer := &net.Dialer{Timeout: timeout, KeepAlive: timeout, DualStack: true}
|
|
|
|
return dialer.DialContext(ctx, network, addrStr)
|
|
|
|
}
|
2019-06-26 19:51:57 +02:00
|
|
|
return (*xTransport.proxyDialer).Dial(network, addrStr)
|
2018-01-30 15:51:07 +01:00
|
|
|
},
|
|
|
|
}
|
2018-11-15 18:47:33 +01:00
|
|
|
if xTransport.httpProxyFunction != nil {
|
|
|
|
transport.Proxy = xTransport.httpProxyFunction
|
|
|
|
}
|
2018-04-07 22:23:29 +02:00
|
|
|
if xTransport.tlsDisableSessionTickets || xTransport.tlsCipherSuite != nil {
|
|
|
|
tlsClientConfig := tls.Config{
|
|
|
|
SessionTicketsDisabled: xTransport.tlsDisableSessionTickets,
|
|
|
|
}
|
2018-04-10 00:36:55 +02:00
|
|
|
if !xTransport.tlsDisableSessionTickets {
|
|
|
|
tlsClientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
|
|
|
|
}
|
2018-04-07 22:23:29 +02:00
|
|
|
if xTransport.tlsCipherSuite != nil {
|
|
|
|
tlsClientConfig.PreferServerCipherSuites = false
|
|
|
|
tlsClientConfig.CipherSuites = xTransport.tlsCipherSuite
|
|
|
|
}
|
|
|
|
transport.TLSClientConfig = &tlsClientConfig
|
|
|
|
}
|
|
|
|
http2.ConfigureTransport(transport)
|
2018-01-30 15:51:07 +01:00
|
|
|
xTransport.transport = transport
|
|
|
|
}
|
|
|
|
|
2019-04-14 13:46:07 +02:00
|
|
|
func (xTransport *XTransport) resolveUsingSystem(host string) (*string, error) {
|
|
|
|
foundIPs, err := net.LookupHost(host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for _, ip := range foundIPs {
|
|
|
|
foundIP := net.ParseIP(ip)
|
|
|
|
if foundIP == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if xTransport.useIPv4 {
|
|
|
|
if ipv4 := foundIP.To4(); ipv4 != nil {
|
|
|
|
foundIPx := foundIP.String()
|
|
|
|
return &foundIPx, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if xTransport.useIPv6 {
|
|
|
|
if ipv6 := foundIP.To16(); ipv6 != nil {
|
|
|
|
foundIPx := "[" + foundIP.String() + "]"
|
|
|
|
return &foundIPx, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (xTransport *XTransport) resolveUsingResolver(dnsClient *dns.Client, host string, resolver string) (*string, error) {
|
2018-03-21 10:03:05 +01:00
|
|
|
var err error
|
|
|
|
if xTransport.useIPv4 {
|
|
|
|
msg := new(dns.Msg)
|
|
|
|
msg.SetQuestion(dns.Fqdn(host), dns.TypeA)
|
2019-10-16 11:47:43 +02:00
|
|
|
msg.SetEdns0(uint16(MaxDNSPacketSize), true)
|
2018-03-21 10:03:05 +01:00
|
|
|
var in *dns.Msg
|
|
|
|
in, _, err = dnsClient.Exchange(msg, resolver)
|
|
|
|
if err == nil {
|
|
|
|
for _, answer := range in.Answer {
|
|
|
|
if answer.Header().Rrtype == dns.TypeA {
|
2019-10-03 18:43:27 +02:00
|
|
|
foundIP := answer.(*dns.A).A.String()
|
|
|
|
return &foundIP, nil
|
2018-03-21 10:03:05 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-10-03 18:43:27 +02:00
|
|
|
if xTransport.useIPv6 {
|
2018-03-21 10:03:05 +01:00
|
|
|
msg := new(dns.Msg)
|
|
|
|
msg.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)
|
2019-10-16 11:47:43 +02:00
|
|
|
msg.SetEdns0(uint16(MaxDNSPacketSize), true)
|
2018-03-21 10:03:05 +01:00
|
|
|
var in *dns.Msg
|
|
|
|
in, _, err = dnsClient.Exchange(msg, resolver)
|
|
|
|
if err == nil {
|
|
|
|
for _, answer := range in.Answer {
|
|
|
|
if answer.Header().Rrtype == dns.TypeAAAA {
|
2019-10-03 18:43:27 +02:00
|
|
|
foundIP := "[" + answer.(*dns.AAAA).AAAA.String() + "]"
|
|
|
|
return &foundIP, nil
|
2018-03-21 10:03:05 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-11-22 17:23:22 +01:00
|
|
|
func (xTransport *XTransport) Fetch(method string, 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
|
|
|
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-11-22 18:09:27 +01:00
|
|
|
if body != nil {
|
|
|
|
h := sha512.Sum512(*body)
|
|
|
|
qs := url.Query()
|
|
|
|
qs.Add("body_hash", hex.EncodeToString(h[:32]))
|
|
|
|
url2 := *url
|
|
|
|
url2.RawQuery = qs.Encode()
|
|
|
|
url = &url2
|
|
|
|
}
|
2018-01-30 15:51:07 +01:00
|
|
|
req := &http.Request{
|
|
|
|
Method: method,
|
|
|
|
URL: url,
|
|
|
|
Header: header,
|
|
|
|
Close: false,
|
|
|
|
}
|
|
|
|
if body != nil {
|
2018-11-22 17:23:22 +01:00
|
|
|
req.ContentLength = int64(len(*body))
|
|
|
|
bc := ioutil.NopCloser(bytes.NewReader(*body))
|
|
|
|
req.Body = bc
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
2018-01-30 17:37:35 +01:00
|
|
|
var err error
|
2019-10-11 12:16:04 +02:00
|
|
|
host, _ := ExtractHostAndPort(url.Host, 0)
|
2019-04-14 14:19:12 +02:00
|
|
|
if xTransport.proxyDialer == nil && strings.HasSuffix(host, ".onion") {
|
|
|
|
return nil, 0, errors.New("Onion service is not reachable without Tor")
|
|
|
|
}
|
2019-04-14 11:18:14 +02:00
|
|
|
resolveByProxy := false
|
2019-04-14 13:46:07 +02:00
|
|
|
if xTransport.proxyDialer != nil || xTransport.httpProxyFunction != nil {
|
2019-04-14 11:18:14 +02:00
|
|
|
resolveByProxy = true
|
|
|
|
}
|
2019-04-14 13:46:07 +02:00
|
|
|
var foundIP *string
|
|
|
|
if !resolveByProxy && net.ParseIP(host) == nil {
|
|
|
|
xTransport.cachedIPs.RLock()
|
|
|
|
cachedIP := xTransport.cachedIPs.cache[host]
|
|
|
|
xTransport.cachedIPs.RUnlock()
|
|
|
|
if len(cachedIP) > 0 {
|
|
|
|
foundIP = &cachedIP
|
|
|
|
} else {
|
|
|
|
if !xTransport.ignoreSystemDNS {
|
|
|
|
foundIP, err = xTransport.resolveUsingSystem(host)
|
|
|
|
} else {
|
|
|
|
dlog.Debug("Ignoring system DNS")
|
2018-01-30 17:37:35 +01:00
|
|
|
}
|
2019-04-14 13:46:07 +02:00
|
|
|
if xTransport.ignoreSystemDNS || err != nil {
|
|
|
|
if xTransport.ignoreSystemDNS {
|
|
|
|
dlog.Debugf("Resolving [%s] using fallback resolver [%s]", host, xTransport.fallbackResolver)
|
|
|
|
} else {
|
|
|
|
dlog.Noticef("System DNS configuration not usable yet, exceptionally resolving [%s] using fallback resolver [%s]", host, xTransport.fallbackResolver)
|
|
|
|
}
|
2019-07-10 13:13:28 +02:00
|
|
|
dnsClient := dns.Client{Net: xTransport.mainProto}
|
|
|
|
foundIP, err = xTransport.resolveUsingResolver(&dnsClient, host, xTransport.fallbackResolver)
|
2019-04-14 13:46:07 +02:00
|
|
|
}
|
|
|
|
if foundIP == nil {
|
|
|
|
return nil, 0, fmt.Errorf("No IP found for [%s]", host)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
xTransport.cachedIPs.Lock()
|
|
|
|
xTransport.cachedIPs.cache[host] = *foundIP
|
|
|
|
xTransport.cachedIPs.Unlock()
|
|
|
|
dlog.Debugf("[%s] IP address [%s] added to the cache", host, *foundIP)
|
2018-01-30 17:37:35 +01:00
|
|
|
}
|
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-04-11 11:42:30 +02:00
|
|
|
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()
|
|
|
|
}
|
2018-01-30 17:37:35 +01:00
|
|
|
}
|
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-11-15 08:56:22 +01:00
|
|
|
return xTransport.Fetch("GET", url, accept, "", nil, timeout, nil)
|
2018-01-30 15:51:07 +01:00
|
|
|
}
|
|
|
|
|
2018-11-22 17:23:22 +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) {
|
|
|
|
|
|
|
|
return xTransport.Fetch("POST", url, accept, contentType, body, 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-05-19 02:39:32 +02:00
|
|
|
dataType := "application/dns-message"
|
2018-02-05 11:30:10 +01:00
|
|
|
if useGet {
|
|
|
|
qs := url.Query()
|
|
|
|
qs.Add("ct", "")
|
2018-02-05 11:36:15 +01:00
|
|
|
encBody := base64.RawURLEncoding.EncodeToString(body)
|
|
|
|
qs.Add("dns", encBody)
|
2018-02-05 11:30:10 +01:00
|
|
|
url2 := *url
|
|
|
|
url2.RawQuery = qs.Encode()
|
|
|
|
return xTransport.Get(&url2, dataType, timeout)
|
|
|
|
}
|
2018-11-22 17:23:22 +01:00
|
|
|
return xTransport.Post(url, dataType, dataType, &body, timeout, padding)
|
2018-02-27 09:30:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|