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 }