mirror of
https://github.com/AdguardTeam/dnsproxy.git
synced 2026-10-12 02:01:26 +00:00
232 lines
5.9 KiB
Go
232 lines
5.9 KiB
Go
package upstream
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/dnsproxy/internal/bootstrap"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// network is the semantic type alias of the network to pass to dialing
|
|
// functions. It's either [networkUDP] or [networkTCP]. It may also be used as
|
|
// URL scheme for plain upstreams.
|
|
type network = string
|
|
|
|
const (
|
|
// networkUDP is the UDP network.
|
|
networkUDP network = "udp"
|
|
|
|
// networkTCP is the TCP network.
|
|
networkTCP network = "tcp"
|
|
)
|
|
|
|
// plainDNS implements the [Upstream] interface for the regular DNS protocol.
|
|
type plainDNS struct {
|
|
// addr is the DNS server URL. Scheme is always "udp" or "tcp".
|
|
addr *url.URL
|
|
|
|
// logger is used for exchange logging. It is never nil.
|
|
logger *slog.Logger
|
|
|
|
// getDialer either returns an initialized dial handler or creates a new
|
|
// one.
|
|
getDialer DialerInitializer
|
|
|
|
// net is the network of the connections.
|
|
net network
|
|
|
|
// timeout is the timeout for DNS requests.
|
|
timeout time.Duration
|
|
}
|
|
|
|
// newPlain returns the plain DNS Upstream. addr.Scheme should be either "udp"
|
|
// or "tcp".
|
|
func newPlain(addr *url.URL, opts *Options) (u *plainDNS, err error) {
|
|
switch addr.Scheme {
|
|
case networkUDP, networkTCP:
|
|
// Go on.
|
|
default:
|
|
return nil, fmt.Errorf("unsupported url scheme: %s", addr.Scheme)
|
|
}
|
|
|
|
addPort(addr, defaultPortPlain)
|
|
|
|
return &plainDNS{
|
|
addr: addr,
|
|
logger: opts.Logger,
|
|
getDialer: newDialerInitializer(addr, opts),
|
|
net: addr.Scheme,
|
|
timeout: opts.Timeout,
|
|
}, nil
|
|
}
|
|
|
|
// type check
|
|
var _ Upstream = &plainDNS{}
|
|
|
|
// Address implements the [Upstream] interface for *plainDNS.
|
|
func (p *plainDNS) Address() string {
|
|
switch p.net {
|
|
case networkUDP:
|
|
return p.addr.Host
|
|
case networkTCP:
|
|
return p.addr.String()
|
|
default:
|
|
panic(fmt.Sprintf("unexpected network: %s", p.net))
|
|
}
|
|
}
|
|
|
|
// dialExchange performs a DNS exchange with the specified dial handler.
|
|
// network must be either [networkUDP] or [networkTCP].
|
|
func (p *plainDNS) dialExchange(
|
|
ctx context.Context,
|
|
network network,
|
|
dial bootstrap.DialHandler,
|
|
req *dns.Msg,
|
|
) (resp *dns.Msg, err error) {
|
|
var cancel context.CancelFunc
|
|
if p.timeout > 0 {
|
|
ctx, cancel = context.WithTimeout(ctx, p.timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
addr := p.Address()
|
|
|
|
// NOTE: It is important to set a client timeout to prevent the use of the
|
|
// default timeout settings.
|
|
client := &dns.Client{Timeout: p.timeout}
|
|
|
|
conn := &dns.Conn{}
|
|
upstreamReq := setRequestForNetwork(req, conn, network)
|
|
defer func() {
|
|
if resp != nil {
|
|
resp.Id = req.Id
|
|
}
|
|
}()
|
|
|
|
logBegin(ctx, p.logger, addr, network, upstreamReq)
|
|
defer func() { logFinish(ctx, p.logger, addr, network, err) }()
|
|
|
|
// NOTE: Discard context timeout to prevent deadline sharing between dial
|
|
// calls.
|
|
//
|
|
// TODO(f.setrakov): Consider supporting context timeout here.
|
|
conn.Conn, err = dial(context.WithoutCancel(ctx), network, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dialing %s over %s: %w", p.addr.Host, network, err)
|
|
}
|
|
defer func(c net.Conn) { err = errors.WithDeferred(err, c.Close()) }(conn.Conn)
|
|
|
|
resp, _, err = client.ExchangeWithConnContext(ctx, upstreamReq, conn)
|
|
if isExpectedConnErr(err) {
|
|
if p.timeout > 0 {
|
|
ctx = context.WithoutCancel(ctx)
|
|
ctx, cancel = context.WithTimeout(ctx, p.timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
conn.Conn, err = dial(context.WithoutCancel(ctx), network, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dialing %s over %s again: %w", p.addr.Host, network, err)
|
|
}
|
|
defer func(c net.Conn) { err = errors.WithDeferred(err, c.Close()) }(conn.Conn)
|
|
|
|
resp, _, err = client.ExchangeWithConnContext(ctx, upstreamReq, conn)
|
|
}
|
|
|
|
if err != nil {
|
|
return resp, fmt.Errorf("exchanging with %s over %s: %w", addr, network, err)
|
|
}
|
|
|
|
return resp, validateResponse(upstreamReq, resp)
|
|
}
|
|
|
|
// setRequestForNetwork sets connection options in conn and overrides the
|
|
// upstream request, if necessary, depending on network. If network is
|
|
// [networkUDP] and orig has a zero ID, req is a copy of orig with a new ID to
|
|
// increase entropy. network must be either [networkUDP] or [networkTCP]. orig
|
|
// and conn must not be nil.
|
|
func setRequestForNetwork(orig *dns.Msg, conn *dns.Conn, network network) (req *dns.Msg) {
|
|
req = orig
|
|
if network != networkUDP {
|
|
return req
|
|
}
|
|
|
|
conn.UDPSize = dns.MinMsgSize
|
|
|
|
if orig.Id == 0 {
|
|
req = orig.Copy()
|
|
req.Id = dns.Id()
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
// isExpectedConnErr returns true if the error is expected. In this case,
|
|
// we will make a second attempt to process the request.
|
|
func isExpectedConnErr(err error) (is bool) {
|
|
var netErr net.Error
|
|
|
|
return err != nil && (errors.As(err, &netErr) || errors.Is(err, io.EOF))
|
|
}
|
|
|
|
// Exchange implements the [Upstream] interface for *plainDNS.
|
|
func (p *plainDNS) Exchange(ctx context.Context, req *dns.Msg) (resp *dns.Msg, err error) {
|
|
dial, err := p.getDialer()
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return nil, err
|
|
}
|
|
|
|
addr := p.Address()
|
|
|
|
resp, err = p.dialExchange(ctx, p.net, dial, req)
|
|
if p.net != networkUDP {
|
|
// The network is already TCP.
|
|
return resp, err
|
|
}
|
|
|
|
if resp == nil {
|
|
// There is likely an error with the upstream.
|
|
return resp, err
|
|
}
|
|
|
|
if errors.Is(err, errQuestion) {
|
|
// The upstream responds with malformed messages, so try TCP.
|
|
p.logger.DebugContext(
|
|
ctx,
|
|
"plain response is malformed, using tcp",
|
|
"addr", addr,
|
|
slogutil.KeyError, err,
|
|
)
|
|
|
|
return p.dialExchange(ctx, networkTCP, dial, req)
|
|
} else if resp.Truncated {
|
|
// Fallback to TCP on truncated responses.
|
|
p.logger.DebugContext(
|
|
ctx,
|
|
"plain response is truncated, using tcp",
|
|
"question", &req.Question[0],
|
|
"addr", addr,
|
|
)
|
|
|
|
return p.dialExchange(ctx, networkTCP, dial, req)
|
|
}
|
|
|
|
// There is either no error or the error isn't related to the received
|
|
// message.
|
|
return resp, err
|
|
}
|
|
|
|
// Close implements the [Upstream] interface for *plainDNS.
|
|
func (p *plainDNS) Close() (err error) {
|
|
return nil
|
|
}
|