123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- package tunneldns
- import (
- "bytes"
- "context"
- "crypto/tls"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/url"
- "time"
- "github.com/miekg/dns"
- "github.com/pkg/errors"
- "github.com/rs/zerolog"
- "golang.org/x/net/http2"
- )
- const (
- defaultTimeout = 5 * time.Second
- )
- // UpstreamHTTPS is the upstream implementation for DNS over HTTPS service
- type UpstreamHTTPS struct {
- client *http.Client
- endpoint *url.URL
- bootstraps []string
- log *zerolog.Logger
- }
- // NewUpstreamHTTPS creates a new DNS over HTTPS upstream from endpoint
- func NewUpstreamHTTPS(endpoint string, bootstraps []string, maxConnections int, log *zerolog.Logger) (Upstream, error) {
- u, err := url.Parse(endpoint)
- if err != nil {
- return nil, err
- }
- return &UpstreamHTTPS{client: configureClient(u.Hostname(), maxConnections), endpoint: u, bootstraps: bootstraps, log: log}, nil
- }
- // Exchange provides an implementation for the Upstream interface
- func (u *UpstreamHTTPS) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error) {
- queryBuf, err := query.Pack()
- if err != nil {
- return nil, errors.Wrap(err, "failed to pack DNS query")
- }
- if len(query.Question) > 0 && query.Question[0].Name == fmt.Sprintf("%s.", u.endpoint.Hostname()) {
- for _, bootstrap := range u.bootstraps {
- endpoint, client, err := configureBootstrap(bootstrap)
- if err != nil {
- u.log.Err(err).Msgf("failed to configure bootstrap upstream %s", bootstrap)
- continue
- }
- msg, err := exchange(queryBuf, query.Id, endpoint, client, u.log)
- if err != nil {
- u.log.Err(err).Msgf("failed to connect to a bootstrap upstream %s", bootstrap)
- continue
- }
- return msg, nil
- }
- return nil, fmt.Errorf("failed to reach any bootstrap upstream: %v", u.bootstraps)
- }
- return exchange(queryBuf, query.Id, u.endpoint, u.client, u.log)
- }
- func exchange(msg []byte, queryID uint16, endpoint *url.URL, client *http.Client, log *zerolog.Logger) (*dns.Msg, error) {
- // No content negotiation for now, use DNS wire format
- buf, backendErr := exchangeWireformat(msg, endpoint, client)
- if backendErr == nil {
- response := &dns.Msg{}
- if err := response.Unpack(buf); err != nil {
- return nil, errors.Wrap(err, "failed to unpack DNS response from body")
- }
- response.Id = queryID
- return response, nil
- }
- log.Err(backendErr).Msgf("failed to connect to an HTTPS backend %q", endpoint)
- return nil, backendErr
- }
- // Perform message exchange with the default UDP wireformat defined in current draft
- // https://datatracker.ietf.org/doc/draft-ietf-doh-dns-over-https
- func exchangeWireformat(msg []byte, endpoint *url.URL, client *http.Client) ([]byte, error) {
- req, err := http.NewRequest("POST", endpoint.String(), bytes.NewBuffer(msg))
- if err != nil {
- return nil, errors.Wrap(err, "failed to create an HTTPS request")
- }
- req.Header.Add("Content-Type", "application/dns-message")
- req.Host = endpoint.Host
- resp, err := client.Do(req)
- if err != nil {
- return nil, errors.Wrap(err, "failed to perform an HTTPS request")
- }
- // Check response status code
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("returned status code %d", resp.StatusCode)
- }
- // Read wireformat response from the body
- buf, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, errors.Wrap(err, "failed to read the response body")
- }
- return buf, nil
- }
- func configureBootstrap(bootstrap string) (*url.URL, *http.Client, error) {
- b, err := url.Parse(bootstrap)
- if err != nil {
- return nil, nil, err
- }
- if ip := net.ParseIP(b.Hostname()); ip == nil {
- return nil, nil, fmt.Errorf("bootstrap address of %s must be an IP address", b.Hostname())
- }
- return b, configureClient(b.Hostname(), MaxUpstreamConnsDefault), nil
- }
- // configureClient will configure a HTTPS client for upstream DoH requests
- func configureClient(hostname string, maxUpstreamConnections int) *http.Client {
- // Update TLS and HTTP client configuration
- tlsConfig := &tls.Config{ServerName: hostname}
- transport := &http.Transport{
- TLSClientConfig: tlsConfig,
- DisableCompression: true,
- MaxIdleConns: 1,
- MaxConnsPerHost: maxUpstreamConnections,
- Proxy: http.ProxyFromEnvironment,
- }
- _ = http2.ConfigureTransport(transport)
- return &http.Client{
- Timeout: defaultTimeout,
- Transport: transport,
- }
- }
|