diff --git a/README.md b/README.md index 6ee6a3f9..a031ecea 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ experimental: # nameserver: # - 114.114.114.114 # - tls://dns.rubyfish.cn:853 # dns over tls + # - https://1.1.1.1/dns-query # dns over https # fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN # - tcp://1.1.1.1 diff --git a/config/config.go b/config/config.go index 9253dd5f..598ac834 100644 --- a/config/config.go +++ b/config/config.go @@ -475,9 +475,14 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) { case "tls": host, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "tcp-tls" // DNS over TLS + case "https": + clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path} + host = clearURL.String() + dnsNetType = "https" // DNS over HTTPS default: return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) } + if err != nil { return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) } diff --git a/dns/client.go b/dns/client.go index 4af716bd..881fcded 100644 --- a/dns/client.go +++ b/dns/client.go @@ -2,270 +2,20 @@ package dns import ( "context" - "crypto/tls" - "errors" - "net" - "strings" - "sync" - "time" - - "github.com/Dreamacro/clash/common/cache" - "github.com/Dreamacro/clash/common/picker" - "github.com/Dreamacro/clash/component/fakeip" - C "github.com/Dreamacro/clash/constant" D "github.com/miekg/dns" - geoip2 "github.com/oschwald/geoip2-golang" ) -var ( - globalSessionCache = tls.NewLRUClientSessionCache(64) - - mmdb *geoip2.Reader - once sync.Once - resolver *Resolver -) - -type Resolver struct { - ipv6 bool - mapping bool - fakeip bool - pool *fakeip.Pool - fallback []*nameserver - main []*nameserver - cache *cache.Cache -} - -type result struct { - Msg *D.Msg - Error error -} - -func isIPRequest(q D.Question) bool { - if q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA) { - return true - } - return false -} - -func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) { - if len(m.Question) == 0 { - return nil, errors.New("should have one question at least") - } - - q := m.Question[0] - cache, expireTime := r.cache.GetWithExpire(q.String()) - if cache != nil { - msg = cache.(*D.Msg).Copy() - setMsgTTL(msg, uint32(expireTime.Sub(time.Now()).Seconds())) - return - } - defer func() { - if msg == nil { - return - } - - putMsgToCache(r.cache, q.String(), msg) - if r.mapping { - ips := r.msgToIP(msg) - for _, ip := range ips { - putMsgToCache(r.cache, ip.String(), msg) - } - } - }() - - isIPReq := isIPRequest(q) - if isIPReq { - msg, err = r.resolveIP(m) - return - } - - msg, err = r.exchange(r.main, m) - return -} - -func (r *Resolver) exchange(servers []*nameserver, m *D.Msg) (msg *D.Msg, err error) { - in := make(chan interface{}) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - fast := picker.SelectFast(ctx, in) - - wg := sync.WaitGroup{} - wg.Add(len(servers)) - for _, server := range servers { - go func(s *nameserver) { - defer wg.Done() - msg, _, err := s.Client.Exchange(m, s.Address) - if err != nil || msg.Rcode != D.RcodeSuccess { - return - } - in <- msg - }(server) - } - - // release in channel - go func() { - wg.Wait() - close(in) - }() - - elm, exist := <-fast - if !exist { - return nil, errors.New("All DNS requests failed") - } - - msg = elm.(*D.Msg) - return -} - -func (r *Resolver) resolveIP(m *D.Msg) (msg *D.Msg, err error) { - msgCh := r.resolve(r.main, m) - if r.fallback == nil { - res := <-msgCh - msg, err = res.Msg, res.Error - return - } - fallbackMsg := r.resolve(r.fallback, m) - res := <-msgCh - if res.Error == nil { - if mmdb == nil { - return nil, errors.New("GeoIP can't use") - } - - if ips := r.msgToIP(res.Msg); len(ips) != 0 { - if record, _ := mmdb.Country(ips[0]); record.Country.IsoCode == "CN" || record.Country.IsoCode == "" { - // release channel - go func() { <-fallbackMsg }() - msg = res.Msg - return msg, err - } - } - } - - res = <-fallbackMsg - msg, err = res.Msg, res.Error - return -} - -func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) { - ip = net.ParseIP(host) - if ip != nil { - return ip, nil - } - - query := &D.Msg{} - dnsType := D.TypeA - if r.ipv6 { - dnsType = D.TypeAAAA - } - query.SetQuestion(D.Fqdn(host), dnsType) - - msg, err := r.Exchange(query) - if err != nil { - return nil, err - } - - ips := r.msgToIP(msg) - if len(ips) == 0 { - return nil, errors.New("can't found ip") - } - - ip = ips[0] - return -} - -func (r *Resolver) msgToIP(msg *D.Msg) []net.IP { - ips := []net.IP{} - - for _, answer := range msg.Answer { - switch ans := answer.(type) { - case *D.AAAA: - ips = append(ips, ans.AAAA) - case *D.A: - ips = append(ips, ans.A) - } - } - - return ips -} - -func (r *Resolver) IPToHost(ip net.IP) (string, bool) { - cache := r.cache.Get(ip.String()) - if cache == nil { - return "", false - } - fqdn := cache.(*D.Msg).Question[0].Name - return strings.TrimRight(fqdn, "."), true -} - -func (r *Resolver) resolve(client []*nameserver, msg *D.Msg) <-chan *result { - ch := make(chan *result) - go func() { - res, err := r.exchange(client, msg) - ch <- &result{Msg: res, Error: err} - }() - return ch -} - -func (r *Resolver) IsMapping() bool { - return r.mapping -} - -func (r *Resolver) IsFakeIP() bool { - return r.fakeip -} - -type NameServer struct { - Net string - Addr string -} - -type nameserver struct { - Client *D.Client +type client struct { + *D.Client Address string } -type Config struct { - Main, Fallback []NameServer - IPv6 bool - EnhancedMode EnhancedMode - Pool *fakeip.Pool +func (c *client) Exchange(m *D.Msg) (msg *D.Msg, err error) { + return c.ExchangeContext(context.Background(), m) } -func transform(servers []NameServer) []*nameserver { - var ret []*nameserver - for _, s := range servers { - ret = append(ret, &nameserver{ - Client: &D.Client{ - Net: s.Net, - TLSConfig: &tls.Config{ - ClientSessionCache: globalSessionCache, - // alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6 - NextProtos: []string{"dns"}, - }, - UDPSize: 4096, - }, - Address: s.Addr, - }) - } - return ret -} - -func New(config Config) *Resolver { - once.Do(func() { - mmdb, _ = geoip2.Open(C.Path.MMDB()) - }) - - r := &Resolver{ - main: transform(config.Main), - ipv6: config.IPv6, - cache: cache.New(time.Second * 60), - mapping: config.EnhancedMode == MAPPING, - fakeip: config.EnhancedMode == FAKEIP, - pool: config.Pool, - } - if config.Fallback != nil { - r.fallback = transform(config.Fallback) - } - return r +func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + msg, _, err = c.Client.ExchangeContext(ctx, m, c.Address) + return } diff --git a/dns/doh.go b/dns/doh.go new file mode 100644 index 00000000..51a69df5 --- /dev/null +++ b/dns/doh.go @@ -0,0 +1,75 @@ +package dns + +import ( + "bytes" + "context" + "crypto/tls" + "io/ioutil" + "net/http" + + D "github.com/miekg/dns" +) + +const ( + // dotMimeType is the DoH mimetype that should be used. + dotMimeType = "application/dns-message" + + // dotPath is the URL path that should be used. + dotPath = "/dns-query" +) + +var dohTransport = &http.Transport{ + TLSClientConfig: &tls.Config{ClientSessionCache: globalSessionCache}, +} + +type dohClient struct { + url string +} + +func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { + return dc.ExchangeContext(context.Background(), m) +} + +func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + req, err := dc.newRequest(m) + if err != nil { + return nil, err + } + + req = req.WithContext(ctx) + return dc.doRequest(req) +} + +// newRequest returns a new DoH request given a dns.Msg. +func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) { + buf, err := m.Pack() + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, dc.url+"?bla=foo:443", bytes.NewReader(buf)) + if err != nil { + return req, err + } + + req.Header.Set("content-type", dotMimeType) + req.Header.Set("accept", dotMimeType) + return req, nil +} + +func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) { + client := &http.Client{Transport: dohTransport} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + msg = &D.Msg{} + err = msg.Unpack(buf) + return msg, err +} diff --git a/dns/resolver.go b/dns/resolver.go new file mode 100644 index 00000000..bb0710d2 --- /dev/null +++ b/dns/resolver.go @@ -0,0 +1,289 @@ +package dns + +import ( + "context" + "crypto/tls" + "errors" + "net" + "strings" + "sync" + "time" + + "github.com/Dreamacro/clash/common/cache" + "github.com/Dreamacro/clash/common/picker" + "github.com/Dreamacro/clash/component/fakeip" + C "github.com/Dreamacro/clash/constant" + + D "github.com/miekg/dns" + geoip2 "github.com/oschwald/geoip2-golang" +) + +var ( + globalSessionCache = tls.NewLRUClientSessionCache(64) + + mmdb *geoip2.Reader + once sync.Once +) + +type resolver interface { + Exchange(m *D.Msg) (msg *D.Msg, err error) + ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) +} + +type result struct { + Msg *D.Msg + Error error +} + +type Resolver struct { + ipv6 bool + mapping bool + fakeip bool + pool *fakeip.Pool + fallback []resolver + main []resolver + cache *cache.Cache +} + +// ResolveIP request with TypeA and TypeAAAA, priority return TypeAAAA +func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) { + ch := make(chan net.IP) + go func() { + ip, err := r.resolveIP(host, D.TypeA) + if err != nil { + close(ch) + return + } + ch <- ip + }() + + ip, err = r.resolveIP(host, D.TypeAAAA) + if err == nil { + go func() { + <-ch + }() + return + } + + ip, closed := <-ch + if closed { + return nil, errors.New("can't found ip") + } + + return ip, nil +} + +// ResolveIPv4 request with TypeA +func (r *Resolver) ResolveIPv4(host string) (ip net.IP, err error) { + ip = net.ParseIP(host) + if ip != nil { + return ip, nil + } + + query := &D.Msg{} + query.SetQuestion(D.Fqdn(host), D.TypeA) + + msg, err := r.Exchange(query) + if err != nil { + return nil, err + } + + ips := r.msgToIP(msg) + if len(ips) == 0 { + return nil, errors.New("can't found ip") + } + + ip = ips[0] + return +} + +// Exchange a batch of dns request, and it use cache +func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) { + if len(m.Question) == 0 { + return nil, errors.New("should have one question at least") + } + + q := m.Question[0] + cache, expireTime := r.cache.GetWithExpire(q.String()) + if cache != nil { + msg = cache.(*D.Msg).Copy() + setMsgTTL(msg, uint32(expireTime.Sub(time.Now()).Seconds())) + return + } + defer func() { + if msg == nil { + return + } + + putMsgToCache(r.cache, q.String(), msg) + if r.mapping { + ips := r.msgToIP(msg) + for _, ip := range ips { + putMsgToCache(r.cache, ip.String(), msg) + } + } + }() + + isIPReq := isIPRequest(q) + if isIPReq { + msg, err = r.fallbackExchange(m) + return + } + + msg, err = r.batchExchange(r.main, m) + return +} + +// IPToHost return fake-ip or redir-host mapping host +func (r *Resolver) IPToHost(ip net.IP) (string, bool) { + cache := r.cache.Get(ip.String()) + if cache == nil { + return "", false + } + fqdn := cache.(*D.Msg).Question[0].Name + return strings.TrimRight(fqdn, "."), true +} + +func (r *Resolver) IsMapping() bool { + return r.mapping +} + +func (r *Resolver) IsFakeIP() bool { + return r.fakeip +} + +func (r *Resolver) batchExchange(clients []resolver, m *D.Msg) (msg *D.Msg, err error) { + in := make(chan interface{}) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + fast := picker.SelectFast(ctx, in) + + wg := sync.WaitGroup{} + wg.Add(len(clients)) + for _, r := range clients { + go func(r resolver) { + defer wg.Done() + msg, err := r.ExchangeContext(ctx, m) + if err != nil || msg.Rcode != D.RcodeSuccess { + return + } + in <- msg + }(r) + } + + // release in channel + go func() { + wg.Wait() + close(in) + }() + + elm, exist := <-fast + if !exist { + return nil, errors.New("All DNS requests failed") + } + + msg = elm.(*D.Msg) + return +} + +func (r *Resolver) fallbackExchange(m *D.Msg) (msg *D.Msg, err error) { + msgCh := r.asyncExchange(r.main, m) + if r.fallback == nil { + res := <-msgCh + msg, err = res.Msg, res.Error + return + } + fallbackMsg := r.asyncExchange(r.fallback, m) + res := <-msgCh + if res.Error == nil { + if mmdb == nil { + return nil, errors.New("GeoIP can't use") + } + + if ips := r.msgToIP(res.Msg); len(ips) != 0 { + if record, _ := mmdb.Country(ips[0]); record.Country.IsoCode == "CN" || record.Country.IsoCode == "" { + // release channel + go func() { <-fallbackMsg }() + msg = res.Msg + return msg, err + } + } + } + + res = <-fallbackMsg + msg, err = res.Msg, res.Error + return +} + +func (r *Resolver) resolveIP(host string, dnsType uint16) (ip net.IP, err error) { + query := &D.Msg{} + query.SetQuestion(D.Fqdn(host), dnsType) + + msg, err := r.Exchange(query) + if err != nil { + return nil, err + } + + ips := r.msgToIP(msg) + if len(ips) == 0 { + return nil, errors.New("can't found ip") + } + + ip = ips[0] + return +} + +func (r *Resolver) msgToIP(msg *D.Msg) []net.IP { + ips := []net.IP{} + + for _, answer := range msg.Answer { + switch ans := answer.(type) { + case *D.AAAA: + ips = append(ips, ans.AAAA) + case *D.A: + ips = append(ips, ans.A) + } + } + + return ips +} + +func (r *Resolver) asyncExchange(client []resolver, msg *D.Msg) <-chan *result { + ch := make(chan *result) + go func() { + res, err := r.batchExchange(client, msg) + ch <- &result{Msg: res, Error: err} + }() + return ch +} + +type NameServer struct { + Net string + Addr string +} + +type Config struct { + Main, Fallback []NameServer + IPv6 bool + EnhancedMode EnhancedMode + Pool *fakeip.Pool +} + +func New(config Config) *Resolver { + once.Do(func() { + mmdb, _ = geoip2.Open(C.Path.MMDB()) + }) + + r := &Resolver{ + main: transform(config.Main), + ipv6: config.IPv6, + cache: cache.New(time.Second * 60), + mapping: config.EnhancedMode == MAPPING, + fakeip: config.EnhancedMode == FAKEIP, + pool: config.Pool, + } + if len(config.Fallback) != 0 { + r.fallback = transform(config.Fallback) + } + return r +} diff --git a/dns/util.go b/dns/util.go index c14cb47a..e29ea1b0 100644 --- a/dns/util.go +++ b/dns/util.go @@ -1,6 +1,7 @@ package dns import ( + "crypto/tls" "encoding/json" "errors" "time" @@ -107,3 +108,34 @@ func setMsgTTL(msg *D.Msg, ttl uint32) { extra.Header().Ttl = ttl } } + +func isIPRequest(q D.Question) bool { + if q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA) { + return true + } + return false +} + +func transform(servers []NameServer) []resolver { + ret := []resolver{} + for _, s := range servers { + if s.Net == "https" { + ret = append(ret, &dohClient{url: s.Addr}) + continue + } + + ret = append(ret, &client{ + Client: &D.Client{ + Net: s.Net, + TLSConfig: &tls.Config{ + ClientSessionCache: globalSessionCache, + // alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6 + NextProtos: []string{"dns"}, + }, + UDPSize: 4096, + }, + Address: s.Addr, + }) + } + return ret +} diff --git a/go.mod b/go.mod index a4cc9e5f..6ffd572e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/go-chi/render v1.0.1 github.com/gofrs/uuid v3.2.0+incompatible github.com/gorilla/websocket v1.4.0 - github.com/miekg/dns v1.1.9 + github.com/miekg/dns v1.1.14 github.com/oschwald/geoip2-golang v1.2.1 github.com/oschwald/maxminddb-golang v1.3.0 // indirect github.com/sirupsen/logrus v1.4.1 diff --git a/go.sum b/go.sum index c9aef370..f4ca045a 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 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.9 h1:OIdC9wT96RzuZMf2PfKRhFgsStHUUBZLM/lo1LqiM9E= -github.com/miekg/dns v1.1.9/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.14 h1:wkQWn9wIp4mZbwW8XV6Km6owkvRPbOiV004ZM2CkGvA= +github.com/miekg/dns v1.1.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU= github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE= github.com/oschwald/maxminddb-golang v1.3.0 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE=