From 828fd30dc359269f8de32c3db5b8fb2cefbebe0f Mon Sep 17 00:00:00 2001 From: H1JK Date: Thu, 15 Jan 2026 22:44:57 +0800 Subject: [PATCH] chore: support connection reuse for DoT --- config/config.go | 2 +- dns/client.go | 34 ++-------- dns/dot.go | 164 +++++++++++++++++++++++++++++++++++++++++++++++ dns/util.go | 2 + 4 files changed, 171 insertions(+), 31 deletions(-) create mode 100644 dns/dot.go diff --git a/config/config.go b/config/config.go index 68b2b1ee..ab2dbdfc 100644 --- a/config/config.go +++ b/config/config.go @@ -1189,7 +1189,7 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns. dnsNetType = "tcp" // TCP case "tls": addr, err = hostWithDefaultPort(u.Host, "853") - dnsNetType = "tcp-tls" // DNS over TLS + dnsNetType = "tls" // DNS over TLS case "http", "https": addr, err = hostWithDefaultPort(u.Host, "443") dnsNetType = "https" // DNS over HTTPS diff --git a/dns/client.go b/dns/client.go index ade44e08..b3d24f14 100644 --- a/dns/client.go +++ b/dns/client.go @@ -7,20 +7,17 @@ import ( "strings" "time" - "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" - "github.com/metacubex/tls" D "github.com/miekg/dns" ) type client struct { - port string - host string - dialer *dnsDialer - schema string - skipCertVerify bool + port string + host string + dialer *dnsDialer + schema string } var _ dnsClient = (*client)(nil) @@ -43,23 +40,6 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) } defer conn.Close() - if c.schema == "tls" { - tlsConfig, err := ca.GetTLSConfig(ca.Option{ - TLSConfig: &tls.Config{ - ServerName: c.host, - InsecureSkipVerify: c.skipCertVerify, - }, - }) - if err != nil { - return nil, err - } - tlsConn := tls.Client(conn, tlsConfig) - if err := tlsConn.HandshakeContext(ctx); err != nil { - return nil, err - } - conn = tlsConn - } - // miekg/dns ExchangeContext doesn't respond to context cancel. // this is a workaround type result struct { @@ -117,12 +97,6 @@ func newClient(addr string, resolver *Resolver, netType string, params map[strin } if strings.HasPrefix(netType, "tcp") { c.schema = "tcp" - if strings.HasSuffix(netType, "tls") { - c.schema = "tls" - } - } - if params["skip-cert-verify"] == "true" { - c.skipCertVerify = true } return c } diff --git a/dns/dot.go b/dns/dot.go new file mode 100644 index 00000000..a4ffe461 --- /dev/null +++ b/dns/dot.go @@ -0,0 +1,164 @@ +package dns + +import ( + "context" + "fmt" + "net" + "runtime" + "sync" + "time" + + "github.com/metacubex/mihomo/common/deque" + "github.com/metacubex/mihomo/component/ca" + C "github.com/metacubex/mihomo/constant" + + "github.com/metacubex/tls" + D "github.com/miekg/dns" +) + +const maxOldDotConns = 8 + +type dnsOverTLS struct { + port string + host string + dialer *dnsDialer + skipCertVerify bool + + access sync.Mutex + connections deque.Deque[net.Conn] // LIFO +} + +var _ dnsClient = (*dnsOverTLS)(nil) + +// Address implements dnsClient +func (t *dnsOverTLS) Address() string { + return fmt.Sprintf("tls://%s", net.JoinHostPort(t.host, t.port)) +} + +func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { + // miekg/dns ExchangeContext doesn't respond to context cancel. + // this is a workaround + type result struct { + msg *D.Msg + err error + } + ch := make(chan result, 1) + + go func() { + var msg *D.Msg + var err error + defer func() { ch <- result{msg, err} }() + for { // retry loop; only retry when reusing old conn + err = ctx.Err() // check context first + if err != nil { + return + } + + var conn net.Conn + isOldConn := true + + t.access.Lock() + if t.connections.Len() > 0 { + conn = t.connections.PopBack() + } + t.access.Unlock() + + if conn == nil { + conn, err = t.dialContext(ctx) + if err != nil { + return + } + isOldConn = false + } + + dClient := &D.Client{ + UDPSize: 4096, + Timeout: 5 * time.Second, + } + dConn := &D.Conn{ + Conn: conn, + UDPSize: dClient.UDPSize, + } + + msg, _, err = dClient.ExchangeWithConn(m, dConn) + if err != nil { + _ = conn.Close() + conn = nil + if isOldConn { // retry + continue + } + return + } + + t.access.Lock() + if t.connections.Len() >= maxOldDotConns { + oldConn := t.connections.PopFront() + go oldConn.Close() // close in a new goroutine, not blocking the current task + } + t.connections.PushBack(conn) + t.access.Unlock() + return + } + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case ret := <-ch: + return ret.msg, ret.err + } +} + +func (t *dnsOverTLS) dialContext(ctx context.Context) (net.Conn, error) { + conn, err := t.dialer.DialContext(ctx, "tcp", net.JoinHostPort(t.host, t.port)) + if err != nil { + return nil, err + } + + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: t.host, + InsecureSkipVerify: t.skipCertVerify, + }, + }) + if err != nil { + return nil, err + } + tlsConn := tls.Client(conn, tlsConfig) + if err = tlsConn.HandshakeContext(ctx); err != nil { + return nil, err + } + conn = tlsConn + + return conn, nil +} + +func (t *dnsOverTLS) ResetConnection() { + t.access.Lock() + for t.connections.Len() > 0 { + oldConn := t.connections.PopFront() + go oldConn.Close() // close in a new goroutine, not blocking the current task + } + t.access.Unlock() +} + +func (t *dnsOverTLS) Close() error { + runtime.SetFinalizer(t, nil) + t.ResetConnection() + return nil +} + +func newDoTClient(addr string, resolver *Resolver, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *dnsOverTLS { + host, port, _ := net.SplitHostPort(addr) + c := &dnsOverTLS{ + port: port, + host: host, + dialer: newDNSDialer(resolver, proxyAdapter, proxyName), + } + c.connections.SetBaseCap(maxOldDotConns) + if params["skip-cert-verify"] == "true" { + c.skipCertVerify = true + } + runtime.SetFinalizer(c, (*dnsOverTLS).Close) + return c +} diff --git a/dns/util.go b/dns/util.go index d46cd65c..b7082818 100644 --- a/dns/util.go +++ b/dns/util.go @@ -95,6 +95,8 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { for _, s := range servers { var c dnsClient switch s.Net { + case "tls": + c = newDoTClient(s.Addr, resolver, s.Params, s.ProxyAdapter, s.ProxyName) case "https": c = newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName) case "dhcp":