https_upstream.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package tunneldns
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/tls"
  6. "fmt"
  7. "io"
  8. "net"
  9. "net/http"
  10. "net/url"
  11. "time"
  12. "github.com/miekg/dns"
  13. "github.com/pkg/errors"
  14. "github.com/rs/zerolog"
  15. "golang.org/x/net/http2"
  16. )
  17. const (
  18. defaultTimeout = 5 * time.Second
  19. )
  20. // UpstreamHTTPS is the upstream implementation for DNS over HTTPS service
  21. type UpstreamHTTPS struct {
  22. client *http.Client
  23. endpoint *url.URL
  24. bootstraps []string
  25. log *zerolog.Logger
  26. }
  27. // NewUpstreamHTTPS creates a new DNS over HTTPS upstream from endpoint
  28. func NewUpstreamHTTPS(endpoint string, bootstraps []string, maxConnections int, log *zerolog.Logger) (Upstream, error) {
  29. u, err := url.Parse(endpoint)
  30. if err != nil {
  31. return nil, err
  32. }
  33. return &UpstreamHTTPS{client: configureClient(u.Hostname(), maxConnections), endpoint: u, bootstraps: bootstraps, log: log}, nil
  34. }
  35. // Exchange provides an implementation for the Upstream interface
  36. func (u *UpstreamHTTPS) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error) {
  37. queryBuf, err := query.Pack()
  38. if err != nil {
  39. return nil, errors.Wrap(err, "failed to pack DNS query")
  40. }
  41. if len(query.Question) > 0 && query.Question[0].Name == fmt.Sprintf("%s.", u.endpoint.Hostname()) {
  42. for _, bootstrap := range u.bootstraps {
  43. endpoint, client, err := configureBootstrap(bootstrap)
  44. if err != nil {
  45. u.log.Err(err).Msgf("failed to configure bootstrap upstream %s", bootstrap)
  46. continue
  47. }
  48. msg, err := exchange(queryBuf, query.Id, endpoint, client, u.log)
  49. if err != nil {
  50. u.log.Err(err).Msgf("failed to connect to a bootstrap upstream %s", bootstrap)
  51. continue
  52. }
  53. return msg, nil
  54. }
  55. return nil, fmt.Errorf("failed to reach any bootstrap upstream: %v", u.bootstraps)
  56. }
  57. return exchange(queryBuf, query.Id, u.endpoint, u.client, u.log)
  58. }
  59. func exchange(msg []byte, queryID uint16, endpoint *url.URL, client *http.Client, log *zerolog.Logger) (*dns.Msg, error) {
  60. // No content negotiation for now, use DNS wire format
  61. buf, backendErr := exchangeWireformat(msg, endpoint, client)
  62. if backendErr == nil {
  63. response := &dns.Msg{}
  64. if err := response.Unpack(buf); err != nil {
  65. return nil, errors.Wrap(err, "failed to unpack DNS response from body")
  66. }
  67. response.Id = queryID
  68. return response, nil
  69. }
  70. log.Err(backendErr).Msgf("failed to connect to an HTTPS backend %q", endpoint)
  71. return nil, backendErr
  72. }
  73. // Perform message exchange with the default UDP wireformat defined in current draft
  74. // https://datatracker.ietf.org/doc/draft-ietf-doh-dns-over-https
  75. func exchangeWireformat(msg []byte, endpoint *url.URL, client *http.Client) ([]byte, error) {
  76. req, err := http.NewRequest("POST", endpoint.String(), bytes.NewBuffer(msg))
  77. if err != nil {
  78. return nil, errors.Wrap(err, "failed to create an HTTPS request")
  79. }
  80. req.Header.Add("Content-Type", "application/dns-message")
  81. req.Host = endpoint.Host
  82. resp, err := client.Do(req)
  83. if err != nil {
  84. return nil, errors.Wrap(err, "failed to perform an HTTPS request")
  85. }
  86. // Check response status code
  87. defer resp.Body.Close()
  88. if resp.StatusCode != http.StatusOK {
  89. return nil, fmt.Errorf("returned status code %d", resp.StatusCode)
  90. }
  91. // Read wireformat response from the body
  92. buf, err := io.ReadAll(resp.Body)
  93. if err != nil {
  94. return nil, errors.Wrap(err, "failed to read the response body")
  95. }
  96. return buf, nil
  97. }
  98. func configureBootstrap(bootstrap string) (*url.URL, *http.Client, error) {
  99. b, err := url.Parse(bootstrap)
  100. if err != nil {
  101. return nil, nil, err
  102. }
  103. if ip := net.ParseIP(b.Hostname()); ip == nil {
  104. return nil, nil, fmt.Errorf("bootstrap address of %s must be an IP address", b.Hostname())
  105. }
  106. return b, configureClient(b.Hostname(), MaxUpstreamConnsDefault), nil
  107. }
  108. // configureClient will configure a HTTPS client for upstream DoH requests
  109. func configureClient(hostname string, maxUpstreamConnections int) *http.Client {
  110. // Update TLS and HTTP client configuration
  111. tlsConfig := &tls.Config{ServerName: hostname}
  112. transport := &http.Transport{
  113. TLSClientConfig: tlsConfig,
  114. DisableCompression: true,
  115. MaxIdleConns: 1,
  116. MaxConnsPerHost: maxUpstreamConnections,
  117. Proxy: http.ProxyFromEnvironment,
  118. }
  119. _ = http2.ConfigureTransport(transport)
  120. return &http.Client{
  121. Timeout: defaultTimeout,
  122. Transport: transport,
  123. }
  124. }