From 14a8364faba89f65dd01ca33b5e56fc125e3cdc9 Mon Sep 17 00:00:00 2001 From: NyaMisty Date: Wed, 19 Feb 2020 21:07:03 +0800 Subject: [PATCH] Refactored the RemoteClient to support connection pooling & drop none answer (#206) * Refactored the RemoteClient to support connection pooling Part of RemoteClient were split into several Resolver, which will be shared across all RemoteClient and RemoteClientBundle, in the resolver the pool was implemented. * Fix dispatcher_test.go * Remove too verbose debugging log output * Add option IdleTimeout * Wait until the answer presents in response (Fixes #181) * Revert accidental change & better code format * Support PoolMaxCapacity config * Fix timeout setting * Add tests for Resolvers --- README.md | 4 + config.sample.json | 2 + config.test.json | 2 + core/config/config.go | 2 + core/init.go | 3 + core/outbound/clients/remote.go | 169 +----------------- core/outbound/clients/remote_bundle.go | 18 +- .../clients/{ => resolver}/address.go | 10 +- .../clients/resolver/base_resolver.go | 130 ++++++++++++++ .../clients/resolver/https_resolver.go | 45 +++++ .../clients/resolver/resolver_test.go | 121 +++++++++++++ .../outbound/clients/resolver/tcp_resolver.go | 35 ++++ .../clients/resolver/tcptls_resolver.go | 52 ++++++ .../outbound/clients/resolver/udp_resolver.go | 25 +++ core/outbound/dispatcher.go | 28 ++- core/outbound/dispatcher_test.go | 3 + debian/config.json | 2 + go.mod | 1 + go.sum | 3 + 19 files changed, 483 insertions(+), 172 deletions(-) rename core/outbound/clients/{ => resolver}/address.go (95%) create mode 100644 core/outbound/clients/resolver/base_resolver.go create mode 100644 core/outbound/clients/resolver/https_resolver.go create mode 100644 core/outbound/clients/resolver/resolver_test.go create mode 100644 core/outbound/clients/resolver/tcp_resolver.go create mode 100644 core/outbound/clients/resolver/tcptls_resolver.go create mode 100644 core/outbound/clients/resolver/udp_resolver.go diff --git a/README.md b/README.md index 0a0e519..9d31ec9 100755 --- a/README.md +++ b/README.md @@ -113,6 +113,8 @@ Configuration file is "config.json" by default: "OnlyPrimaryDNS": false, "IPv6UseAlternativeDNS": false, "AlternativeDNSConcurrent": false, + "PoolIdleTimeout": 15, + "PoolMaxCapacity": 15, "WhenPrimaryDNSAnswerNoneUse": "PrimaryDNS", "IPNetworkFile": { "Primary": "./ip_network_primary_sample", @@ -206,6 +208,8 @@ IPv6). Overture will handle both TCP and UDP requests. Literal IPv6 addresses ar + OnlyPrimaryDNS: Disable dispatcher feature, use primary DNS only. + IPv6UseAlternativeDNS: Redirect IPv6 DNS queries to alternative DNS servers. + AlternativeDNSConcurrent: Query the PrimaryDNS and AlternativeDNS at the same time ++ PoolIdleTimeout: Specify idle timeout for connection in pool ++ PoolMaxCapacity: Specify max capacity for connection pool + WhenPrimaryDNSAnswerNoneUse: If the response of PrimaryDNS exists and there is no `ANSWER SECTION` in it, the final DNS should be defined. (There is no `AAAA` record for most domains right now) + File: Absolute path like `/path/to/file` is allowed. For Windows users, please use properly escaped path like `C:\\path\\to\\file.txt` in the configuration. diff --git a/config.sample.json b/config.sample.json index ff05614..1037690 100644 --- a/config.sample.json +++ b/config.sample.json @@ -32,6 +32,8 @@ "OnlyPrimaryDNS": false, "IPv6UseAlternativeDNS": false, "AlternativeDNSConcurrent": false, + "PoolIdleTimeout": 15, + "PoolMaxCapacity": 15, "WhenPrimaryDNSAnswerNoneUse": "PrimaryDNS", "IPNetworkFile": { "Primary": "./ip_network_primary_sample", diff --git a/config.test.json b/config.test.json index 78e9405..1200c53 100644 --- a/config.test.json +++ b/config.test.json @@ -32,6 +32,8 @@ "OnlyPrimaryDNS": false, "IPv6UseAlternativeDNS": false, "AlternativeDNSConcurrent": false, + "PoolIdleTimeout": 15, + "PoolMaxCapacity": 15, "WhenPrimaryDNSAnswerNoneUse": "PrimaryDNS", "IPNetworkFile": { "Primary": "./ip_network_primary_sample", diff --git a/core/config/config.go b/core/config/config.go index 03a6591..b795544 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -38,6 +38,8 @@ type Config struct { OnlyPrimaryDNS bool IPv6UseAlternativeDNS bool AlternativeDNSConcurrent bool + PoolIdleTimeout int + PoolMaxCapacity int IPNetworkFile struct { Primary string Alternative string diff --git a/core/init.go b/core/init.go index b35223f..b839c0a 100644 --- a/core/init.go +++ b/core/init.go @@ -28,12 +28,15 @@ func InitServer(configFilePath string) { RedirectIPv6Record: conf.IPv6UseAlternativeDNS, AlternativeDNSConcurrent: conf.AlternativeDNSConcurrent, + PoolIdleTimeout: conf.PoolIdleTimeout, + PoolMaxCapacity: conf.PoolMaxCapacity, MinimumTTL: conf.MinimumTTL, DomainTTLMap: conf.DomainTTLMap, Hosts: conf.Hosts, Cache: conf.Cache, } + dispatcher.Init() s := inbound.NewServer(conf.BindAddress, conf.DebugHTTPAddress, dispatcher, conf.RejectQType) diff --git a/core/outbound/clients/remote.go b/core/outbound/clients/remote.go index 0776e0b..358fb58 100644 --- a/core/outbound/clients/remote.go +++ b/core/outbound/clients/remote.go @@ -8,19 +8,13 @@ package clients import ( - "bytes" - "crypto/tls" - "io/ioutil" - "net" - "net/http" - "time" - "github.com/miekg/dns" log "github.com/sirupsen/logrus" - "golang.org/x/net/proxy" + "net" "github.com/shawn1m/overture/core/cache" "github.com/shawn1m/overture/core/common" + "github.com/shawn1m/overture/core/outbound/clients/resolver" ) type RemoteClient struct { @@ -30,12 +24,13 @@ type RemoteClient struct { dnsUpstream *common.DNSUpstream ednsClientSubnetIP string inboundIP string + dnsResolver resolver.Resolver cache *cache.Cache } -func NewClient(q *dns.Msg, u *common.DNSUpstream, ip string, cache *cache.Cache) *RemoteClient { - c := &RemoteClient{questionMessage: q.Copy(), dnsUpstream: u, inboundIP: ip, cache: cache} +func NewClient(q *dns.Msg, u *common.DNSUpstream, resolver resolver.Resolver, ip string, cache *cache.Cache) *RemoteClient { + c := &RemoteClient{questionMessage: q.Copy(), dnsUpstream: u, dnsResolver: resolver, inboundIP: ip, cache: cache} c.getEDNSClientSubnetIP() return c @@ -77,25 +72,9 @@ func (c *RemoteClient) Exchange(isLog bool) *dns.Msg { return c.responseMessage } - var conn net.Conn = nil - var err error - if c.dnsUpstream.SOCKS5Address != "" { - if conn, err = c.createSocks5Conn(); err != nil { - return nil - } - } - var temp *dns.Msg - switch c.dnsUpstream.Protocol { - case "udp": - temp, err = c.ExchangeByUDP(conn) - case "tcp": - temp, err = c.ExchangeByTCP(conn) - case "tcp-tls": - temp, err = c.ExchangeByTLS(conn) - case "https": - temp, err = c.ExchangeByHTTPS(conn) - } + var err error + temp, err = c.dnsResolver.Exchange(c.questionMessage) if err != nil { log.Debugf("%s Fail: %s", c.dnsUpstream.Name, err) @@ -127,137 +106,3 @@ func (c *RemoteClient) logAnswer(indicator string) { log.Debugf("Answer from %s: %s", name, a.String()) } } - -func (c *RemoteClient) createSocks5Conn() (conn net.Conn, err error) { - socksAddress, err := ExtractSocksAddress(c.dnsUpstream.SOCKS5Address) - if err != nil { - return nil, err - } - network := ToNetwork(c.dnsUpstream.Protocol) - s, err := proxy.SOCKS5(network, socksAddress, nil, proxy.Direct) - if err != nil { - log.Warnf("Failed to connect to SOCKS5 proxy: %s", err) - return nil, err - } - host, port, err := ExtractDNSAddress(c.dnsUpstream.Address, c.dnsUpstream.Protocol) - if err != nil { - return nil, err - } - address := net.JoinHostPort(host, port) - conn, err = s.Dial(network, address) - if err != nil { - log.Warnf("Failed to connect to upstream via SOCKS5 proxy: %s", err) - return nil, err - } - return conn, err -} - -func (c *RemoteClient) exchangeByDNSClient(conn net.Conn) (msg *dns.Msg, err error) { - if conn == nil { - network := ToNetwork(c.dnsUpstream.Protocol) - host, port, err := ExtractDNSAddress(c.dnsUpstream.Address, c.dnsUpstream.Protocol) - if err != nil { - return nil, err - } - address := net.JoinHostPort(host, port) - if conn, err = net.Dial(network, address); err != nil { - log.Warnf("Failed to connect to DNS upstream: %s", err) - return nil, err - } - } - c.setTimeout(conn) - dc := &dns.Conn{Conn: conn, UDPSize: 65535} - defer dc.Close() - err = dc.WriteMsg(c.questionMessage) - if err != nil { - log.Warnf("%s Fail: Send question message failed", c.dnsUpstream.Name) - return nil, err - } - return dc.ReadMsg() -} - -// ExchangeByUDP send dns record by udp protocol -func (c *RemoteClient) ExchangeByUDP(conn net.Conn) (*dns.Msg, error) { - return c.exchangeByDNSClient(conn) -} - -// ExchangeByTCP send dns record by tcp protocol -func (c *RemoteClient) ExchangeByTCP(conn net.Conn) (*dns.Msg, error) { - return c.exchangeByDNSClient(conn) -} - -// ExchangeByTLS send dns record by tcp-tls protocol -func (c *RemoteClient) ExchangeByTLS(conn net.Conn) (msg *dns.Msg, err error) { - host, port, ip := ExtractTLSDNSAddress(c.dnsUpstream.Address) - var address string - if len(ip) > 0 { - address = net.JoinHostPort(ip, port) - } else { - address = net.JoinHostPort(host, port) - } - - conf := &tls.Config{ - InsecureSkipVerify: false, - ServerName: host, - } - if conn != nil { - // crate tls client use the existing connection - conn = tls.Client(conn, conf) - } else { - if conn, err = tls.Dial("tcp", address, conf); err != nil { - log.Warnf("Failed to connect to DNS-over-TLS upstream: %s", err) - return nil, err - } - } - c.setTimeout(conn) - return c.exchangeByDNSClient(conn) -} - -// ExchangeByHTTPS send dns record by https protocol -func (c *RemoteClient) ExchangeByHTTPS(conn net.Conn) (*dns.Msg, error) { - if conn == nil { - host, port, err := ExtractHTTPSAddress(c.dnsUpstream.Address) - if err != nil { - return nil, err - } - address := net.JoinHostPort(host, port) - conn, err = net.Dial("tcp", address) - if err != nil { - log.Warnf("Fail connect to dns server %s", address) - return nil, err - } - } - c.setTimeout(conn) - client := http.Client{ - Transport: &http.Transport{ - Dial: func(network, addr string) (net.Conn, error) { - return conn, nil - }, - }, - } - defer client.CloseIdleConnections() - request, err := c.questionMessage.Pack() - resp, err := client.Post(c.dnsUpstream.Address, "application/dns-message", - bytes.NewBuffer(request)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - msg := new(dns.Msg) - err = msg.Unpack(data) - if err != nil { - return nil, err - } - return msg, nil -} - -func (c *RemoteClient) setTimeout(conn net.Conn) { - dnsTimeout := time.Duration(c.dnsUpstream.Timeout) * time.Second / 3 - conn.SetDeadline(time.Now().Add(dnsTimeout)) - conn.SetReadDeadline(time.Now().Add(dnsTimeout)) - conn.SetWriteDeadline(time.Now().Add(dnsTimeout)) -} diff --git a/core/outbound/clients/remote_bundle.go b/core/outbound/clients/remote_bundle.go index 8e6903a..31a6a3c 100644 --- a/core/outbound/clients/remote_bundle.go +++ b/core/outbound/clients/remote_bundle.go @@ -8,6 +8,8 @@ package clients import ( "github.com/miekg/dns" + "github.com/shawn1m/overture/core/outbound/clients/resolver" + log "github.com/sirupsen/logrus" "github.com/shawn1m/overture/core/cache" "github.com/shawn1m/overture/core/common" @@ -26,14 +28,15 @@ type RemoteClientBundle struct { cache *cache.Cache Name string -} -func NewClientBundle(q *dns.Msg, ul []*common.DNSUpstream, ip string, minimumTTL int, cache *cache.Cache, name string, domainTTLMap map[string]uint32) *RemoteClientBundle { - cb := &RemoteClientBundle{questionMessage: q.Copy(), dnsUpstreams: ul, inboundIP: ip, minimumTTL: minimumTTL, cache: cache, Name: name, domainTTLMap: domainTTLMap} + dnsResolvers []resolver.Resolver +} - for _, u := range ul { +func NewClientBundle(q *dns.Msg, ul []*common.DNSUpstream, resolvers []resolver.Resolver, ip string, minimumTTL int, cache *cache.Cache, name string, domainTTLMap map[string]uint32) *RemoteClientBundle { + cb := &RemoteClientBundle{questionMessage: q.Copy(), dnsUpstreams: ul, dnsResolvers: resolvers, inboundIP: ip, minimumTTL: minimumTTL, cache: cache, Name: name, domainTTLMap: domainTTLMap} - c := NewClient(cb.questionMessage, u, cb.inboundIP, cb.cache) + for i, u := range ul { + c := NewClient(cb.questionMessage, u, cb.dnsResolvers[i], cb.inboundIP, cb.cache) cb.clients = append(cb.clients, c) } @@ -56,7 +59,10 @@ func (cb *RemoteClientBundle) Exchange(isCache bool, isLog bool) *dns.Msg { c := <-ch if c != nil { ec = c - break + if ec.responseMessage != nil && ec.responseMessage.Answer != nil { + break + } + log.Debugf("DNSUpstream %s returned None answer, dropping it and wait the next one", ec.dnsUpstream.Address) } } diff --git a/core/outbound/clients/address.go b/core/outbound/clients/resolver/address.go similarity index 95% rename from core/outbound/clients/address.go rename to core/outbound/clients/resolver/address.go index 2054bff..e6b855d 100644 --- a/core/outbound/clients/address.go +++ b/core/outbound/clients/resolver/address.go @@ -5,7 +5,7 @@ */ // Package outbound implements multiple dns client and dispatcher for outbound connection. -package clients +package resolver import ( "errors" @@ -118,7 +118,13 @@ func ExtractDNSAddress(rawAddress string, protocol string) (host string, port st case "https": host, port, err = ExtractHTTPSAddress(rawAddress) case "tcp-tls": - _, port, host = ExtractTLSDNSAddress(rawAddress) + _host, _port, _ip := ExtractTLSDNSAddress(rawAddress) + if len(_ip) > 0 { + host = _ip + } else { + host = _host + } + port = _port default: host, port, err = ExtractNormalDNSAddress(rawAddress, protocol) } diff --git a/core/outbound/clients/resolver/base_resolver.go b/core/outbound/clients/resolver/base_resolver.go new file mode 100644 index 0000000..49b0e24 --- /dev/null +++ b/core/outbound/clients/resolver/base_resolver.go @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2019 shawn1m. All rights reserved. + * Use of this source code is governed by The MIT License (MIT) that can be + * found in the LICENSE file.. + */ +package resolver + +import ( + "github.com/miekg/dns" + "github.com/silenceper/pool" + log "github.com/sirupsen/logrus" + "golang.org/x/net/proxy" + "net" + "time" + + "github.com/shawn1m/overture/core/common" +) + +type Resolver interface { + Exchange(*dns.Msg) (*dns.Msg, error) + Init() +} + +type BaseResolver struct { + dnsUpstream *common.DNSUpstream +} + +func (r *BaseResolver) Exchange(*dns.Msg) (*dns.Msg, error) { + return nil, nil +} + +func (r *BaseResolver) Init() { + +} + +func NewResolver(u *common.DNSUpstream) Resolver { + var resolver Resolver + switch u.Protocol { + case "udp": + resolver = &UDPResolver{BaseResolver: BaseResolver{u}} + case "tcp": + resolver = &TCPResolver{BaseResolver: BaseResolver{u}} + case "tcp-tls": + resolver = &TCPTLSResolver{BaseResolver: BaseResolver{u}} + case "https": + resolver = &HTTPSResolver{BaseResolver: BaseResolver{u}} + default: + log.Fatalf("Unsupported protocol: %s", u.Protocol) + return nil + } + resolver.Init() + return resolver +} + +func (c *BaseResolver) CreateBaseConn() (conn net.Conn, err error) { + dialer := net.Dial + if c.dnsUpstream.SOCKS5Address != "" { + socksAddress, err := ExtractSocksAddress(c.dnsUpstream.SOCKS5Address) + if err != nil { + return nil, err + } + network := ToNetwork(c.dnsUpstream.Protocol) + s, err := proxy.SOCKS5(network, socksAddress, nil, proxy.Direct) + if err != nil { + log.Warnf("Failed to connect to SOCKS5 proxy: %s", err) + return nil, err + } + dialer = s.Dial + } + + network := ToNetwork(c.dnsUpstream.Protocol) + host, port, err := ExtractDNSAddress(c.dnsUpstream.Address, c.dnsUpstream.Protocol) + if err != nil { + return nil, err + } + address := net.JoinHostPort(host, port) + log.Debugf("Creating new connection to %s:%s", host, port) + if conn, err = dialer(network, address); err != nil { + log.Warnf("Failed to connect to DNS upstream: %s", err) + return nil, err + } + + // the Timeout setting is now moved to each resolver to support pool's idle timeout + // c.setTimeout(conn) + return conn, err +} + +var IdleTimeout time.Duration = 30 * time.Second +var PoolMaxCapacity int = 15 + +func (c *BaseResolver) setTimeout(conn net.Conn) { + dnsTimeout := time.Duration(c.dnsUpstream.Timeout) * time.Second / 3 + conn.SetDeadline(time.Now().Add(dnsTimeout)) + conn.SetReadDeadline(time.Now().Add(dnsTimeout)) + conn.SetWriteDeadline(time.Now().Add(dnsTimeout)) +} + +func (c *BaseResolver) setIdleTimeout(conn net.Conn) { + conn.SetDeadline(time.Now().Add(IdleTimeout)) + conn.SetReadDeadline(time.Now().Add(IdleTimeout)) + conn.SetWriteDeadline(time.Now().Add(IdleTimeout)) +} + +func (c *BaseResolver) createConnectionPool(connCreate func() (interface{}, error), connClose func(interface{}) error) pool.Pool { + poolConfig := &pool.Config{ + InitialCap: 0, + MaxCap: PoolMaxCapacity, + Factory: connCreate, + Close: connClose, + //Ping: ping, + IdleTimeout: IdleTimeout, + } + ret, _ := pool.NewChannelPool(poolConfig) + return ret +} + +func (c *BaseResolver) exchangeByDNSClient(q *dns.Msg, conn net.Conn) (msg *dns.Msg, err error) { + if conn == nil { + log.Fatal("Conn not initialized for exchangeByDNSClient") + return nil, err + } + + dc := &dns.Conn{Conn: conn, UDPSize: 65535} + err = dc.WriteMsg(q) + if err != nil { + log.Warnf("%s Fail: Send question message failed", c.dnsUpstream.Name) + return nil, err + } + return dc.ReadMsg() +} diff --git a/core/outbound/clients/resolver/https_resolver.go b/core/outbound/clients/resolver/https_resolver.go new file mode 100644 index 0000000..b1bbb9f --- /dev/null +++ b/core/outbound/clients/resolver/https_resolver.go @@ -0,0 +1,45 @@ +package resolver + +import ( + "bytes" + "github.com/miekg/dns" + "io/ioutil" + "net" + "net/http" +) + +type HTTPSResolver struct { + BaseResolver + + client http.Client +} + +func (r *HTTPSResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { + request, err := q.Pack() + resp, err := r.client.Post(r.dnsUpstream.Address, "application/dns-message", + bytes.NewBuffer(request)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + msg := new(dns.Msg) + err = msg.Unpack(data) + if err != nil { + return nil, err + } + return msg, nil +} + +func (r *HTTPSResolver) Init() { + r.client = http.Client{ + Transport: &http.Transport{ + Dial: func(network, addr string) (net.Conn, error) { + return r.CreateBaseConn() + }, + }, + } +} diff --git a/core/outbound/clients/resolver/resolver_test.go b/core/outbound/clients/resolver/resolver_test.go new file mode 100644 index 0000000..70a2aac --- /dev/null +++ b/core/outbound/clients/resolver/resolver_test.go @@ -0,0 +1,121 @@ +package resolver + +import ( + "github.com/miekg/dns" + "github.com/shawn1m/overture/core/common" + "net" + "os" + "testing" +) + +var questionDomain = "www.yahoo.com." +var udpUpstream = &common.DNSUpstream{ + Name: "Test-UDP", + Address: "8.8.8.8", + Protocol: "udp", + SOCKS5Address: "", + Timeout: 6, + EDNSClientSubnet: &common.EDNSClientSubnetType{ + Policy: "disable", + ExternalIP: "", + NoCookie: false, + }, +} + +var tcpUpstream = &common.DNSUpstream{ + Name: "Test-TCP", + Address: "8.8.8.8", + Protocol: "tcp", + SOCKS5Address: "", + Timeout: 6, + EDNSClientSubnet: &common.EDNSClientSubnetType{ + Policy: "disable", + ExternalIP: "", + NoCookie: false, + }, +} + +var tcptlsUpstream = &common.DNSUpstream{ + Name: "Test-TCPTLS", + Address: "dns.google:853@8.8.8.8", + Protocol: "tcp-tls", + SOCKS5Address: "", + Timeout: 8, + EDNSClientSubnet: &common.EDNSClientSubnetType{ + Policy: "disable", + ExternalIP: "", + NoCookie: false, + }, +} + +var httpsUpstream = &common.DNSUpstream{ + Name: "Test-HTTPS", + Address: "https://dns.google/dns-query", + Protocol: "https", + SOCKS5Address: "", + Timeout: 8, + EDNSClientSubnet: &common.EDNSClientSubnetType{ + Policy: "disable", + ExternalIP: "", + NoCookie: false, + }, +} + +func init() { + os.Chdir("../..") + //conf := config.NewConfig("config.test.json") +} + +func TestDispatcher(t *testing.T) { + testUDP(t) + testTCP(t) + testTCPTLS(t) + testHTTPS(t) +} + +func testUDP(t *testing.T) { + q := getQueryMsg(questionDomain, dns.TypeA) + resolver := NewResolver(udpUpstream) + resp, err := resolver.Exchange(q) + if err != nil { + t.Errorf("Got error: %s", err) + } + if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { + t.Error(questionDomain + " should have A record") + } +} + +func testTCP(t *testing.T) { + q := getQueryMsg(questionDomain, dns.TypeA) + resolver := NewResolver(udpUpstream) + resp, _ := resolver.Exchange(q) + if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { + t.Error(questionDomain + " should have A record") + } +} + + +func testTCPTLS(t *testing.T) { + q := getQueryMsg(questionDomain, dns.TypeA) + resolver := NewResolver(udpUpstream) + resp, _ := resolver.Exchange(q) + if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { + t.Error(questionDomain + " should have A record") + } +} + + +func testHTTPS(t *testing.T) { + q := getQueryMsg(questionDomain, dns.TypeA) + resolver := NewResolver(udpUpstream) + resp, _ := resolver.Exchange(q) + if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { + t.Error(questionDomain + " should have A record") + } +} + +func getQueryMsg(z string, t uint16) *dns.Msg { + q := new(dns.Msg) + q.SetQuestion(z, t) + return q +} diff --git a/core/outbound/clients/resolver/tcp_resolver.go b/core/outbound/clients/resolver/tcp_resolver.go new file mode 100644 index 0000000..8a4e126 --- /dev/null +++ b/core/outbound/clients/resolver/tcp_resolver.go @@ -0,0 +1,35 @@ +package resolver + +import ( + "github.com/miekg/dns" + "github.com/silenceper/pool" + "net" +) + +type TCPResolver struct { + BaseResolver + connpool pool.Pool +} + +func (r *TCPResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { + _conn, err := r.connpool.Get() + if err != nil { + return nil, err + } + conn := _conn.(net.Conn) + r.setTimeout(conn) + ret, err := r.exchangeByDNSClient(q, conn) + if err != nil { + r.connpool.Close(conn) + } else { + r.setIdleTimeout(conn) + r.connpool.Put(conn) + } + return ret, err +} + +func (r *TCPResolver) Init() { + r.connpool = r.createConnectionPool( + func() (interface{}, error) { return r.CreateBaseConn() }, + func(v interface{}) error { return v.(net.Conn).Close() }) +} diff --git a/core/outbound/clients/resolver/tcptls_resolver.go b/core/outbound/clients/resolver/tcptls_resolver.go new file mode 100644 index 0000000..c2bfd2f --- /dev/null +++ b/core/outbound/clients/resolver/tcptls_resolver.go @@ -0,0 +1,52 @@ +package resolver + +import ( + "crypto/tls" + "github.com/miekg/dns" + "github.com/silenceper/pool" + "net" +) + +type TCPTLSResolver struct { + BaseResolver + + connpool pool.Pool +} + +func (r *TCPTLSResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { + _conn, err := r.connpool.Get() + if err != nil { + return nil, err + } + conn := _conn.(net.Conn) + r.setTimeout(conn) + ret, err := r.exchangeByDNSClient(q, conn) + if err != nil { + r.connpool.Close(conn) + } else { + r.setIdleTimeout(conn) + r.connpool.Put(conn) + } + return ret, err +} + +func (r *TCPTLSResolver) createTlsConn() (conn net.Conn, err error) { + conn, err = r.CreateBaseConn() + if conn == nil { + return nil, err + } + host, _, _ := ExtractTLSDNSAddress(r.dnsUpstream.Address) + conf := &tls.Config{ + InsecureSkipVerify: false, + ServerName: host, + } + conn = tls.Client(conn, conf) + + return conn, nil +} + +func (r *TCPTLSResolver) Init() { + r.connpool = r.createConnectionPool( + func() (interface{}, error) { return r.createTlsConn() }, + func(v interface{}) error { return v.(net.Conn).Close() }) +} diff --git a/core/outbound/clients/resolver/udp_resolver.go b/core/outbound/clients/resolver/udp_resolver.go new file mode 100644 index 0000000..917aa2f --- /dev/null +++ b/core/outbound/clients/resolver/udp_resolver.go @@ -0,0 +1,25 @@ +package resolver + +import ( + "github.com/miekg/dns" +) + +type UDPResolver struct { + BaseResolver +} + +func (r *UDPResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { + // we don't need to pooling for UDP sockets + conn, err := r.CreateBaseConn() + if err != nil { + return nil, err + } + defer conn.Close() + r.setTimeout(conn) + ret, err := r.exchangeByDNSClient(q, conn) + return ret, err +} + +func (r *UDPResolver) Init() { + +} diff --git a/core/outbound/dispatcher.go b/core/outbound/dispatcher.go index 0bc287e..ad5c778 100644 --- a/core/outbound/dispatcher.go +++ b/core/outbound/dispatcher.go @@ -1,7 +1,9 @@ package outbound import ( + "github.com/shawn1m/overture/core/outbound/clients/resolver" "net" + "time" "github.com/miekg/dns" log "github.com/sirupsen/logrus" @@ -25,17 +27,39 @@ type Dispatcher struct { DomainAlternativeList matcher.Matcher RedirectIPv6Record bool AlternativeDNSConcurrent bool + PoolIdleTimeout int + PoolMaxCapacity int MinimumTTL int DomainTTLMap map[string]uint32 Hosts *hosts.Hosts Cache *cache.Cache + + primaryResolvers []resolver.Resolver + alternativeResolvers []resolver.Resolver +} + +func createResolver(ul []*common.DNSUpstream) (resolvers []resolver.Resolver) { + resolvers = make([]resolver.Resolver, len(ul)) + for i, u := range ul { + resolvers[i] = resolver.NewResolver(u) + } + return resolvers +} + +func (d *Dispatcher) Init() { + resolver.IdleTimeout = time.Duration(d.PoolIdleTimeout) * time.Second + resolver.PoolMaxCapacity = d.PoolMaxCapacity + log.Debugf("Set pool's IdleTimeout to %d, MaxCapacity to %d", d.PoolIdleTimeout, d.PoolMaxCapacity) + d.primaryResolvers = createResolver(d.PrimaryDNS) + d.alternativeResolvers = createResolver(d.AlternativeDNS) } func (d *Dispatcher) Exchange(query *dns.Msg, inboundIP string) *dns.Msg { - PrimaryClientBundle := clients.NewClientBundle(query, d.PrimaryDNS, inboundIP, d.MinimumTTL, d.Cache, "Primary", d.DomainTTLMap) - AlternativeClientBundle := clients.NewClientBundle(query, d.AlternativeDNS, inboundIP, d.MinimumTTL, d.Cache, "Alternative", d.DomainTTLMap) + PrimaryClientBundle := clients.NewClientBundle(query, d.PrimaryDNS, d.primaryResolvers, inboundIP, d.MinimumTTL, d.Cache, "Primary", d.DomainTTLMap) + AlternativeClientBundle := clients.NewClientBundle(query, d.AlternativeDNS, d.alternativeResolvers, inboundIP, d.MinimumTTL, d.Cache, "Alternative", d.DomainTTLMap) + var ActiveClientBundle *clients.RemoteClientBundle localClient := clients.NewLocalClient(query, d.Hosts, d.MinimumTTL, d.DomainTTLMap) diff --git a/core/outbound/dispatcher_test.go b/core/outbound/dispatcher_test.go index db525a5..292945a 100644 --- a/core/outbound/dispatcher_test.go +++ b/core/outbound/dispatcher_test.go @@ -30,12 +30,15 @@ func init() { RedirectIPv6Record: conf.IPv6UseAlternativeDNS, AlternativeDNSConcurrent: conf.AlternativeDNSConcurrent, + PoolIdleTimeout: conf.PoolIdleTimeout, + PoolMaxCapacity: conf.PoolMaxCapacity, MinimumTTL: conf.MinimumTTL, DomainTTLMap: conf.DomainTTLMap, Hosts: conf.Hosts, Cache: conf.Cache, } + dispatcher.Init() } func TestDispatcher(t *testing.T) { diff --git a/debian/config.json b/debian/config.json index d58e50b..3e08871 100644 --- a/debian/config.json +++ b/debian/config.json @@ -29,6 +29,8 @@ "OnlyPrimaryDNS": false, "RedirectIPv6Record": false, "AlternativeDNSConcurrent": false, + "PoolIdleTimeout": 15, + "PoolMaxCapacity": 15, "DomainBase64Decode": true, "MinimumTTL": 0, "CacheSize" : 0, diff --git a/go.mod b/go.mod index 37d9125..35eeb18 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/miekg/dns v1.1.8 + github.com/silenceper/pool v0.0.0-20191105065223-1f4530b6ba17 github.com/sirupsen/logrus v1.4.1 golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 // indirect golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 diff --git a/go.sum b/go.sum index 575d7cb..f83a039 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,11 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI= github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/silenceper/pool v0.0.0-20191105065223-1f4530b6ba17 h1:dsxWVMu7pjgs0j/Nj4WZ1oMyeCxbesUzL3y9G3bOXXY= +github.com/silenceper/pool v0.0.0-20191105065223-1f4530b6ba17/go.mod h1:i2v38p3EJGWv7qR7neimK3eGm+576HdPdYlW0VIEph8= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=