clash/dns/doh.go

118 lines
2.6 KiB
Go
Raw Normal View History

2019-06-28 12:29:08 +08:00
package dns
import (
"bytes"
"context"
"crypto/tls"
"fmt"
2021-10-09 20:35:06 +08:00
"io"
"math/rand"
"net"
2019-06-28 12:29:08 +08:00
"net/http"
2020-02-09 17:02:48 +08:00
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
2020-02-09 17:02:48 +08:00
2019-06-28 12:29:08 +08:00
D "github.com/miekg/dns"
)
const (
// dotMimeType is the DoH mimetype that should be used.
dotMimeType = "application/dns-message"
)
type dohClient struct {
url string
transport *http.Transport
2019-06-28 12:29:08 +08:00
}
func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) {
return dc.ExchangeContext(context.Background(), m)
}
func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
// https://datatracker.ietf.org/doc/html/rfc8484#section-4.1
// In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request.
newM := *m
newM.Id = 0
req, err := dc.newRequest(&newM)
2019-06-28 12:29:08 +08:00
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
msg, err = dc.doRequest(req)
2021-10-11 21:05:38 +08:00
if err == nil {
msg.Id = m.Id
}
return
2019-06-28 12:29:08 +08:00
}
// newRequest returns a new DoH request given a dns.Msg.
func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) {
buf, err := m.Pack()
if err != nil {
return nil, err
}
2020-06-01 13:43:26 +08:00
req, err := http.NewRequest(http.MethodPost, dc.url, bytes.NewReader(buf))
2019-06-28 12:29:08 +08:00
if err != nil {
return req, err
}
req.Header.Set("content-type", dotMimeType)
req.Header.Set("accept", dotMimeType)
return req, nil
}
func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) {
client := &http.Client{Transport: dc.transport}
2019-06-28 12:29:08 +08:00
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
2021-10-09 20:35:06 +08:00
buf, err := io.ReadAll(resp.Body)
2019-06-28 12:29:08 +08:00
if err != nil {
return nil, err
}
msg = &D.Msg{}
err = msg.Unpack(buf)
return msg, err
}
func newDoHClient(url, iface string, r *Resolver) *dohClient {
return &dohClient{
url: url,
transport: &http.Transport{
2020-06-01 13:43:26 +08:00
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
ips, err := resolver.LookupIPWithResolver(ctx, host, r)
if err != nil {
return nil, err
} else if len(ips) == 0 {
return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host)
}
ip := ips[rand.Intn(len(ips))]
options := []dialer.Option{}
if iface != "" {
options = append(options, dialer.WithInterface(iface))
}
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port), options...)
},
TLSClientConfig: &tls.Config{
// alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6
NextProtos: []string{"dns"},
},
},
}
}