github.com/kelleygo/clashcore@v1.0.2/dns/dhcp.go (about)

     1  //go:build !(android && cmfa)
     2  
     3  package dns
     4  
     5  import (
     6  	"context"
     7  	"net"
     8  	"net/netip"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/kelleygo/clashcore/component/dhcp"
    14  	"github.com/kelleygo/clashcore/component/iface"
    15  	D "github.com/miekg/dns"
    16  )
    17  
    18  const (
    19  	IfaceTTL    = time.Second * 20
    20  	DHCPTTL     = time.Hour
    21  	DHCPTimeout = time.Minute
    22  )
    23  
    24  type dhcpClient struct {
    25  	ifaceName string
    26  
    27  	lock            sync.Mutex
    28  	ifaceInvalidate time.Time
    29  	dnsInvalidate   time.Time
    30  
    31  	ifaceAddr netip.Prefix
    32  	done      chan struct{}
    33  	clients   []dnsClient
    34  	err       error
    35  }
    36  
    37  var _ dnsClient = (*dhcpClient)(nil)
    38  
    39  // Address implements dnsClient
    40  func (d *dhcpClient) Address() string {
    41  	addrs := make([]string, 0)
    42  	for _, c := range d.clients {
    43  		addrs = append(addrs, c.Address())
    44  	}
    45  	return strings.Join(addrs, ",")
    46  }
    47  
    48  func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
    49  	clients, err := d.resolve(ctx)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	msg, _, err = batchExchange(ctx, clients, m)
    55  	return
    56  }
    57  
    58  func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) {
    59  	d.lock.Lock()
    60  
    61  	invalidated, err := d.invalidate()
    62  	if err != nil {
    63  		d.err = err
    64  	} else if invalidated {
    65  		done := make(chan struct{})
    66  
    67  		d.done = done
    68  
    69  		go func() {
    70  			ctx, cancel := context.WithTimeout(context.Background(), DHCPTimeout)
    71  			defer cancel()
    72  
    73  			var res []dnsClient
    74  			dns, err := dhcp.ResolveDNSFromDHCP(ctx, d.ifaceName)
    75  			// dns never empty if err is nil
    76  			if err == nil {
    77  				nameserver := make([]NameServer, 0, len(dns))
    78  				for _, item := range dns {
    79  					nameserver = append(nameserver, NameServer{
    80  						Addr:      net.JoinHostPort(item.String(), "53"),
    81  						Interface: d.ifaceName,
    82  					})
    83  				}
    84  
    85  				res = transform(nameserver, nil)
    86  			}
    87  
    88  			d.lock.Lock()
    89  			defer d.lock.Unlock()
    90  
    91  			close(done)
    92  
    93  			d.done = nil
    94  			d.clients = res
    95  			d.err = err
    96  		}()
    97  	}
    98  
    99  	d.lock.Unlock()
   100  
   101  	for {
   102  		d.lock.Lock()
   103  
   104  		res, err, done := d.clients, d.err, d.done
   105  
   106  		d.lock.Unlock()
   107  
   108  		// initializing
   109  		if res == nil && err == nil {
   110  			select {
   111  			case <-done:
   112  				continue
   113  			case <-ctx.Done():
   114  				return nil, ctx.Err()
   115  			}
   116  		}
   117  
   118  		// dirty return
   119  		return res, err
   120  	}
   121  }
   122  
   123  func (d *dhcpClient) invalidate() (bool, error) {
   124  	if time.Now().Before(d.ifaceInvalidate) {
   125  		return false, nil
   126  	}
   127  
   128  	d.ifaceInvalidate = time.Now().Add(IfaceTTL)
   129  
   130  	ifaceObj, err := iface.ResolveInterface(d.ifaceName)
   131  	if err != nil {
   132  		return false, err
   133  	}
   134  
   135  	addr, err := ifaceObj.PickIPv4Addr(netip.Addr{})
   136  	if err != nil {
   137  		return false, err
   138  	}
   139  
   140  	if time.Now().Before(d.dnsInvalidate) && d.ifaceAddr == addr {
   141  		return false, nil
   142  	}
   143  
   144  	d.dnsInvalidate = time.Now().Add(DHCPTTL)
   145  	d.ifaceAddr = addr
   146  
   147  	return d.done == nil, nil
   148  }
   149  
   150  func newDHCPClient(ifaceName string) *dhcpClient {
   151  	return &dhcpClient{ifaceName: ifaceName}
   152  }