github.com/yaling888/clash@v1.53.0/dns/client.go (about)

     1  package dns
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"math/rand/v2"
     8  	"net"
     9  	"net/netip"
    10  	"strings"
    11  	"time"
    12  
    13  	D "github.com/miekg/dns"
    14  
    15  	"github.com/yaling888/clash/component/dialer"
    16  	"github.com/yaling888/clash/component/resolver"
    17  )
    18  
    19  var _ dnsClient = (*client)(nil)
    20  
    21  type client struct {
    22  	*D.Client
    23  	r      *Resolver
    24  	port   string
    25  	host   string
    26  	iface  string
    27  	proxy  string
    28  	ip     string
    29  	lan    bool
    30  	source string
    31  }
    32  
    33  func (c *client) IsLan() bool {
    34  	return c.lan
    35  }
    36  
    37  func (c *client) Exchange(m *D.Msg) (*rMsg, error) {
    38  	return c.ExchangeContext(context.Background(), m)
    39  }
    40  
    41  func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*rMsg, error) {
    42  	var err error
    43  	if c.ip == "" {
    44  		if c.r == nil {
    45  			return nil, fmt.Errorf("dns %s not a valid ip", c.host)
    46  		} else {
    47  			var ips []netip.Addr
    48  			ips, err = resolver.LookupIPByResolver(context.Background(), c.host, c.r)
    49  			if err != nil {
    50  				return nil, fmt.Errorf("use default dns resolve failed: %w", err)
    51  			} else if len(ips) == 0 {
    52  				return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, c.host)
    53  			}
    54  			ip := ips[rand.IntN(len(ips))]
    55  			c.ip = ip.String()
    56  			c.lan = ip.IsLoopback() || ip.IsPrivate()
    57  		}
    58  	}
    59  
    60  	network := "udp"
    61  	if strings.HasPrefix(c.Client.Net, "tcp") {
    62  		network = "tcp"
    63  	}
    64  
    65  	var (
    66  		options []dialer.Option
    67  		conn    net.Conn
    68  		proxy   = c.proxy
    69  		msg     = &rMsg{Source: c.source, Lan: c.lan}
    70  	)
    71  
    72  	if p, ok := resolver.GetProxy(ctx); ok && !c.lan {
    73  		proxy = p
    74  	}
    75  
    76  	if c.iface != "" {
    77  		options = append(options, dialer.WithInterface(c.iface))
    78  	}
    79  
    80  	if proxy != "" {
    81  		msg.Source += "(" + proxy + ")"
    82  		conn, err = dialContextByProxyOrInterface(ctx, network, netip.MustParseAddr(c.ip), c.port, proxy, options...)
    83  	} else {
    84  		conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(c.ip, c.port), options...)
    85  	}
    86  
    87  	if err != nil {
    88  		return msg, err
    89  	}
    90  
    91  	if c.Client.Net == "tcp-tls" {
    92  		conn = tls.Client(conn, c.TLSConfig)
    93  	}
    94  
    95  	co := &D.Conn{
    96  		Conn:         conn,
    97  		UDPSize:      c.Client.UDPSize,
    98  		TsigSecret:   c.Client.TsigSecret,
    99  		TsigProvider: c.Client.TsigProvider,
   100  	}
   101  
   102  	defer co.Close()
   103  
   104  	// miekg/dns ExchangeContext doesn't respond to context cancel.
   105  	// this is a workaround
   106  	type result struct {
   107  		msg *D.Msg
   108  		err error
   109  	}
   110  
   111  	ch := make(chan result, 1)
   112  
   113  	go func() {
   114  		msg1, _, err1 := c.Client.ExchangeWithConn(m, co)
   115  		ch <- result{msg1, err1}
   116  	}()
   117  
   118  	select {
   119  	case <-ctx.Done():
   120  		return msg, ctx.Err()
   121  	case ret := <-ch:
   122  		msg.Msg = ret.msg
   123  		return msg, ret.err
   124  	}
   125  }
   126  
   127  func newClient(nw, addr, proxy, iface string, dhcp bool, r *Resolver) *client {
   128  	host, port, _ := net.SplitHostPort(addr)
   129  	var (
   130  		ip  string
   131  		lan bool
   132  	)
   133  	if a, err := netip.ParseAddr(host); err == nil {
   134  		ip = host
   135  		lan = a.IsLoopback() || a.IsPrivate()
   136  	}
   137  
   138  	var timeout time.Duration
   139  	if proxy != "" {
   140  		timeout = proxyTimeout
   141  	} else {
   142  		timeout = resolver.DefaultDNSTimeout
   143  	}
   144  
   145  	clientNet := nw
   146  	if dhcp {
   147  		clientNet = "dhcp"
   148  	} else if clientNet == "tcp-tls" {
   149  		clientNet = "tls"
   150  	}
   151  	if clientNet != "" {
   152  		clientNet += "://"
   153  	}
   154  	source := clientNet + addr
   155  
   156  	return &client{
   157  		Client: &D.Client{
   158  			Net: nw,
   159  			TLSConfig: &tls.Config{
   160  				ServerName: host,
   161  			},
   162  			UDPSize: 4096,
   163  			Timeout: timeout,
   164  		},
   165  		port:   port,
   166  		host:   host,
   167  		ip:     ip,
   168  		iface:  iface,
   169  		proxy:  proxy,
   170  		source: source,
   171  		lan:    lan,
   172  		r:      r,
   173  	}
   174  }