github.com/igoogolx/clash@v1.19.8/dns/doh.go (about)

     1  package dns
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/tls"
     7  	C "github.com/igoogolx/clash/constant"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"strconv"
    12  
    13  	"github.com/igoogolx/clash/component/dialer"
    14  	D "github.com/miekg/dns"
    15  )
    16  
    17  const (
    18  	// dotMimeType is the DoH mimetype that should be used.
    19  	dotMimeType = "application/dns-message"
    20  )
    21  
    22  type dohClient struct {
    23  	url       string
    24  	transport *http.Transport
    25  }
    26  
    27  func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) {
    28  	return dc.ExchangeContext(context.Background(), m)
    29  }
    30  
    31  func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
    32  	// https://datatracker.ietf.org/doc/html/rfc8484#section-4.1
    33  	// In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request.
    34  	newM := *m
    35  	newM.Id = 0
    36  	req, err := dc.newRequest(&newM)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  
    41  	req = req.WithContext(ctx)
    42  	msg, err = dc.doRequest(req)
    43  	if err == nil {
    44  		msg.Id = m.Id
    45  	}
    46  	return
    47  }
    48  
    49  // newRequest returns a new DoH request given a dns.Msg.
    50  func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) {
    51  	buf, err := m.Pack()
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	req, err := http.NewRequest(http.MethodPost, dc.url, bytes.NewReader(buf))
    57  	if err != nil {
    58  		return req, err
    59  	}
    60  
    61  	req.Header.Set("content-type", dotMimeType)
    62  	req.Header.Set("accept", dotMimeType)
    63  	return req, nil
    64  }
    65  
    66  func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) {
    67  	client := &http.Client{Transport: dc.transport}
    68  	resp, err := client.Do(req)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	defer resp.Body.Close()
    73  
    74  	buf, err := io.ReadAll(resp.Body)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	msg = &D.Msg{}
    79  	err = msg.Unpack(buf)
    80  	return msg, err
    81  }
    82  
    83  func newDoHClient(url, iface string, getDialer func() (C.Proxy, error)) *dohClient {
    84  	return &dohClient{
    85  		url: url,
    86  		transport: &http.Transport{
    87  			ForceAttemptHTTP2: true,
    88  			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
    89  				host, port, err := net.SplitHostPort(addr)
    90  				if err != nil {
    91  					return nil, err
    92  				}
    93  
    94  				numPort, err := strconv.Atoi(port)
    95  
    96  				if err != nil {
    97  					return nil, err
    98  				}
    99  
   100  				connDial, err := getDialer()
   101  				if err != nil {
   102  					return nil, err
   103  				}
   104  
   105  				return connDial.DialContext(ctx, &C.Metadata{
   106  					NetWork: C.TCP,
   107  					SrcIP:   nil,
   108  					DstIP:   nil,
   109  					SrcPort: 0,
   110  					DstPort: C.Port(numPort),
   111  					Host:    host,
   112  				}, dialer.WithInterface(iface))
   113  
   114  			},
   115  			TLSClientConfig: &tls.Config{
   116  				// alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6
   117  				NextProtos: []string{"dns"},
   118  			},
   119  		},
   120  	}
   121  }