Feature: support DoH
This commit is contained in:
parent
662038e40e
commit
bc3fc0c840
8 changed files with 412 additions and 260 deletions
|
@ -123,6 +123,7 @@ experimental:
|
||||||
# nameserver:
|
# nameserver:
|
||||||
# - 114.114.114.114
|
# - 114.114.114.114
|
||||||
# - tls://dns.rubyfish.cn:853 # dns over tls
|
# - 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
|
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
|
||||||
# - tcp://1.1.1.1
|
# - tcp://1.1.1.1
|
||||||
|
|
||||||
|
|
|
@ -475,9 +475,14 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
|
||||||
case "tls":
|
case "tls":
|
||||||
host, err = hostWithDefaultPort(u.Host, "853")
|
host, err = hostWithDefaultPort(u.Host, "853")
|
||||||
dnsNetType = "tcp-tls" // DNS over TLS
|
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:
|
default:
|
||||||
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
|
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
|
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
|
||||||
}
|
}
|
||||||
|
|
264
dns/client.go
264
dns/client.go
|
@ -2,270 +2,20 @@ package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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"
|
D "github.com/miekg/dns"
|
||||||
geoip2 "github.com/oschwald/geoip2-golang"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type client struct {
|
||||||
globalSessionCache = tls.NewLRUClientSessionCache(64)
|
*D.Client
|
||||||
|
|
||||||
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
|
|
||||||
Address string
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
func (c *client) Exchange(m *D.Msg) (msg *D.Msg, err error) {
|
||||||
Main, Fallback []NameServer
|
return c.ExchangeContext(context.Background(), m)
|
||||||
IPv6 bool
|
|
||||||
EnhancedMode EnhancedMode
|
|
||||||
Pool *fakeip.Pool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func transform(servers []NameServer) []*nameserver {
|
func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||||
var ret []*nameserver
|
msg, _, err = c.Client.ExchangeContext(ctx, m, c.Address)
|
||||||
for _, s := range servers {
|
return
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
75
dns/doh.go
Normal file
75
dns/doh.go
Normal file
|
@ -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
|
||||||
|
}
|
289
dns/resolver.go
Normal file
289
dns/resolver.go
Normal file
|
@ -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
|
||||||
|
}
|
32
dns/util.go
32
dns/util.go
|
@ -1,6 +1,7 @@
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
@ -107,3 +108,34 @@ func setMsgTTL(msg *D.Msg, ttl uint32) {
|
||||||
extra.Header().Ttl = ttl
|
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
|
||||||
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -8,7 +8,7 @@ require (
|
||||||
github.com/go-chi/render v1.0.1
|
github.com/go-chi/render v1.0.1
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible
|
github.com/gofrs/uuid v3.2.0+incompatible
|
||||||
github.com/gorilla/websocket v1.4.0
|
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/geoip2-golang v1.2.1
|
||||||
github.com/oschwald/maxminddb-golang v1.3.0 // indirect
|
github.com/oschwald/maxminddb-golang v1.3.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.4.1
|
github.com/sirupsen/logrus v1.4.1
|
||||||
|
|
4
go.sum
4
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/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 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
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.14 h1:wkQWn9wIp4mZbwW8XV6Km6owkvRPbOiV004ZM2CkGvA=
|
||||||
github.com/miekg/dns v1.1.9/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
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 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU=
|
||||||
github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
|
github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
|
||||||
github.com/oschwald/maxminddb-golang v1.3.0 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE=
|
github.com/oschwald/maxminddb-golang v1.3.0 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE=
|
||||||
|
|
Loading…
Reference in a new issue