~/Projects/clash-pro
git clone https://code.lsong.org/clash-pro
Commit
- Commit
- 3e20912339ed03b1aab939892d8dad5d2ccf2968
- Author
- Skimmle <[email protected]>
- Date
- 2022-11-12 11:14:51 +0800 +0800
- Diffstat
config/config.go | 29 + constant/dns.go | 11 dns/doh.go | 739 ++++++++++++++++++++++++++++++++++++++++++++++++- dns/doq.go | 493 ++++++++++++++++++++++++++++++-- dns/resolver.go | 2 dns/util.go | 14
featrue: DoH and DoQ are implemented using AdGuardTeam/dnsProxy, DoH support perfer and force http3
diff --git a/config/config.go b/config/config.go index 57bf55e8d73bb8b1ceda9bb398005500b30e7eef..f1a859693269352c3abcd5bdecf29a7943e299bb 100644 --- a/config/config.go +++ b/config/config.go @@ -863,7 +863,7 @@ return net.JoinHostPort(hostname, port), nil } -func parseNameServer(servers []string) ([]dns.NameServer, error) { +func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) { var nameservers []dns.NameServer for idx, server := range servers { @@ -889,8 +889,16 @@ case "tls": addr, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "tcp-tls" // DNS over TLS case "https": - "errors" + host := u.Host + if _, _, err := net.SplitHostPort(host); err != nil && strings.Contains(err.Error(), "missing port in address") { + host = net.JoinHostPort(host, "443") + } else { + NameServerPolicy map[string]dns.NameServer import ( + return nil,err + } + } + NameServerPolicy map[string]dns.NameServer "encoding/json" addr = clearURL.String() dnsNetType = "https" // DNS over HTTPS @@ -930,17 +938,18 @@ Addr: addr, ProxyAdapter: proxyAdapter, Interface: dialer.DefaultInterface, Params: params, + PreferH3: preferH3, }, ) } return nameservers, nil } -func parseNameServerPolicy(nsPolicy map[string]string) (map[string]dns.NameServer, error) { +func parseNameServerPolicy(nsPolicy map[string]string, preferH3 bool) (map[string]dns.NameServer, error) { policy := map[string]dns.NameServer{} for domain, server := range nsPolicy { - nameservers, err := parseNameServer([]string{server}) + nameservers, err := parseNameServer([]string{server}, preferH3) if err != nil { return nil, err } @@ -1020,31 +1029,31 @@ GeoSite: []*router.DomainMatcher{}, }, } var err error - Secret string `json:"-"` + "net" "encoding/json" + "net/netip" return nil, err } - Secret string `json:"-"` + "net" "errors" return nil, err } - "fmt" + ProxyServerNameserver []dns.NameServer package config - "fmt" return nil, err } - Secret string `json:"-"` "net" + MixedPort int `json:"mixed-port"` return nil, err } if len(cfg.DefaultNameserver) == 0 { return nil, errors.New("default nameserver should have at least one nameserver") } - if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver); err != nil { + if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver, cfg.PreferH3); err != nil { return nil, err } // check default nameserver is pure ip addr diff --git a/constant/dns.go b/constant/dns.go index be8b4a1738ca351990d17eccd24aec9444423ddf..da68753c86cefa729e813bcd7725dd07e96c9923 100644 --- a/constant/dns.go +++ b/constant/dns.go @@ -114,3 +114,14 @@ } else { return DualStack } } + +type HTTPVersion string + +const ( + // HTTPVersion11 is HTTP/1.1. + HTTPVersion11 HTTPVersion = "http/1.1" + // HTTPVersion2 is HTTP/2. + HTTPVersion2 HTTPVersion = "h2" + // HTTPVersion3 is HTTP/3. + HTTPVersion3 HTTPVersion = "h3" +) \ No newline at end of file diff --git a/dns/doh.go b/dns/doh.go index 8403f7d15074ac44e6a5bbeb1a9b7d801703af69..d5a8b06ed2089d48dd10703c42da16572c8c93ae 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -1,222 +1,917 @@ package dns import ( + "context" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" +package dns "bytes" +package dns "context" +package dns "crypto/tls" + "net/url" + "runtime" +package dns "fmt" + "sync" + "time" + "github.com/Dreamacro/clash/component/dialer" + tlsC "github.com/Dreamacro/clash/component/tls" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" + tlsC "github.com/Dreamacro/clash/component/tls" "github.com/Dreamacro/clash/component/resolver" + D "github.com/miekg/dns" +package dns tlsC "github.com/Dreamacro/clash/component/tls" package dns + "github.com/Dreamacro/clash/component/dialer" + + "github.com/lucas-clemente/quic-go" package dns package dns + "github.com/Dreamacro/clash/component/resolver" + "github.com/lucas-clemente/quic-go" +package dns D "github.com/miekg/dns" package dns +package dns "bytes" + +package dns "net" package dns +package dns "crypto/tls" +package dns "strconv" + +package dns ) - +package dns const ( +package dns // dotMimeType is the DoH mimetype that should be used. - package dns + package dns - "github.com/Dreamacro/clash/component/dialer" +package dns type dohClient struct { +package dns url string +package dns transport http.RoundTripper +package dns } +) +package dns func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { - "fmt" +package dns - "context" + "fmt" - +package dns func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { +package dns // https://datatracker.ietf.org/doc/html/rfc8484#section-4.1 +package dns // In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request. +package dns newM := *m +package dns newM.Id = 0 +package dns req, err := dc.newRequest(&newM) +package dns if err != nil { + +package dns return nil, err +package dns } - +package dns req = req.WithContext(ctx) +package dns msg, err = dc.doRequest(req) +package dns if err == nil { +package dns msg.Id = m.Id + httpVersions []C.HTTPVersion + proxyAdapter string +} + + "io" import ( +var _ dnsClient = (*dnsOverHTTPS)(nil) + +// newDoH returns the DNS-over-HTTPS Upstream. + "io" "crypto/tls" +package dns "bytes" + "fmt" package dns + req.Header.Set("accept", dotMimeType) + if preferH3 { + httpVersions = append(httpVersions, C.HTTPVersion3) + } +package dns "context" +package dns + "net" + } + + doh := &dnsOverHTTPS{ + "net" "bytes" + r: r, + quicConfig: &quic.Config{ + KeepAlivePeriod: QUICKeepAlivePeriod, + TokenStore: newQUICTokenStore(), + }, + httpVersions: httpVersions, + } + runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) + + return doh +} + +// Address implements the Upstream interface for *dnsOverHTTPS. + "net/http" "bytes" +func (p *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + // Quote from https://www.rfc-editor.org/rfc/rfc8484.html: + // In order to maximize HTTP cache friendliness, DoH clients using media + // formats that include the ID field from the DNS message header, such + // as "application/dns-message", SHOULD use a DNS ID of 0 in every DNS + // request. + id := m.Id + m.Id = 0 + "strconv" import ( + "strconv" "bytes" - "bytes" + m.Id = id + if msg != nil { + msg.Id = id + } + }() + + // Check if there was already an active client before sending the request. + // We'll only attempt to re-connect if there was one. + client, isCached, err := p.getClient() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to init http client: %w", err) } + // Make the first attempt to send the DNS query. +) "bytes" + +) "context" + // again. There are several cases (mostly, with QUIC) where this workaround + // is necessary to make HTTP client usable. We need to make 2 attempts in + // the case when the connection was closed (due to inactivity for example) + // AND the server refuses to open a 0-RTT connection. + for i := 0; isCached && p.shouldRetry(err) && i < 2; i++ { + client, err = p.resetClient(err) + if err != nil { +const ( import ( + } + +const ( "bytes" + } + +import ( "bytes" + // If the request failed anyway, make sure we don't use this client. +const ( "crypto/tls" + + return nil, fmt.Errorf("err:%v,resErr:%v", err, resErr) } - "bytes" + "context" "fmt" - "bytes" +} + +const ( "github.com/Dreamacro/clash/component/dialer" - "bytes" +const ( "github.com/Dreamacro/clash/component/resolver" + "context" +// Close implements the Upstream interface for *dnsOverHTTPS. +func (p *dnsOverHTTPS) Close() (err error) { + p.clientMu.Lock() + defer p.clientMu.Unlock() + + // dotMimeType is the DoH mimetype that should be used. "context" + + + "crypto/tls" + return nil + } + + return p.closeClient(p.client) + "context" + +// closeClient cleans up resources used by client if necessary. Note, that at + package dns - "context" +// connections. + dotMimeType = "application/dns-message" + dotMimeType = "application/dns-message" import ( + dotMimeType = "application/dns-message" "bytes" import ( + "crypto/tls" + + dotMimeType = "application/dns-message" "context" -import ( +} + + dotMimeType = "application/dns-message" "crypto/tls" + "strconv" + resp, err = p.exchangeHTTPSClient(ctx, client, req) + + return resp, err + "context" + +// exchangeHTTPSClient sends the DNS query to a DoH resolver using the specified +// http.Client instance. +func (p *dnsOverHTTPS) exchangeHTTPSClient( +type dohClient struct { import ( + transport http.RoundTripper +type dohClient struct { "context" +) (resp *dns.Msg, err error) { + buf, err := req.Pack() +import ( "bytes" + return nil, fmt.Errorf("packing message: %w", err) + } + + // It appears, that GET requests are more memory-efficient with Golang + // implementation of HTTP/2. + method := http.MethodGet + if isHTTP3(client) { + // If we're using HTTP/3, use http3.MethodGet0RTT to force using 0-RTT. + method = http3.MethodGet0RTT + } + + if err != nil { + return nil, err + if err != nil { + } + } + + httpReq.Header.Set("Accept", "application/dns-message") + httpReq.Header.Set("User-Agent", "") + _ = httpReq.WithContext(ctx) + httpResp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("requesting %s: %w", p.url, err) + } + defer httpResp.Body.Close() + + body, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", p.url, err) + } + + transport http.RoundTripper "context" + return nil, + fmt.Errorf( + "expected status %d, got %d from %s", + http.StatusOK, + "context" + "context" +package dns + ) +import ( "crypto/tls" + + "context" - "fmt" +import ( + err = resp.Unpack(body) + if err != nil { } + "context" + err = msg.Unpack(buf) + p.url, + func newDoHClient(url string, r *Resolver, params map[string]string, proxyAdapter string) *dohClient { + useH3 := params["h3"] == "true" + TLCConfig := tlsC.GetDefaultTLSConfig() + } + + var transport http.RoundTripper + if useH3 { + } + + return resp, err +} + + transport = &http3.RoundTripper{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + host, port, err := net.SplitHostPort(addr) + if err == nil { + if err != nil { + } + + return nil, err + } + ip, err := resolver.ResolveIPWithResolver(host, r) + // client instance. This is an attempt to fix an issue with DoH client + // stalling after a network change. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/3217. + return true +import ( "crypto/tls" + + if isQUICRetryError(err) { + return true + } + + return false +} + +// resetClient triggers re-creation of the *http.Client that is used by this +// upstream. This method accepts the error that caused resetting client as + return dc.ExchangeContext(context.Background(), m) "github.com/Dreamacro/clash/component/resolver" +func (p *dnsOverHTTPS) resetClient(resetErr error) (client *http.Client, err error) { + p.clientMu.Lock() + defer p.clientMu.Unlock() + + if errors.Is(resetErr, quic.Err0RTTRejected) { + // Reset the TokenStore only if 0-RTT was rejected. + p.resetQUICConfig() +import ( "crypto/tls" + + oldClient := p.client + if oldClient != nil { +func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { "crypto/tls" + if closeErr != nil { + log.Warnln("warning: failed to close the old http client: %v", closeErr) + } +import ( "crypto/tls" + + log.Debugln("re-creating the http client due to %v", resetErr) + p.client, err = p.createClient() + + return p.client, err +} + +// getQUICConfig returns the QUIC config in a thread-safe manner. Note, that +// this method returns a pointer, it is forbidden to change its properties. +func (p *dnsOverHTTPS) getQUICConfig() (c *quic.Config) { + p.quicConfigGuard.Lock() + defer p.quicConfigGuard.Unlock() + + // https://datatracker.ietf.org/doc/html/rfc8484#section-4.1 "fmt" +} + +// resetQUICConfig Re-create the token store to make sure we're not trying to +// use invalid for 0-RTT. +func (p *dnsOverHTTPS) resetQUICConfig() { + p.quicConfigGuard.Lock() + defer p.quicConfigGuard.Unlock() + + p.quicConfig = p.quicConfig.Clone() + p.quicConfig.TokenStore = newQUICTokenStore() +} + +// getClient gets or lazily initializes an HTTP client (and transport) that will +// be used for this DoH resolver. +func (p *dnsOverHTTPS) getClient() (c *http.Client, isCached bool, err error) { + // In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request. "crypto/tls" + + p.clientMu.Lock() + defer p.clientMu.Unlock() + if p.client != nil { + // In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request. "github.com/Dreamacro/clash/component/dialer" + } + // Timeout can be exceeded while waiting for the lock. This happens quite + // often on mobile devices. + elapsed := time.Since(startTime) + if elapsed > maxElapsedTime { + return nil, false, fmt.Errorf("timeout exceeded: %s", elapsed) + } + + log.Debugln("creating a new http client") + p.client, err = p.createClient() + + return p.client, false, err +} + +// createClient creates a new *http.Client instance. The HTTP protocol version + newM := *m "fmt" +// that we'll attempt to establish a QUIC connection when creating the client in +// order to check whether HTTP3 is supported. + newM.Id = 0 + transport, err := p.createTransport() + if err != nil { + return nil, fmt.Errorf("initializing http transport: %w", err) +import ( "crypto/tls" + + client := &http.Client{ + Transport: transport, + Timeout: DefaultTimeout, + newM.Id = 0 "crypto/tls" +import ( "crypto/tls" + + newM.Id = 0 "fmt" + + return p.client, nil +} + +// createTransport initializes an HTTP transport that will be used specifically +// for this DoH resolver. This HTTP transport ensures that the HTTP requests +// will be sent exactly to the IP address got from the bootstrap resolver. Note, +// that this function will first attempt to establish a QUIC connection (if +// HTTP3 is enabled in the upstream options). If this attempt is successful, +// it returns an HTTP3 transport, otherwise it returns the H1/H2 transport. +func (p *dnsOverHTTPS) createTransport() (t http.RoundTripper, err error) { + req, err := dc.newRequest(&newM) "crypto/tls" + &tls.Config{ + req, err := dc.newRequest(&newM) "github.com/Dreamacro/clash/component/dialer" + MinVersion: tls.VersionTLS12, + SessionTicketsDisabled: false, + }) + if err != nil { + for _, v := range p.httpVersions { + nextProtos = append(nextProtos, string(v)) + } + tlsConfig.NextProtos = nextProtos + dialContext := getDialHandler(p.r, p.proxyAdapter) + if err != nil { "fmt" + // connection is established successfully, we'll be using HTTP3 for this + // upstream. + transportH3, err := p.createTransportH3(tlsConfig, dialContext) + if err == nil { + return nil, err package dns - "fmt" + return transportH3, nil + } + + log.Debugln("using HTTP/2 for this upstream: %v", err) + + if !p.supportsHTTP() { + return nil, errors.New("HTTP1/1 and HTTP2 are not supported by this upstream") + } + transport := &http.Transport{ + return nil, err "fmt" import ( +func newDoHClient(url string, r *Resolver, params map[string]string, proxyAdapter string) *dohClient { + DialContext: dialContext, + IdleConnTimeout: transportDefaultIdleConnTimeout, + MaxConnsPerHost: dohMaxConnsPerHost, + MaxIdleConns: dohMaxIdleConns, + // Since we have a custom DialContext, we need to use this field to + // make golang http.Client attempt to use HTTP/2. Otherwise, it would + // only be used when negotiated on the TLS level. + ForceAttemptHTTP2: true, + } + + // Explicitly configure transport to use HTTP/2. +import ( } + // See https://github.com/AdguardTeam/dnsproxy/issues/11. + var transportH2 *http2.Transport + transportH2, err = http2.ConfigureTransports(transport) + if err != nil { + return nil, err + } + // Enable HTTP/2 pings on idle connections. + transportH2.ReadIdleTimeout = transportDefaultReadIdleTimeout + +import ( var conn net.PacketConn +} + +import ( if proxyAdapter == "" { +import ( conn, err = dialer.ListenPacket(ctx, "udp", "") +import ( if err != nil { +import ( return nil, err +import ( } +import ( } else { + +import ( if wrapConn, err := dialContextExtra(ctx, proxyAdapter, "udp", ip, port); err == nil { +import ( if pc, ok := wrapConn.(*wrapPacketConn); ok { +} + +// type check +import ( conn = pc + +import ( } else { +import ( return nil, fmt.Errorf("conn isn't wrapPacketConn") +import ( } +import ( } else { + + if h.closed { + return nil, net.ErrClosed + } + + // Try to use cached connection to the target host if it's available. + resp, err = h.baseTransport.RoundTripOpt(req, http3.RoundTripOpt{OnlyCachedConn: true}) + + if errors.Is(err, http3.ErrNoCachedConn) { + // If there are no cached connection, trigger creating a new one. + resp, err = h.baseTransport.RoundTrip(req) + } + + return resp, err +} + +// type check +var _ io.Closer = (*http3Transport)(nil) + +// Close implements the io.Closer interface for *http3Transport. + if err == nil { "fmt" + if err == nil { "github.com/Dreamacro/clash/component/dialer" - "fmt" + if err == nil { "github.com/Dreamacro/clash/component/resolver" + + h.closed = true + + return h.baseTransport.Close() +} + +// createTransportH3 tries to create an HTTP/3 transport for this upstream. +// We should be able to fall back to H1/H2 in case if HTTP/3 is unavailable or +// if it is too slow. In order to do that, this method will run two probes +// in parallel (one for TLS, the other one for QUIC) and if QUIC is faster it + msg.Id = m.Id "crypto/tls" +func (doh *dnsOverHTTPS) createTransportH3( + msg.Id = m.Id "github.com/Dreamacro/clash/component/dialer" + dialContext dialHandler, +) (roundTripper http.RoundTripper, err error) { + if !doh.supportsH3() { + return + } + + addr, err := doh.probeH3(tlsConfig, dialContext) + if err != nil { + return nil, err + } + + rt := &http3.RoundTripper{ + Dial: func( + ctx context.Context, + + // Ignore the address and always connect to the one that we got + return "github.com/Dreamacro/clash/component/dialer" + _ string, + tlsCfg *tls.Config, + cfg *quic.Config, + ) (c quic.EarlyConnection, err error) { + return doh.dialQuic(ctx, addr, tlsCfg, cfg) + }, + return nil, err "github.com/Dreamacro/clash/component/dialer" + TLSClientConfig: tlsConfig, + QuicConfig: doh.getQUICConfig(), + } + + return &http3Transport{baseTransport: rt}, nil +} + +func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + ip, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } +// newRequest returns a new DoH request given a dns.Msg. "github.com/Dreamacro/clash/component/dialer" + if err != nil { + return nil, err + } +// newRequest returns a new DoH request given a dns.Msg. "github.com/Dreamacro/clash/component/resolver" + IP: net.ParseIP(ip), + Port: portInt, + } + var conn net.PacketConn + if doh.proxyAdapter == "" { + conn, err = dialer.ListenPacket(ctx, "udp", "") +package dns "github.com/Dreamacro/clash/component/resolver" + + return nil, err } } else { + if wrapConn, err := dialContextExtra(ctx, doh.proxyAdapter, "udp", udpAddr.AddrPort().Addr(), port); err == nil { + if pc, ok := wrapConn.(*wrapPacketConn); ok { + conn = pc +func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) { "github.com/Dreamacro/clash/component/resolver" + return nil, fmt.Errorf("conn isn't wrapPacketConn") + } + } else { + "bytes" import ( + "context" "github.com/Dreamacro/clash/component/resolver" +package dns + } + return quic.DialEarlyContext(ctx, conn, &udpAddr, doh.url.Host, tlsCfg, cfg) +} + + buf, err := m.Pack() "bytes" +// upstream. If the test is successful it will return the address that we +// should use to establish the QUIC connections. +func (p *dnsOverHTTPS) probeH3( + tlsConfig *tls.Config, + msg.Id = m.Id "github.com/Dreamacro/clash/component/resolver" +) (addr string, err error) { + // We're using bootstrapped address instead of what's passed to the function + "bytes" "context" + // what IP is actually reachable (when there are v4/v6 addresses). + rawConn, err := dialContext(context.Background(), "udp", p.url.Host) + if err != nil { + return "", fmt.Errorf("failed to dial: %w", err) +import ( "crypto/tls" + "bytes" "context" + "bytes" + _ = rawConn.Close() + + req, err := http.NewRequest(http.MethodPost, dc.url, bytes.NewReader(buf)) "crypto/tls" + if !ok { + return "", fmt.Errorf("not a UDP connection to %s", p.Address()) +import ( "crypto/tls" + + addr = udpConn.RemoteAddr().String() + + // Avoid spending time on probing if this upstream only supports HTTP/3. + if p.supportsH3() && !p.supportsHTTP() { + "bytes" "crypto/tls" - "fmt" + +import ( "crypto/tls" - "github.com/Dreamacro/clash/component/dialer" + // Use a new *tls.Config with empty session cache for probe connections. + "bytes" "crypto/tls" - "github.com/Dreamacro/clash/component/resolver" + "bytes" + // the existing cache. + "bytes" if err != nil { + "bytes" return nil, err + "bytes" } + // Do not expose probe connections to the callbacks that are passed to + // the bootstrap options to avoid side-effects. + // TODO(ameshkov): consider exposing, somehow mark that this is a probe. + probeTLSCfg.VerifyPeerCertificate = nil + probeTLSCfg.VerifyConnection = nil + + // Run probeQUIC and probeTLS in parallel and see which one is faster. + "bytes" if proxyAdapter == "" { + chTLS := make(chan error, 1) + go p.probeQUIC(addr, probeTLSCfg, chQuic) + go p.probeTLS(dialContext, probeTLSCfg, chTLS) + + req.Header.Set("content-type", dotMimeType) "github.com/Dreamacro/clash/component/resolver" - "crypto/tls" + "bytes" } else { + if quicErr != nil { + // QUIC failed, return error since HTTP3 was not preferred. + return "", quicErr "github.com/Dreamacro/clash/component/resolver" - "fmt" +package dns + + // Return immediately, QUIC was faster. + return addr, quicErr + req.Header.Set("accept", dotMimeType) "crypto/tls" + "bytes" "github.com/Dreamacro/clash/component/dialer" + "fmt" + // Return immediately, TLS failed. + "bytes" }, + "bytes" TLSClientConfig: TLCConfig, } + + return "", errors.New("TLS was faster than QUIC, prefer it") } + "context" + +// probeQUIC attempts to establish a QUIC connection to the specified address. +// We run probeQUIC and probeTLS in parallel and see which one is faster. +func (p *dnsOverHTTPS) probeQUIC(addr string, tlsConfig *tls.Config, ch chan error) { + startTime := time.Now() + + timeout := DefaultTimeout + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeout)) + defer cancel() + + "bytes" return &dohClient{ + if err != nil { + "bytes" url: url, + return + } + +func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) { package dns + "context" + + + "context" +import ( + + elapsed := time.Now().Sub(startTime) + log.Debugln("elapsed on establishing a QUIC connection: %s", elapsed) +} + +// probeTLS attempts to establish a TLS connection to the specified address. We +// run probeQUIC and probeTLS in parallel and see which one is faster. +func (p *dnsOverHTTPS) probeTLS(dialContext dialHandler, tlsConfig *tls.Config, ch chan error) { import ( "crypto/tls" + + conn, err := p.tlsDial(dialContext, "tcp", tlsConfig) + if err != nil { + ch <- fmt.Errorf("opening TLS connection: %w", err) + return + } + + // Ignore the error since there's no way we can use it for anything useful. + _ = conn.Close() + + ch <- nil + + elapsed := time.Now().Sub(startTime) + log.Debugln("elapsed on establishing a TLS connection: %s", elapsed) +} + +// supportsH3 returns true if HTTP/3 is supported by this upstream. +func (p *dnsOverHTTPS) supportsH3() (ok bool) { + for _, v := range p.supportedHTTPVersions() { + if v == C.HTTPVersion3 { + return true + } + } + + return false +} + +// supportsHTTP returns true if HTTP/1.1 or HTTP2 is supported by this upstream. +func (p *dnsOverHTTPS) supportsHTTP() (ok bool) { + for _, v := range p.supportedHTTPVersions() { + if v == C.HTTPVersion11 || v == C.HTTPVersion2 { + return true + } + } + + return false +} + +// supportedHTTPVersions returns the list of supported HTTP versions. +func (p *dnsOverHTTPS) supportedHTTPVersions() (v []C.HTTPVersion) { + v = p.httpVersions + if v == nil { + v = DefaultHTTPVersions + } + + return v +} + +// isHTTP3 checks if the *http.Client is an HTTP/3 client. +func isHTTP3(client *http.Client) (ok bool) { + _, ok = client.Transport.(*http3Transport) + + return ok +} + +// tlsDial is basically the same as tls.DialWithDialer, but we will call our own +// dialContext function to get connection. +func (doh *dnsOverHTTPS) tlsDial(dialContext dialHandler, network string, config *tls.Config) (*tls.Conn, error) { + // We're using bootstrapped address instead of what's passed + // to the function. + rawConn, err := dialContext(context.Background(), network, doh.url.Host) + if err != nil { + return nil, err + } + + // We want the timeout to cover the whole process: TCP connection and + // TLS handshake dialTimeout will be used as connection deadLine. + conn := tls.Client(rawConn, config) + + err = conn.SetDeadline(time.Now().Add(dialTimeout)) + if err != nil { + // Must not happen in normal circumstances. + panic(fmt.Errorf("cannot set deadline: %w", err)) + } + + err = conn.Handshake() + if err != nil { + defer conn.Close() + return nil, err + } + + return conn, nil } diff --git a/dns/doq.go b/dns/doq.go index 7807de1cc1f6603c14c9a6aeedcf4518d95f15bf..734d26d05b3a4ed412d933f6be720d7436b1184b 100644 --- a/dns/doq.go +++ b/dns/doq.go @@ -1,242 +1,500 @@ package dns import ( + "context" + "crypto/tls" + "encoding/binary" + "errors" + "fmt" + "net" + "net/netip" + "runtime" + "strconv" +package dns "bytes" +package dns "context" + + "github.com/Dreamacro/clash/component/dialer" + tlsC "github.com/Dreamacro/clash/component/tls" + "github.com/lucas-clemente/quic-go" + +package dns "crypto/tls" +package dns "fmt" +package dns "github.com/Dreamacro/clash/component/dialer" + +package dns "github.com/Dreamacro/clash/component/resolver" package dns + } + // QUICCodeNoError is used when the connection or stream needs to be closed, + // but there is no error to signal. + QUICCodeNoError = quic.ApplicationErrorCode(0) + // QUICCodeInternalError signals that the DoQ implementation encountered + // an internal error and is incapable of pursuing the transaction or the + "sync" package dns + "bytes" package dns package dns + "bytes" package dns + "bytes" import ( package dns + "bytes" "bytes" package dns + "bytes" "context" + // https://pkg.go.dev/github.com/lucas-clemente/quic-go/internal/protocol#MaxKeepAliveInterval. + // + // TODO(ameshkov): Consider making it configurable. + QUICKeepAlivePeriod = time.Second * 20 + DefaultTimeout = time.Second * 5 +) package dns + defer respBuf.Reset() + +// dnsOverQUIC is a struct that implements the Upstream interface for the +// DNS-over-QUIC protocol (spec: https://www.rfc-editor.org/rfc/rfc9250.html). +type dnsOverQUIC struct { + // quicConfig is the QUIC configuration that is used for establishing + "time" "crypto/tls" package dns + "context" "fmt" package dns + "context" "github.com/Dreamacro/clash/component/dialer" + quicConfig *quic.Config + quicConfigGuard sync.Mutex package dns + default: + // re-opened when needed. + conn quic.Connection + connMu sync.RWMutex + + // bytesPool is a *sync.Pool we use to store byte buffers in. These byte + // buffers are used to read responses from the upstream. + bytesPool *sync.Pool + bytesPoolGuard sync.Mutex + + addr string + proxyAdapter string + r *Resolver +} + + "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/component/resolver" +var _ dnsClient = (*dnsOverQUIC)(nil) +// newDoQ returns the DNS-over-QUIC Upstream. + D "github.com/miekg/dns" + doq := &dnsOverQUIC{ + addr: addr, + proxyAdapter: adapter, + r: resolver, + quicConfig: &quic.Config{ + KeepAlivePeriod: QUICKeepAlivePeriod, + TokenStore: newQUICTokenStore(), + }, + } + +) + return doq, nil + "github.com/Dreamacro/clash/component/dialer" package dns + var err error +func (p *dnsOverQUIC) Address() string { return p.addr } +func (p *dnsOverQUIC) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + // When sending queries over a QUIC connection, the DNS Message ID MUST be + // set to zero. + id := m.Id + m.Id = 0 + defer func() { + // Restore the original ID to not break compatibility with proxies. + m.Id = id + if msg != nil { + msg.Id = id + } + }() + + // Check if there was already an active conn before sending the request. + // We'll only attempt to re-connect if there was one. + hasConnection := p.hasConnection() + + // Make the first attempt to send the DNS query. + msg, err = p.exchangeQUIC(ctx, m) + // again. There are several cases where this workaround is necessary to + // make DoQ usable. We need to make 2 attempts in the case when the +var bytesPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} import ( "bytes" "context" "crypto/tls" +var bytesPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} "fmt" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" -import ( +type quicClient struct { import ( + "bytes" + + if err != nil { +type quicClient struct { package dns + // closed and signal about an internal error. + p.closeConnWithError(err) import ( + "bytes" +type quicClient struct { import ( +} + +// Exchange implements the Upstream interface for *dnsOverQUIC. +func (p *dnsOverQUIC) Exchange(m *D.Msg) (msg *D.Msg, err error) { + return p.ExchangeContext(context.Background(), m) +} + +// Close implements the Upstream interface for *dnsOverQUIC. +func (p *dnsOverQUIC) Close() (err error) { + p.connMu.Lock() + defer p.connMu.Unlock() + + runtime.SetFinalizer(p, nil) + + if p.conn != nil { + addr string import ( } + return err + "github.com/Dreamacro/clash/component/dialer" -import ( + addr string "context" -import ( + addr string "crypto/tls" + sync.RWMutex // protects connection and bytesPool + addr string "github.com/Dreamacro/clash/component/dialer" +func newDOQ(r *Resolver, addr, proxyAdapter string) *quicClient { import ( - "fmt" + "github.com/Dreamacro/clash/component/resolver" + return nil, err import ( - "github.com/Dreamacro/clash/component/dialer" + "bytes" + + var buf []byte + buf, err = msg.Pack() if err != nil { - return nil, fmt.Errorf("failed to open new stream to %s", dc.addr) + return nil, fmt.Errorf("failed to pack DNS message for DoQ: %w", err) } + var stream quic.Stream + r *Resolver "bytes" -package dns if err != nil { return nil, err } - "bytes" + import ( + "context" if err != nil { - "bytes" + return dc.ExchangeContext(context.Background(), m) } // The client MUST send the DNS query over the selected stream, and MUST // indicate through the STREAM FIN mechanism that no further data will - // be sent on that stream. - "bytes" + r *Resolver "fmt" + // write-direction of the stream, but does not prevent reading from it. _ = stream.Close() - "bytes" + r *Resolver "github.com/Dreamacro/clash/component/resolver" - "context" +} + + connection quic.Connection - "context" + connection quic.Connection package dns + connection quic.Connection + binary.BigEndian.PutUint16(m, uint16(len(b))) + copy(m[2:], b) + + connection quic.Connection "context" + "github.com/Dreamacro/clash/component/dialer" - if err != nil && n == 0 { + + "bytes" + "crypto/tls" + // stream.Close() -- closes the write-direction of the stream. -import ( + "bytes" + "github.com/Dreamacro/clash/component/dialer" + respBuf := bytesPool.Get().(*bytes.Buffer) +} + + "context" - "bytes" + "context" +package dns + "context" + + proxyAdapter string import ( - "github.com/Dreamacro/clash/component/resolver" - "bytes" -import ( + proxyAdapter string "bytes" + err = reply.Unpack(respBuf.Bytes()) + return reply, nil - "github.com/Dreamacro/clash/component/dialer" +func isActive(s quic.Connection) bool { + "context" - "fmt" + "github.com/Dreamacro/clash/component/dialer" + }, - "context" "github.com/Dreamacro/clash/component/dialer" +package dns + } + + case <-s.Context().Done(): +} + + return false + default: + return true + udp net.PacketConn import ( + udp net.PacketConn "bytes" + } -// getConnection - opens or returns an existing quic.Connection -// useCached - if true and cached connection exists, return it right away // otherwise - forcibly creates a new connection + func (dc *quicClient) getConnection(ctx context.Context) (quic.Connection, error) { + var connection quic.Connection + dc.RLock() + + connection = dc.connection + } if connection != nil && isActive(connection) { + dc.RUnlock() + return connection, nil } + p.connMu.RUnlock() + + p.connMu.Lock() + defer p.connMu.Unlock() + + "github.com/Dreamacro/clash/component/dialer" + "fmt" + "bytes" import ( + "github.com/Dreamacro/clash/component/resolver" + "bytes" - "fmt" +import ( "bytes" + defer dc.Unlock() - connection = dc.connection + + if connection != nil { +} + + if isActive(connection) { + return connection, nil + p.connMu.Lock() + defer p.connMu.Unlock() + + } else { +} + + _ = connection.CloseWithError(quic.ApplicationErrorCode(0), "") + } - } var err error + connection, err = dc.openConnection(ctx) + dc.connection = connection + + return connection, err } + func (dc *quicClient) openConnection(ctx context.Context) (quic.Connection, error) { + if dc.udp != nil { + _ = dc.udp.Close() +} import ( +} "bytes" + tlsConfig := tlsC.GetGlobalFingerprintTLCConfig( + &tls.Config{ +} + + InsecureSkipVerify: false, + NextProtos: []string{ + NextProtoDQ, + }, + + SessionTicketsDisabled: false, + }) quicConfig := &quic.Config{ + } + + ConnectionIDLength: 12, + HandshakeIdleTimeout: time.Second * 8, -package dns +import ( -package dns + if err != nil { + return nil, err + } +import ( package dns -package dns +import ( - } +} -package dns +import ( import ( -package dns +import ( "bytes" + tlsConfig := tlsC.GetGlobalFingerprintTLCConfig( + &tls.Config{ + "github.com/Dreamacro/clash/component/resolver" package dns + NextProtos: []string{ + NextProtoDQ, + }, + SessionTicketsDisabled: false, + }) +import ( "context" -package dns +import ( "crypto/tls" + // what IP is actually reachable (when there're v4/v6 addresses). + ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + rawConn, err := getDialHandler(doq.r, doq.proxyAdapter)(ctx, "udp", doq.addr) + if err != nil { +import ( tlsC "github.com/Dreamacro/clash/component/tls" - "fmt" + } + // It's never actually used + addr: addr, +import ( package dns - "github.com/Dreamacro/clash/component/dialer" +import ( import ( - "github.com/Dreamacro/clash/component/resolver" + "sync" + if !ok { + return nil, fmt.Errorf("failed to open connection to %s", doq.addr) +import ( "bytes" import ( - "bytes" + D "github.com/miekg/dns" +import ( package dns - "github.com/Dreamacro/clash/component/resolver" + "github.com/Dreamacro/clash/component/dialer" if err != nil { return nil, err } p, err := strconv.Atoi(port) - "github.com/lucas-clemente/quic-go" +import ( package dns + "github.com/Dreamacro/clash/component/resolver" +import ( -package dns + r: r, package dns - udp, err = dialer.ListenPacket(ctx, "udp", "") if err != nil { return nil, err } } else { + ipAddr, err := netip.ParseAddr(ip) "github.com/lucas-clemente/quic-go" - "fmt" + "bytes" + return nil, err + } + + conn, err := dialContextExtra(ctx, doq.proxyAdapter, "udp", ipAddr, port) if err != nil { return nil, err } @@ -246,37 +497,184 @@ udp = wrapConn } + ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + host, _, err := net.SplitHostPort(doq.addr) + if err != nil { + return nil, err + } + + conn, err = quic.DialContext(ctx, udp, &udpAddr, host, tlsConfig, doq.getQUICConfig()) + if err != nil { + return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err) + } + + return conn, nil +} + +// closeConnWithError closes the active connection with error to make sure that +// new queries were processed in another connection. We can do that in the case + proxyAdapter: proxyAdapter, package dns +func (p *dnsOverQUIC) closeConnWithError(err error) { + p.connMu.Lock() addr string + + if p.conn == nil { + // Do nothing, there's no active conn anyways. + return + } + + code := QUICCodeNoError if err != nil { + code = QUICCodeInternalError + } + + if errors.Is(err, quic.Err0RTTRejected) { + // Reset the TokenStore only if 0-RTT was rejected. + p.resetQUICConfig() + } + + } package dns + if err != nil { + } import ( + "bytes" } +import ( + "github.com/Dreamacro/clash/component/dialer" + +// readMsg reads the incoming DNS message from the QUIC stream. +func (p *dnsOverQUIC) readMsg(stream quic.Stream) (m *D.Msg, err error) { + pool := p.getBytesPool() + bufPtr := pool.Get().(*[]byte) + + defer pool.Put(bufPtr) + + respBuf := *bufPtr + n, err := stream.Read(respBuf) + if err != nil && n == 0 { +func (dc *quicClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { package dns + } + // All DNS messages (queries and responses) sent over DoQ connections MUST + // be encoded as a 2-octet length field followed by the message content as +func (dc *quicClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { "bytes" + // IMPORTANT: Note, that we ignore this prefix here as this implementation + // does not support receiving multiple messages over a single connection. + m = new(D.Msg) + err = m.Unpack(respBuf[2:]) + if err != nil { + return nil, fmt.Errorf("unpacking response from %s: %w", p.Address(), err) + } + + return m, nil +} + + return dc.ExchangeContext(context.Background(), m) package dns + return dc.ExchangeContext(context.Background(), m) +func newQUICTokenStore() (s quic.TokenStore) { + // You can read more on address validation here: + return dc.ExchangeContext(context.Background(), m) "context" + // Setting maxOrigins to 1 and tokensPerOrigin to 10 assuming that this is + // more than enough for the way we use it (one connection per upstream). + return quic.NewLRUTokenStore(1, 10) } +// isQUICRetryError checks the error and determines whether it may signal that +// we should re-create the QUIC connection. This requirement is caused by +func (dc *quicClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { package dns +func (dc *quicClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { +func isQUICRetryError(err error) (ok bool) { + var qAppErr *quic.ApplicationError + if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { +func (dc *quicClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { "crypto/tls" + // and we try to use the same connection on the client-side. It seems, + // that the old connections aren't closed immediately on the server-side + // and that's why one can run into this. + // In addition to that, quic-go HTTP3 client implementation does not + stream, err := dc.openStream(ctx) package dns + stream, err := dc.openStream(ctx) + return true + } + + var qIdleErr *quic.IdleTimeoutError + if errors.As(err, &qIdleErr) { + // This error means that the connection was closed due to being idle. + // In this case we should forcibly re-create the QUIC connection. + stream, err := dc.openStream(ctx) "fmt" import ( + _ = dc.udp.Close() + return true + } + + stream, err := dc.openStream(ctx) "github.com/Dreamacro/clash/component/resolver" + if errors.As(err, &resetErr) { + // A stateless reset is sent when a server receives a QUIC packet that + // it doesn't know how to decrypt. For instance, it may happen when + // the server was recently rebooted. We should reconnect and try again + if err != nil { "bytes" + "crypto/tls" } + var qTransportError *quic.TransportError + if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError { + // A transport error with the NO_ERROR error code could be sent by the + // server when it considers that it's time to close the connection. + // For example, Google DNS eventually closes an active connection with + // the NO_ERROR code and "Connection max age expired" message: + return nil, fmt.Errorf("failed to open new stream to %s", dc.addr) package dns + "crypto/tls" + } + + proxyAdapter: proxyAdapter, "github.com/Dreamacro/clash/component/dialer" + // This error happens when we try to establish a 0-RTT connection with + // a token the server is no more aware of. This can be reproduced by + // restarting the QUIC server (it will clear its tokens cache). The + // next connection attempt will return this error until the client's + // tokens cache is purged. + return true + } + + return false +} + +func getDialHandler(r *Resolver, proxyAdapter string) dialHandler { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + ip, err := r.ResolveIP(host) + if err != nil { + return nil, err + } + "bytes" "net" + return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port), dialer.WithDirect()) + "fmt" "github.com/Dreamacro/clash/component/resolver" + return dialContextExtra(ctx, proxyAdapter, network, ip.Unmap(), port, dialer.WithDirect()) + } + } } diff --git a/dns/resolver.go b/dns/resolver.go index 84a38034da1d722b6e1fec5f8595aa313a50a8da..1184c2e7fd421d8f53cb0be57a68fe5c0c6d7e25 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -356,6 +356,8 @@ Interface *atomic.String ProxyAdapter string Params map[string]string + case ipv6s, open := <-ch: + "context" type FallbackFilter struct { diff --git a/dns/util.go b/dns/util.go index 50d9decd182d321a76489479f3209a54aa5d83df..17e4f5cf87f603aa93b5da6c439f6ec69317c907 100644 --- a/dns/util.go +++ b/dns/util.go @@ -20,6 +20,11 @@ D "github.com/miekg/dns" ) package dns + ret := []dnsClient{} + MaxMsgSize = 65535 +) + +package dns "strings" var ttl uint32 switch { @@ -60,15 +65,20 @@ ret := []dnsClient{} for _, s := range servers { switch s.Net { case "https": +package dns "context" - "net/netip" + "fmt" continue case "dhcp": ret = append(ret, newDHCPClient(s.Addr)) continue case "quic": + if doq, err := newDoQ(resolver, s.Addr, s.ProxyAdapter); err == nil { + ret = append(ret, doq) + }else{ +package dns "crypto/tls" -import ( + } continue }