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 }