diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go index 329909e6..3f22c11d 100644 --- a/dnscrypt-proxy/config.go +++ b/dnscrypt-proxy/config.go @@ -11,6 +11,7 @@ import ( "os" "path" "path/filepath" + "strconv" "strings" "time" @@ -51,6 +52,8 @@ type Config struct { CacheNegMaxTTL uint32 `toml:"cache_neg_max_ttl"` CacheMinTTL uint32 `toml:"cache_min_ttl"` CacheMaxTTL uint32 `toml:"cache_max_ttl"` + RejectTTL uint32 `toml:"reject_ttl"` + CloakTTL uint32 `toml:"cloak_ttl"` QueryLog QueryLogConfig `toml:"query_log"` NxLog NxLogConfig `toml:"nx_log"` BlockName BlockNameConfig `toml:"blacklist"` @@ -102,6 +105,8 @@ func newConfig() Config { CacheNegMaxTTL: 600, CacheMinTTL: 60, CacheMaxTTL: 86400, + RejectTTL: 600, + CloakTTL: 600, SourceRequireNoLog: true, SourceRequireNoFilter: true, SourceIPv4: true, @@ -272,11 +277,14 @@ func ConfigLoad(proxy *Proxy, svcFlag *string) error { proxy.xTransport = NewXTransport() proxy.xTransport.tlsDisableSessionTickets = config.TLSDisableSessionTickets proxy.xTransport.tlsCipherSuite = config.TLSCipherSuite - proxy.xTransport.fallbackResolver = config.FallbackResolver proxy.xTransport.mainProto = proxy.mainProto if len(config.FallbackResolver) > 0 { + if err := CheckResolver(config.FallbackResolver); err != nil { + dlog.Fatalf("fallback_resolver [%v]", err) + } proxy.xTransport.ignoreSystemDNS = config.IgnoreSystemDNS } + proxy.xTransport.fallbackResolver = config.FallbackResolver proxy.xTransport.useIPv4 = config.SourceIPv4 proxy.xTransport.useIPv6 = config.SourceIPv6 proxy.xTransport.keepAlive = time.Duration(config.KeepAlive) * time.Second @@ -361,6 +369,8 @@ func ConfigLoad(proxy *Proxy, svcFlag *string) error { proxy.cacheMinTTL = config.CacheMinTTL proxy.cacheMaxTTL = config.CacheMaxTTL + proxy.rejectTTL = config.RejectTTL + proxy.cloakTTL = config.CloakTTL proxy.queryMeta = config.QueryMeta @@ -690,3 +700,15 @@ func cdLocal() { } os.Chdir(filepath.Dir(exeFileName)) } + +func CheckResolver(resolver string) error { + host, port := ExtractHostAndPort(resolver, -1) + if ip := ParseIP(host); ip == nil { + return fmt.Errorf("Host does not parse as IP '%s'", resolver) + } else if port == -1 { + return fmt.Errorf("Port missing '%s'", resolver) + } else if _, err := strconv.ParseUint(strconv.Itoa(port), 10, 16); err != nil { + return fmt.Errorf("Port does not parse '%s' [%v]", resolver, err) + } + return nil +} diff --git a/dnscrypt-proxy/example-dnscrypt-proxy.toml b/dnscrypt-proxy/example-dnscrypt-proxy.toml index 4a3add11..41bf1081 100644 --- a/dnscrypt-proxy/example-dnscrypt-proxy.toml +++ b/dnscrypt-proxy/example-dnscrypt-proxy.toml @@ -266,6 +266,12 @@ log_files_max_backups = 1 block_ipv6 = false +## TTL for synthetic responses sent when a request has been blocked (due to +## IPv6 or blacklists). + +reject_ttl = 600 + + ################################################################################## # Route queries for specific domains to a dedicated set of servers # @@ -293,6 +299,9 @@ block_ipv6 = false # cloaking_rules = 'cloaking-rules.txt' +## TTL used when serving entries in cloaking-rules.txt + +# cloak_ttl = 600 ########################### diff --git a/dnscrypt-proxy/plugin_cache.go b/dnscrypt-proxy/plugin_cache.go index 267e433b..73f2e808 100644 --- a/dnscrypt-proxy/plugin_cache.go +++ b/dnscrypt-proxy/plugin_cache.go @@ -65,14 +65,15 @@ func (plugin *PluginCacheResponse) Eval(pluginsState *PluginsState, msg *dns.Msg msg: *msg, } plugin.cachedResponses.Lock() - defer plugin.cachedResponses.Unlock() if plugin.cachedResponses.cache == nil { plugin.cachedResponses.cache, err = lru.NewARC(pluginsState.cacheSize) if err != nil { + plugin.cachedResponses.Unlock() return err } } plugin.cachedResponses.cache.Add(cacheKey, cachedResponse) + plugin.cachedResponses.Unlock() updateTTL(msg, cachedResponse.expiration) return nil diff --git a/dnscrypt-proxy/plugin_cloak.go b/dnscrypt-proxy/plugin_cloak.go index 086bb16f..8d43205e 100644 --- a/dnscrypt-proxy/plugin_cloak.go +++ b/dnscrypt-proxy/plugin_cloak.go @@ -41,7 +41,7 @@ func (plugin *PluginCloak) Init(proxy *Proxy) error { if err != nil { return err } - plugin.ttl = proxy.cacheMinTTL + plugin.ttl = proxy.cloakTTL plugin.patternMatcher = NewPatternPatcher() cloakedNames := make(map[string]*CloakedName) for lineNo, line := range strings.Split(string(bin), "\n") { diff --git a/dnscrypt-proxy/plugins.go b/dnscrypt-proxy/plugins.go index 92e635d1..45e1f344 100644 --- a/dnscrypt-proxy/plugins.go +++ b/dnscrypt-proxy/plugins.go @@ -76,6 +76,7 @@ type PluginsState struct { cacheNegMaxTTL uint32 cacheMinTTL uint32 cacheMaxTTL uint32 + rejectTTL uint32 questionMsg *dns.Msg requestStart time.Time requestEnd time.Time @@ -221,6 +222,7 @@ func NewPluginsState(proxy *Proxy, clientProto string, clientAddr *net.Addr, sta cacheNegMaxTTL: proxy.cacheNegMaxTTL, cacheMinTTL: proxy.cacheMinTTL, cacheMaxTTL: proxy.cacheMaxTTL, + rejectTTL: proxy.rejectTTL, questionMsg: nil, requestStart: start, maxUnencryptedUDPSafePayloadSize: MaxDNSUDPSafePacketSize, @@ -249,7 +251,7 @@ func (pluginsState *PluginsState) ApplyQueryPlugins(pluginsGlobals *PluginsGloba return packet, err } if pluginsState.action == PluginsActionReject { - synth, err := RefusedResponseFromMessage(&msg, pluginsGlobals.refusedCodeInResponses, pluginsGlobals.respondWithIPv4, pluginsGlobals.respondWithIPv6, pluginsState.cacheMinTTL) + synth, err := RefusedResponseFromMessage(&msg, pluginsGlobals.refusedCodeInResponses, pluginsGlobals.respondWithIPv4, pluginsGlobals.respondWithIPv6, pluginsState.rejectTTL) if err != nil { return nil, err } @@ -296,7 +298,7 @@ func (pluginsState *PluginsState) ApplyResponsePlugins(pluginsGlobals *PluginsGl return packet, err } if pluginsState.action == PluginsActionReject { - synth, err := RefusedResponseFromMessage(&msg, pluginsGlobals.refusedCodeInResponses, pluginsGlobals.respondWithIPv4, pluginsGlobals.respondWithIPv6, pluginsState.cacheMinTTL) + synth, err := RefusedResponseFromMessage(&msg, pluginsGlobals.refusedCodeInResponses, pluginsGlobals.respondWithIPv4, pluginsGlobals.respondWithIPv6, pluginsState.rejectTTL) if err != nil { return nil, err } diff --git a/dnscrypt-proxy/proxy.go b/dnscrypt-proxy/proxy.go index 5fd60b84..3013bb09 100644 --- a/dnscrypt-proxy/proxy.go +++ b/dnscrypt-proxy/proxy.go @@ -41,6 +41,8 @@ type Proxy struct { cacheNegMaxTTL uint32 cacheMinTTL uint32 cacheMaxTTL uint32 + rejectTTL uint32 + cloakTTL uint32 queryLogFile string queryLogFormat string queryLogIgnoredQtypes []string diff --git a/dnscrypt-proxy/serversInfo.go b/dnscrypt-proxy/serversInfo.go index eda32757..d38ba49c 100644 --- a/dnscrypt-proxy/serversInfo.go +++ b/dnscrypt-proxy/serversInfo.go @@ -191,9 +191,9 @@ func (serversInfo *ServersInfo) estimatorUpdate() { func (serversInfo *ServersInfo) getOne() *ServerInfo { serversInfo.Lock() - defer serversInfo.Unlock() serversCount := len(serversInfo.inner) if serversCount <= 0 { + serversInfo.Unlock() return nil } if serversInfo.lbEstimator { @@ -211,6 +211,7 @@ func (serversInfo *ServersInfo) getOne() *ServerInfo { candidate = rand.Intn(Min(serversCount, 2)) } serverInfo := serversInfo.inner[candidate] + serversInfo.Unlock() dlog.Debugf("Using candidate [%s] RTT: %d", (*serverInfo).Name, int((*serverInfo).rtt.Value())) return serverInfo @@ -328,9 +329,9 @@ func fetchDNSCryptServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp func fetchDoHServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) { if len(stamp.ServerAddrStr) > 0 { ipOnly, _ := ExtractHostAndPort(stamp.ServerAddrStr, -1) - proxy.xTransport.cachedIPs.Lock() - proxy.xTransport.cachedIPs.cache[stamp.ProviderName] = ipOnly - proxy.xTransport.cachedIPs.Unlock() + if ip := ParseIP(ipOnly); ip != nil { + proxy.xTransport.saveCachedIP(stamp.ProviderName, ip, -1*time.Second) + } } url := &url.URL{ Scheme: "https", diff --git a/dnscrypt-proxy/xtransport.go b/dnscrypt-proxy/xtransport.go index a43428f3..acf2eb11 100644 --- a/dnscrypt-proxy/xtransport.go +++ b/dnscrypt-proxy/xtransport.go @@ -8,8 +8,8 @@ import ( "encoding/base64" "encoding/hex" "errors" - "fmt" "io/ioutil" + "math/rand" "net" "net/http" "net/url" @@ -25,11 +25,21 @@ import ( netproxy "golang.org/x/net/proxy" ) -const DefaultFallbackResolver = "9.9.9.9:53" +const ( + DefaultFallbackResolver = "9.9.9.9:53" + DefaultKeepAlive = 5 * time.Second + DefaultTimeout = 30 * time.Second + SystemResolverTTL = 24 * time.Hour +) + +type CachedIPItem struct { + ip net.IP + expiration *time.Time +} type CachedIPs struct { sync.RWMutex - cache map[string]string + cache map[string]*CachedIPItem } type XTransport struct { @@ -48,12 +58,12 @@ type XTransport struct { httpProxyFunction func(*http.Request) (*url.URL, error) } -var DefaultKeepAlive = 5 * time.Second -var DefaultTimeout = 30 * time.Second - func NewXTransport() *XTransport { + if err := CheckResolver(DefaultFallbackResolver); err != nil { + panic("DefaultFallbackResolver does not parse") + } xTransport := XTransport{ - cachedIPs: CachedIPs{cache: make(map[string]string)}, + cachedIPs: CachedIPs{cache: make(map[string]*CachedIPItem)}, keepAlive: DefaultKeepAlive, timeout: DefaultTimeout, fallbackResolver: DefaultFallbackResolver, @@ -67,11 +77,41 @@ func NewXTransport() *XTransport { return &xTransport } -func (xTransport *XTransport) clearCache() { +func ParseIP(ipStr string) net.IP { + return net.ParseIP(strings.TrimRight(strings.TrimLeft(ipStr, "["), "]")) +} + +// If ttl < 0, never expire +// Otherwise, ttl is set to max(ttl, xTransport.timeout) +func (xTransport *XTransport) saveCachedIP(host string, ip net.IP, ttl time.Duration) { + item := &CachedIPItem{ip: ip, expiration: nil} + if ttl >= 0 { + if ttl < xTransport.timeout { + ttl = xTransport.timeout + } + expiration := time.Now().Add(ttl) + item.expiration = &expiration + } xTransport.cachedIPs.Lock() - xTransport.cachedIPs.cache = make(map[string]string) + xTransport.cachedIPs.cache[host] = item xTransport.cachedIPs.Unlock() - dlog.Info("IP cache cleared") +} + +func (xTransport *XTransport) loadCachedIP(host string, deleteIfExpired bool) (net.IP, bool) { + xTransport.cachedIPs.RLock() + item, ok := xTransport.cachedIPs.cache[host] + xTransport.cachedIPs.RUnlock() + if !ok { + return nil, false + } + expiration := item.expiration + if deleteIfExpired && expiration != nil && time.Until(*expiration) < 0 { + xTransport.cachedIPs.Lock() + delete(xTransport.cachedIPs.cache, host) + xTransport.cachedIPs.Unlock() + return nil, false + } + return item.ip, ok } func (xTransport *XTransport) rebuildTransport() { @@ -91,11 +131,9 @@ func (xTransport *XTransport) rebuildTransport() { DialContext: func(ctx context.Context, network, addrStr string) (net.Conn, error) { host, port := ExtractHostAndPort(addrStr, stamps.DefaultPort) ipOnly := host - xTransport.cachedIPs.RLock() - cachedIP := xTransport.cachedIPs.cache[host] - xTransport.cachedIPs.RUnlock() - if len(cachedIP) > 0 { - ipOnly = cachedIP + cachedIP, ok := xTransport.loadCachedIP(host, false) + if ok { + ipOnly = cachedIP.String() } else { dlog.Debugf("[%s] IP address was not cached", host) } @@ -127,47 +165,54 @@ func (xTransport *XTransport) rebuildTransport() { xTransport.transport = transport } -func (xTransport *XTransport) resolveUsingSystem(host string) (*string, error) { - foundIPs, err := net.LookupHost(host) +func (xTransport *XTransport) resolveUsingSystem(host string) (ip net.IP, ttl time.Duration, err error) { + ttl = SystemResolverTTL + var foundIPs []string + foundIPs, err = net.LookupHost(host) if err != nil { - return nil, err + return } + ips := make([]net.IP, 0) 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 foundIP := net.ParseIP(ip); foundIP != nil { + if xTransport.useIPv4 { + if ipv4 := foundIP.To4(); ipv4 != nil { + ips = append(ips, foundIP) + } } - } - if xTransport.useIPv6 { - if ipv6 := foundIP.To16(); ipv6 != nil { - foundIPx := "[" + foundIP.String() + "]" - return &foundIPx, nil + if xTransport.useIPv6 { + if ipv6 := foundIP.To16(); ipv6 != nil { + ips = append(ips, foundIP) + } } } } - return nil, err + if len(ips) > 0 { + ip = ips[rand.Intn(len(ips))] + } + return } -func (xTransport *XTransport) resolveUsingResolver(dnsClient *dns.Client, host string, resolver string) (*string, error) { - var err error +func (xTransport *XTransport) resolveUsingResolver(proto, host string, resolver string) (ip net.IP, ttl time.Duration, err error) { + dnsClient := dns.Client{Net: proto} if xTransport.useIPv4 { msg := new(dns.Msg) msg.SetQuestion(dns.Fqdn(host), dns.TypeA) msg.SetEdns0(uint16(MaxDNSPacketSize), true) var in *dns.Msg - in, _, err = dnsClient.Exchange(msg, resolver) - if err == nil { + if in, _, err = dnsClient.Exchange(msg, resolver); err == nil { + answers := make([]dns.RR, 0) for _, answer := range in.Answer { if answer.Header().Rrtype == dns.TypeA { - foundIP := answer.(*dns.A).A.String() - return &foundIP, nil + answers = append(answers, answer) } } + if len(answers) > 0 { + answer := answers[rand.Intn(len(answers))] + ip = answer.(*dns.A).A + ttl = time.Duration(answer.Header().Ttl) * time.Second + return + } } } if xTransport.useIPv6 { @@ -175,17 +220,62 @@ func (xTransport *XTransport) resolveUsingResolver(dnsClient *dns.Client, host s msg.SetQuestion(dns.Fqdn(host), dns.TypeAAAA) msg.SetEdns0(uint16(MaxDNSPacketSize), true) var in *dns.Msg - in, _, err = dnsClient.Exchange(msg, resolver) - if err == nil { + if in, _, err = dnsClient.Exchange(msg, resolver); err == nil { + answers := make([]dns.RR, 0) for _, answer := range in.Answer { if answer.Header().Rrtype == dns.TypeAAAA { - foundIP := "[" + answer.(*dns.AAAA).AAAA.String() + "]" - return &foundIP, nil + answers = append(answers, answer) } } + if len(answers) > 0 { + answer := answers[rand.Intn(len(answers))] + ip = answer.(*dns.AAAA).AAAA + ttl = time.Duration(answer.Header().Ttl) * time.Second + return + } } } - return nil, err + return +} + +func (xTransport *XTransport) resolveHost(host string) (err error) { + if xTransport.proxyDialer != nil || xTransport.httpProxyFunction != nil { + return + } + if ParseIP(host) != nil { + return + } + if _, ok := xTransport.loadCachedIP(host, true); ok { + return + } + var foundIP net.IP + var ttl time.Duration + if !xTransport.ignoreSystemDNS { + foundIP, ttl, err = xTransport.resolveUsingSystem(host) + } + if xTransport.ignoreSystemDNS || err != nil { + protos := []string{"udp", "tcp"} + if xTransport.mainProto == "tcp" { + protos = []string{"tcp", "udp"} + } + for _, proto := range protos { + if err != nil { + dlog.Noticef("System DNS configuration not usable yet, exceptionally resolving [%s] using resolver %s[%s]", host, proto, xTransport.fallbackResolver) + } else { + dlog.Debugf("Resolving [%s] using resolver %s[%s]", host, proto, xTransport.fallbackResolver) + } + foundIP, ttl, err = xTransport.resolveUsingResolver(proto, host, xTransport.fallbackResolver) + if err == nil { + break + } + } + } + if err != nil { + return + } + xTransport.saveCachedIP(host, foundIP, ttl) + dlog.Debugf("[%s] IP address [%s] added to the cache, valid until %v", host, foundIP, ttl) + return } 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) { @@ -215,6 +305,9 @@ func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string, if xTransport.proxyDialer == nil && strings.HasSuffix(host, ".onion") { return nil, 0, errors.New("Onion service is not reachable without Tor") } + if err := xTransport.resolveHost(host); err != nil { + return nil, 0, err + } req := &http.Request{ Method: method, URL: url, @@ -225,45 +318,6 @@ func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string, req.ContentLength = int64(len(*body)) req.Body = ioutil.NopCloser(bytes.NewReader(*body)) } - var err error - resolveByProxy := false - if xTransport.proxyDialer != nil || xTransport.httpProxyFunction != nil { - resolveByProxy = true - } - 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") - } - 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) - } - dnsClient := dns.Client{Net: xTransport.mainProto} - foundIP, err = xTransport.resolveUsingResolver(&dnsClient, host, xTransport.fallbackResolver) - } - 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) - } - } start := time.Now() resp, err := client.Do(req) rtt := time.Since(start)