diff --git a/config/config.go b/config/config.go index f82793d6..dc4ba2b4 100644 --- a/config/config.go +++ b/config/config.go @@ -79,6 +79,7 @@ type Controller struct { // DNS config type DNS struct { Enable bool `yaml:"enable"` + PreferH3 bool `yaml:"prefer-h3"` IPv6 bool `yaml:"ipv6"` NameServer []dns.NameServer `yaml:"nameserver"` Fallback []dns.NameServer `yaml:"fallback"` diff --git a/dns/doh.go b/dns/doh.go index 36f5d21c..5010bbcb 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -3,14 +3,17 @@ package dns import ( "bytes" "context" + "crypto/tls" + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/resolver" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" + D "github.com/miekg/dns" + "go.uber.org/atomic" "io" "net" "net/http" - - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - - D "github.com/miekg/dns" + "strconv" ) const ( @@ -22,6 +25,9 @@ type dohClient struct { url string proxyAdapter string transport *http.Transport + h3Transport *http3.RoundTripper + supportH3 *atomic.Bool + firstTest *atomic.Bool } func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { @@ -63,12 +69,32 @@ func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) { return req, nil } -func (dc *dohClient) doRequest(req *http.Request) (*D.Msg, error) { - client := &http.Client{Transport: dc.transport} +func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) { + if dc.supportH3.Load() { + msg, err = dc.doRequestWithTransport(req, dc.h3Transport) + if err != nil { + if dc.firstTest.CAS(true, false) { + dc.supportH3.Store(false) + _ = dc.h3Transport.Close() + dc.h3Transport = nil + } + } + } else { + msg, err = dc.doRequestWithTransport(req, dc.transport) + } + + return +} + +func (dc *dohClient) doRequestWithTransport(req *http.Request, transport http.RoundTripper) (*D.Msg, error) { + client := &http.Client{Transport: transport} resp, err := client.Do(req) if err != nil { - return nil, err + if err != nil { + return nil, err + } } + defer resp.Body.Close() buf, err := io.ReadAll(resp.Body) @@ -80,7 +106,7 @@ func (dc *dohClient) doRequest(req *http.Request) (*D.Msg, error) { return msg, err } -func newDoHClient(url string, r *Resolver, proxyAdapter string) *dohClient { +func newDoHClient(url string, r *Resolver, preferH3 bool, proxyAdapter string) *dohClient { return &dohClient{ url: url, proxyAdapter: proxyAdapter, @@ -104,5 +130,40 @@ func newDoHClient(url string, r *Resolver, proxyAdapter string) *dohClient { } }, }, + + h3Transport: &http3.RoundTripper{ + Dial: func(ctx context.Context, network, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + ip, err := resolver.ResolveIPWithResolver(host, r) + if err != nil { + return nil, err + } + if proxyAdapter == "" { + return quic.DialAddrEarlyContext(ctx, net.JoinHostPort(ip.String(), port), tlsCfg, cfg) + } else { + if conn, err := dialContextExtra(ctx, proxyAdapter, "udp", ip, port); err == nil { + portInt, err := strconv.Atoi(port) + if err != nil { + return nil, err + } + + udpAddr := net.UDPAddr{ + IP: net.ParseIP(ip.String()), + Port: portInt, + } + + return quic.DialEarlyContext(ctx, conn.(net.PacketConn), &udpAddr, host, tlsCfg, cfg) + } else { + return nil, err + } + } + }, + }, + supportH3: atomic.NewBool(preferH3), + firstTest: atomic.NewBool(true), } } diff --git a/dns/resolver.go b/dns/resolver.go index 4d788c18..597254c4 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -365,6 +365,7 @@ type FallbackFilter struct { } type Config struct { + PreferH3 bool Main, Fallback []NameServer Default []NameServer ProxyServer []NameServer @@ -378,29 +379,29 @@ type Config struct { func NewResolver(config Config) *Resolver { defaultResolver := &Resolver{ - main: transform(config.Default, nil), + main: transform(config.Default, nil, config.PreferH3), lruCache: cache.NewLRUCache[string, *D.Msg](cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), } r := &Resolver{ ipv6: config.IPv6, - main: transform(config.Main, defaultResolver), + main: transform(config.Main, defaultResolver, config.PreferH3), lruCache: cache.NewLRUCache[string, *D.Msg](cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), hosts: config.Hosts, } if len(config.Fallback) != 0 { - r.fallback = transform(config.Fallback, defaultResolver) + r.fallback = transform(config.Fallback, defaultResolver, config.PreferH3) } if len(config.ProxyServer) != 0 { - r.proxyServer = transform(config.ProxyServer, defaultResolver) + r.proxyServer = transform(config.ProxyServer, defaultResolver, config.PreferH3) } if len(config.Policy) != 0 { r.policy = trie.New[*Policy]() for domain, nameserver := range config.Policy { - _ = r.policy.Insert(domain, NewPolicy(transform([]NameServer{nameserver}, defaultResolver))) + _ = r.policy.Insert(domain, NewPolicy(transform([]NameServer{nameserver}, defaultResolver, config.PreferH3))) } } diff --git a/dns/util.go b/dns/util.go index 7abbbe97..a5cb6c68 100644 --- a/dns/util.go +++ b/dns/util.go @@ -54,12 +54,12 @@ func isIPRequest(q D.Question) bool { return q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA) } -func transform(servers []NameServer, resolver *Resolver) []dnsClient { +func transform(servers []NameServer, resolver *Resolver, preferH3 bool) []dnsClient { ret := []dnsClient{} for _, s := range servers { switch s.Net { case "https": - ret = append(ret, newDoHClient(s.Addr, resolver, s.ProxyAdapter)) + ret = append(ret, newDoHClient(s.Addr, resolver, preferH3, s.ProxyAdapter)) continue case "dhcp": ret = append(ret, newDHCPClient(s.Addr)) diff --git a/docs/config.yaml b/docs/config.yaml index ea2149be..d0dfcddb 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -68,7 +68,7 @@ profile: dns: enable: false # 关闭将使用系统DNS listen: 0.0.0.0:53 # 开启DNS服务器监听 - + prefer-h3: false # 启动DOH优先使用http/3 # ipv6: false # false将返回AAAA的空结果 # 用于解析nameserver,fallbacky以及其他DNS服务器配置的,DNS服务域名 diff --git a/go.mod b/go.mod index fa956385..ddbaf328 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/google/btree v1.0.1 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/kr/pretty v0.2.1 // indirect + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect diff --git a/go.sum b/go.sum index 843a6d90..4b814898 100644 --- a/go.sum +++ b/go.sum @@ -117,6 +117,7 @@ github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ= github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index fef9cb70..28717bae 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -163,6 +163,7 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { Default: c.DefaultNameserver, Policy: c.NameServerPolicy, ProxyServer: c.ProxyServerNameserver, + PreferH3: c.PreferH3, } r := dns.NewResolver(cfg)