github.com/chwjbn/xclash@v0.2.0/dns/doh.go (about)

     1  package dns
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  
    10  	"github.com/chwjbn/xclash/component/dialer"
    11  	"github.com/chwjbn/xclash/component/resolver"
    12  
    13  	D "github.com/miekg/dns"
    14  )
    15  
    16  const (
    17  	// dotMimeType is the DoH mimetype that should be used.
    18  	dotMimeType = "application/dns-message"
    19  )
    20  
    21  type dohClient struct {
    22  	url       string
    23  	transport *http.Transport
    24  }
    25  
    26  func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) {
    27  	return dc.ExchangeContext(context.Background(), m)
    28  }
    29  
    30  func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
    31  	// https://datatracker.ietf.org/doc/html/rfc8484#section-4.1
    32  	// In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request.
    33  	newM := *m
    34  	newM.Id = 0
    35  	req, err := dc.newRequest(&newM)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  
    40  	req = req.WithContext(ctx)
    41  	msg, err = dc.doRequest(req)
    42  	if err == nil {
    43  		msg.Id = m.Id
    44  	}
    45  	return
    46  }
    47  
    48  // newRequest returns a new DoH request given a dns.Msg.
    49  func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) {
    50  	buf, err := m.Pack()
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	req, err := http.NewRequest(http.MethodPost, dc.url, bytes.NewReader(buf))
    56  	if err != nil {
    57  		return req, err
    58  	}
    59  
    60  	req.Header.Set("content-type", dotMimeType)
    61  	req.Header.Set("accept", dotMimeType)
    62  	return req, nil
    63  }
    64  
    65  func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) {
    66  	client := &http.Client{Transport: dc.transport}
    67  	resp, err := client.Do(req)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	defer resp.Body.Close()
    72  
    73  	buf, err := io.ReadAll(resp.Body)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	msg = &D.Msg{}
    78  	err = msg.Unpack(buf)
    79  	return msg, err
    80  }
    81  
    82  func newDoHClient(url string, r *Resolver) *dohClient {
    83  	return &dohClient{
    84  		url: url,
    85  		transport: &http.Transport{
    86  			ForceAttemptHTTP2: true,
    87  			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
    88  				host, port, err := net.SplitHostPort(addr)
    89  				if err != nil {
    90  					return nil, err
    91  				}
    92  
    93  				ip, err := resolver.ResolveIPWithResolver(host, r)
    94  				if err != nil {
    95  					return nil, err
    96  				}
    97  
    98  				return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port))
    99  			},
   100  		},
   101  	}
   102  }