blocky/resolver/upstream_resolver.go

402 lines
10 KiB
Go
Raw Normal View History

2020-01-12 18:23:35 +01:00
package resolver
import (
2020-02-17 22:06:10 +01:00
"bytes"
"context"
"crypto/tls"
2020-02-17 22:06:10 +01:00
"errors"
2020-01-12 18:23:35 +01:00
"fmt"
2022-08-19 22:04:35 +02:00
"io"
2020-01-12 18:23:35 +01:00
"net"
2020-02-17 22:06:10 +01:00
"net/http"
2020-01-12 18:23:35 +01:00
"strconv"
"time"
2021-12-24 22:40:06 +01:00
"github.com/avast/retry-go/v4"
2021-08-25 22:06:34 +02:00
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/model"
2021-08-25 22:06:34 +02:00
"github.com/0xERR0R/blocky/util"
2020-01-12 18:23:35 +01:00
"github.com/miekg/dns"
"github.com/sirupsen/logrus"
)
2020-02-17 22:06:10 +01:00
const (
dnsContentType = "application/dns-message"
retryAttempts = 3
2020-02-17 22:06:10 +01:00
)
// UpstreamServerError wraps a response with RCode ServFail so no other resolver tries to use it.
type UpstreamServerError struct {
Msg *dns.Msg
}
func (e *UpstreamServerError) Error() string {
return "upstream server failed"
}
type upstreamConfig struct {
config.Upstreams
config.Upstream
}
func newUpstreamConfig(upstream config.Upstream, cfg config.Upstreams) upstreamConfig {
return upstreamConfig{cfg, upstream}
}
func (c upstreamConfig) String() string {
return c.Upstream.String()
}
// IsEnabled implements `config.Configurable`.
func (c upstreamConfig) IsEnabled() bool {
return true
}
// LogConfig implements `config.Configurable`.
func (c upstreamConfig) LogConfig(logger *logrus.Entry) {
logger.Info(c.Upstream)
}
2020-01-12 18:23:35 +01:00
// UpstreamResolver sends request to external DNS server
type UpstreamResolver struct {
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
typed
configurable[upstreamConfig]
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
2020-02-17 22:06:10 +01:00
upstreamClient upstreamClient
bootstrap *Bootstrap
2020-02-17 22:06:10 +01:00
}
type upstreamClient interface {
fmtURL(ip net.IP, port uint16, path string) string
callExternal(
ctx context.Context, msg *dns.Msg, upstreamURL string, protocol model.RequestProtocol,
) (response *dns.Msg, rtt time.Duration, err error)
2020-02-17 22:06:10 +01:00
}
type dnsUpstreamClient struct {
tcpClient, udpClient *dns.Client
2020-02-17 22:06:10 +01:00
}
type httpUpstreamClient struct {
client *http.Client
host string
userAgent string
2020-02-17 22:06:10 +01:00
}
func createUpstreamClient(cfg upstreamConfig) upstreamClient {
tlsConfig := tls.Config{
2022-09-15 19:09:50 +02:00
ServerName: cfg.Host,
MinVersion: tls.VersionTLS12,
}
2022-09-17 21:34:34 +02:00
if cfg.CommonName != "" {
tlsConfig.ServerName = cfg.CommonName
2022-09-15 19:09:50 +02:00
}
switch cfg.Net {
case config.NetProtocolHttps:
transport := util.DefaultHTTPTransport()
transport.TLSClientConfig = &tlsConfig
2020-02-17 22:06:10 +01:00
return &httpUpstreamClient{
userAgent: cfg.UserAgent,
2020-02-17 22:06:10 +01:00
client: &http.Client{
Transport: transport,
2020-02-17 22:06:10 +01:00
},
host: cfg.Host,
}
case config.NetProtocolTcpTls:
return &dnsUpstreamClient{
tcpClient: &dns.Client{
TLSConfig: &tlsConfig,
Net: cfg.Net.String(),
},
}
2020-02-17 22:06:10 +01:00
case config.NetProtocolTcpUdp:
return &dnsUpstreamClient{
tcpClient: &dns.Client{
Net: "tcp",
},
udpClient: &dns.Client{
Net: "udp",
},
}
default:
log.Log().Fatalf("invalid protocol %s", cfg.Net)
panic("unreachable")
}
}
func (r *httpUpstreamClient) fmtURL(ip net.IP, port uint16, path string) string {
return fmt.Sprintf("https://%s%s", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))), path)
2020-02-17 22:06:10 +01:00
}
func (r *httpUpstreamClient) callExternal(
ctx context.Context, msg *dns.Msg, upstreamURL string, _ model.RequestProtocol,
) (*dns.Msg, time.Duration, error) {
2020-02-17 22:06:10 +01:00
start := time.Now()
rawDNSMessage, err := msg.Pack()
if err != nil {
return nil, 0, fmt.Errorf("can't pack message: %w", err)
2020-02-17 22:06:10 +01:00
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, bytes.NewReader(rawDNSMessage))
if err != nil {
return nil, 0, fmt.Errorf("can't create the new request %w", err)
}
req.Header.Set("User-Agent", r.userAgent)
req.Header.Set("Content-Type", dnsContentType)
req.Host = r.host
httpResponse, err := r.client.Do(req)
2020-02-17 22:06:10 +01:00
if err != nil {
return nil, 0, fmt.Errorf("can't perform https request: %w", err)
2020-02-17 22:06:10 +01:00
}
2021-01-19 21:52:24 +01:00
defer func() {
util.LogOnError(ctx, "can't close response body ", httpResponse.Body.Close())
2021-01-19 21:52:24 +01:00
}()
2020-02-17 22:06:10 +01:00
if httpResponse.StatusCode != http.StatusOK {
return nil, 0, fmt.Errorf("http return code should be %d, but received %d", http.StatusOK, httpResponse.StatusCode)
}
contentType := httpResponse.Header.Get("content-type")
if contentType != dnsContentType {
return nil, 0, fmt.Errorf("http return content type should be '%s', but was '%s'",
dnsContentType, contentType)
}
2022-08-19 22:04:35 +02:00
body, err := io.ReadAll(httpResponse.Body)
2020-02-17 22:06:10 +01:00
if err != nil {
return nil, 0, fmt.Errorf("can't read response body: %w", err)
2020-02-17 22:06:10 +01:00
}
response := dns.Msg{}
err = response.Unpack(body)
if err != nil {
return nil, 0, fmt.Errorf("can't unpack message: %w", err)
2020-02-17 22:06:10 +01:00
}
return &response, time.Since(start), nil
}
func (r *dnsUpstreamClient) fmtURL(ip net.IP, port uint16, _ string) string {
return net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))
}
func (r *dnsUpstreamClient) callExternal(
ctx context.Context, msg *dns.Msg, upstreamURL string, protocol model.RequestProtocol,
) (response *dns.Msg, rtt time.Duration, err error) {
if r.udpClient == nil {
return r.tcpClient.ExchangeContext(ctx, msg, upstreamURL)
}
return r.raceClients(ctx, msg, upstreamURL, protocol)
}
type exchangeResult struct {
proto model.RequestProtocol
msg *dns.Msg
rtt time.Duration
err error
}
func (r *dnsUpstreamClient) raceClients(
ctx context.Context, msg *dns.Msg, upstreamURL string, protocol model.RequestProtocol,
) (response *dns.Msg, rtt time.Duration, err error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// We don't explicitly close the channel, but since the buffer is big enough for all goroutines,
// it will be GC'ed and closed automatically.
ch := make(chan exchangeResult, 2) //nolint:gomnd // TCP and UDP
exchange := func(client *dns.Client, proto model.RequestProtocol) {
msg, rtt, err := client.ExchangeContext(ctx, msg, upstreamURL)
if err == nil && msg.Rcode == dns.RcodeServerFailure {
err = &UpstreamServerError{msg}
}
ch <- exchangeResult{proto, msg, rtt, err}
}
go exchange(r.tcpClient, model.RequestProtocolTCP)
go exchange(r.udpClient, model.RequestProtocolUDP)
// We don't care about a response too big for the downstream protocol: that's handled by `Server`,
// and returning a larger request from here might allow us to cache it.
res1 := <-ch
if res1.err == nil && !res1.msg.Truncated {
return res1.msg, res1.rtt, nil
}
res2 := <-ch
if res2.err == nil && !res2.msg.Truncated {
return res2.msg, res2.rtt, nil
}
resWhere := func(pred func(*exchangeResult) bool) *exchangeResult {
if pred(&res1) {
return &res1
}
return &res2
}
// When both failed, return the result that used the same protocol as the downstream request
if res1.err != nil && res2.err != nil {
sameProto := resWhere(func(r *exchangeResult) bool { return r.proto == protocol })
return sameProto.msg, sameProto.rtt, sameProto.err
}
// Only a single one failed, use the one that succeeded
successful := resWhere(func(r *exchangeResult) bool { return r.err == nil })
return successful.msg, successful.rtt, nil
2020-01-12 18:23:35 +01:00
}
// NewUpstreamResolver creates new resolver instance
func NewUpstreamResolver(
ctx context.Context, cfg upstreamConfig, bootstrap *Bootstrap,
) (*UpstreamResolver, error) {
r := newUpstreamResolverUnchecked(cfg, bootstrap)
onErr := func(err error) {
_, logger := r.log(ctx)
logger.WithError(err).Warn("initial resolver test failed")
}
err := cfg.Init.Strategy.Do(ctx, r.testResolve, onErr)
if err != nil {
return nil, err
}
return r, nil
}
// newUpstreamResolverUnchecked creates new resolver instance without validating the upstream
func newUpstreamResolverUnchecked(cfg upstreamConfig, bootstrap *Bootstrap) *UpstreamResolver {
upstreamClient := createUpstreamClient(cfg)
2020-01-12 18:23:35 +01:00
return &UpstreamResolver{
typed: withType("upstream"),
configurable: withConfig(cfg),
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
2020-02-17 22:06:10 +01:00
upstreamClient: upstreamClient,
bootstrap: bootstrap,
}
2020-01-12 18:23:35 +01:00
}
func (r UpstreamResolver) String() string {
return fmt.Sprintf("%s '%s'", r.Type(), r.cfg)
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
}
func (r UpstreamResolver) Upstream() config.Upstream {
return r.cfg.Upstream
}
func (r *UpstreamResolver) log(ctx context.Context) (context.Context, *logrus.Entry) {
return r.logWithFields(ctx, logrus.Fields{
"upstream": r.cfg.String(),
})
2020-01-12 18:23:35 +01:00
}
// testResolve sends a test query to verify the upstream is reachable and working
func (r *UpstreamResolver) testResolve(ctx context.Context) error {
// example.com MUST always resolve. See SUDN resolver
request := newRequest("example.com.", dns.Type(dns.TypeA))
_, err := r.Resolve(ctx, request)
return err
}
// Resolve calls external resolver
func (r *UpstreamResolver) Resolve(ctx context.Context, request *model.Request) (response *model.Response, err error) {
ctx, logger := r.log(ctx)
ips, err := r.bootstrap.UpstreamIPs(ctx, r)
if err != nil {
return nil, err
}
2020-01-12 18:23:35 +01:00
var (
resp *dns.Msg
ip net.IP
)
2020-01-12 18:23:35 +01:00
err = retry.Do(
func() error {
ip = ips.Current()
upstreamURL := r.upstreamClient.fmtURL(ip, r.cfg.Port, r.cfg.Path)
ctx, cancel := context.WithTimeout(ctx, r.cfg.Timeout.ToDuration())
defer cancel()
response, rtt, err := r.upstreamClient.callExternal(ctx, request.Req, upstreamURL, request.Protocol)
if err != nil {
return fmt.Errorf("can't resolve request via upstream server %s (%s): %w", r.cfg, upstreamURL, err)
}
resp = response
r.logResponse(logger, request, response, ip, rtt)
return nil
},
retry.Context(ctx),
retry.Attempts(retryAttempts),
retry.DelayType(retry.FixedDelay),
retry.Delay(1*time.Millisecond),
retry.LastErrorOnly(true),
retry.RetryIf(isTimeout),
retry.OnRetry(func(n uint, err error) {
logger.WithFields(logrus.Fields{
"upstream": r.cfg.String(),
"upstream_ip": ip.String(),
"question": util.QuestionToString(request.Req.Question),
"attempt": fmt.Sprintf("%d/%d", n+1, retryAttempts),
}).Debugf("%s, retrying...", err)
ips.Next()
}))
if err != nil {
return nil, err
2020-01-12 18:23:35 +01:00
}
return &model.Response{Res: resp, Reason: fmt.Sprintf("RESOLVED (%s)", r.cfg)}, nil
2020-01-12 18:23:35 +01:00
}
func (r *UpstreamResolver) logResponse(
logger *logrus.Entry, request *model.Request, resp *dns.Msg, ip net.IP, rtt time.Duration,
) {
logger.WithFields(logrus.Fields{
"answer": util.AnswerToString(resp.Answer),
"return_code": dns.RcodeToString[resp.Rcode],
"upstream": r.cfg.String(),
"upstream_ip": ip.String(),
"protocol": request.Protocol,
"net": r.cfg.Net,
"response_time_ms": rtt.Milliseconds(),
}).Debugf("received response from upstream")
}
func isTimeout(err error) bool {
var netErr net.Error
return errors.As(err, &netErr) && netErr.Timeout()
}