blocky/server/server.go

690 lines
17 KiB
Go
Raw Normal View History

2020-01-12 18:23:35 +01:00
package server
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
2021-10-02 22:54:56 +02:00
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
2021-08-25 22:06:34 +02:00
"fmt"
"math"
"math/big"
mrand "math/rand"
2021-08-25 22:06:34 +02:00
"net"
"net/http"
2020-02-10 16:12:01 +01:00
"runtime"
"runtime/debug"
"strings"
"time"
2020-01-12 18:23:35 +01:00
2021-08-25 22:06:34 +02:00
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/metrics"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/redis"
2021-08-25 22:06:34 +02:00
"github.com/0xERR0R/blocky/resolver"
"github.com/0xERR0R/blocky/util"
"github.com/hashicorp/go-multierror"
2020-01-12 18:23:35 +01:00
2021-12-24 22:56:41 +01:00
"github.com/go-chi/chi/v5"
2020-01-12 18:23:35 +01:00
"github.com/miekg/dns"
"github.com/sirupsen/logrus"
)
const (
maxUDPBufferSize = 65535
caExpiryYears = 10
certExpiryYears = 5
)
// Server controls the endpoints for DNS and HTTP
2020-01-12 18:23:35 +01:00
type Server struct {
dnsServers []*dns.Server
httpListeners []net.Listener
httpsListeners []net.Listener
queryResolver resolver.ChainedResolver
cfg *config.Config
httpMux *chi.Mux
httpsMux *chi.Mux
cert tls.Certificate
2020-01-12 18:23:35 +01:00
}
func logger() *logrus.Entry {
return log.PrefixedLog("server")
2020-01-12 18:23:35 +01:00
}
func tlsCipherSuites() []uint16 {
tlsCipherSuites := []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
}
return tlsCipherSuites
}
func getServerAddress(addr string) string {
if !strings.Contains(addr, ":") {
addr = fmt.Sprintf(":%s", addr)
}
return addr
}
type NewServerFunc func(address string) (*dns.Server, error)
2021-12-21 22:02:15 +01:00
func retrieveCertificate(cfg *config.Config) (cert tls.Certificate, err error) {
if cfg.CertFile == "" && cfg.KeyFile == "" {
cert, err = createSelfSignedCert()
if err != nil {
return tls.Certificate{}, fmt.Errorf("unable to generate self-signed certificate: %w", err)
}
log.Log().Info("using self-signed certificate")
} else {
cert, err = tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
if err != nil {
return tls.Certificate{}, fmt.Errorf("can't load certificate files: %w", err)
}
}
return
}
// NewServer creates new server instance with passed config
//
//nolint:funlen
func NewServer(ctx context.Context, cfg *config.Config) (server *Server, err error) {
var cert tls.Certificate
if len(cfg.Ports.HTTPS) > 0 || len(cfg.Ports.TLS) > 0 {
cert, err = retrieveCertificate(cfg)
if err != nil {
return nil, fmt.Errorf("can't retrieve cert: %w", err)
}
}
dnsServers, err := createServers(cfg, cert)
if err != nil {
return nil, fmt.Errorf("server creation failed: %w", err)
}
2020-04-08 23:03:07 +02:00
httpRouter := createHTTPRouter(cfg)
httpsRouter := createHTTPSRouter(cfg)
2021-12-21 22:02:15 +01:00
httpListeners, httpsListeners, err := createHTTPListeners(cfg)
if err != nil {
return nil, err
}
if len(httpListeners) != 0 || len(httpsListeners) != 0 {
metrics.Start(httpRouter, cfg.Prometheus)
metrics.Start(httpsRouter, cfg.Prometheus)
}
2021-01-19 21:52:24 +01:00
metrics.RegisterEventListeners()
bootstrap, err := resolver.NewBootstrap(ctx, cfg)
if err != nil {
return nil, err
}
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
var redisClient *redis.Client
if cfg.Redis.IsEnabled() {
redisClient, err = redis.New(ctx, &cfg.Redis)
if err != nil && cfg.Redis.Required {
return nil, err
}
}
queryResolver, queryError := createQueryResolver(ctx, cfg, bootstrap, redisClient)
if queryError != nil {
return nil, queryError
2021-10-13 21:40:18 +02:00
}
2020-01-12 18:23:35 +01:00
2021-10-13 21:40:18 +02:00
server = &Server{
dnsServers: dnsServers,
queryResolver: queryResolver,
cfg: cfg,
httpListeners: httpListeners,
httpsListeners: httpsListeners,
httpMux: httpRouter,
httpsMux: httpsRouter,
cert: cert,
2021-10-13 21:40:18 +02:00
}
2020-01-12 18:23:35 +01:00
2021-10-13 21:40:18 +02:00
server.printConfiguration()
2020-01-12 18:23:35 +01:00
server.registerDNSHandlers(ctx)
err = server.registerAPIEndpoints(httpRouter)
2021-02-04 21:59:41 +01:00
if err != nil {
return nil, err
}
err = server.registerAPIEndpoints(httpsRouter)
if err != nil {
return nil, err
}
2021-10-13 21:40:18 +02:00
return server, err
2020-01-12 18:23:35 +01:00
}
func createServers(cfg *config.Config, cert tls.Certificate) ([]*dns.Server, error) {
var dnsServers []*dns.Server
var err *multierror.Error
addServers := func(newServer NewServerFunc, addresses config.ListenConfig) error {
for _, address := range addresses {
server, err := newServer(getServerAddress(address))
if err != nil {
return err
}
dnsServers = append(dnsServers, server)
}
return nil
}
err = multierror.Append(err,
addServers(createUDPServer, cfg.Ports.DNS),
addServers(createTCPServer, cfg.Ports.DNS),
addServers(func(address string) (*dns.Server, error) {
return createTLSServer(cfg, address, cert)
}, cfg.Ports.TLS))
return dnsServers, err.ErrorOrNil()
}
func createHTTPListeners(cfg *config.Config) (httpListeners, httpsListeners []net.Listener, err error) {
httpListeners, err = newListeners("http", cfg.Ports.HTTP)
2021-12-21 22:02:15 +01:00
if err != nil {
return nil, nil, err
}
httpsListeners, err = newListeners("https", cfg.Ports.HTTPS)
2021-12-21 22:02:15 +01:00
if err != nil {
return nil, nil, err
}
return httpListeners, httpsListeners, nil
}
func newListeners(proto string, addresses config.ListenConfig) ([]net.Listener, error) {
listeners := make([]net.Listener, 0, len(addresses))
for _, address := range addresses {
listener, err := net.Listen("tcp", getServerAddress(address))
if err != nil {
return nil, fmt.Errorf("start %s listener on %s failed: %w", proto, address, err)
}
listeners = append(listeners, listener)
}
return listeners, nil
}
func createTLSServer(cfg *config.Config, address string, cert tls.Certificate) (*dns.Server, error) {
2021-10-02 22:54:56 +02:00
return &dns.Server{
Addr: address,
Net: "tcp-tls",
//nolint:gosec
2021-10-02 22:54:56 +02:00
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: uint16(cfg.MinTLSServeVer),
CipherSuites: tlsCipherSuites(),
2021-10-02 22:54:56 +02:00
},
Handler: dns.NewServeMux(),
NotifyStartedFunc: func() {
logger().Infof("TLS server is up and running on address %s", address)
},
}, nil
2021-10-02 22:54:56 +02:00
}
func createTCPServer(address string) (*dns.Server, error) {
2021-02-20 22:02:24 +01:00
return &dns.Server{
2021-01-19 21:52:24 +01:00
Addr: address,
Net: "tcp",
Handler: dns.NewServeMux(),
NotifyStartedFunc: func() {
2021-10-02 22:54:56 +02:00
logger().Infof("TCP server is up and running on address %s", address)
2021-01-19 21:52:24 +01:00
},
}, nil
2021-01-19 21:52:24 +01:00
}
func createUDPServer(address string) (*dns.Server, error) {
2021-02-20 22:02:24 +01:00
return &dns.Server{
2021-01-19 21:52:24 +01:00
Addr: address,
Net: "udp",
Handler: dns.NewServeMux(),
NotifyStartedFunc: func() {
2021-10-02 22:54:56 +02:00
logger().Infof("UDP server is up and running on address %s", address)
2021-01-19 21:52:24 +01:00
},
UDPSize: maxUDPBufferSize,
}, nil
2021-01-19 21:52:24 +01:00
}
//nolint:funlen
func createSelfSignedCert() (tls.Certificate, error) {
// Create CA
ca := &x509.Certificate{
//nolint:gosec
SerialNumber: big.NewInt(int64(mrand.Intn(math.MaxInt))),
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(caExpiryYears, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, err
}
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
return tls.Certificate{}, err
}
caPEM := new(bytes.Buffer)
if err = pem.Encode(caPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
}); err != nil {
return tls.Certificate{}, err
}
caPrivKeyPEM := new(bytes.Buffer)
b, err := x509.MarshalECPrivateKey(caPrivKey)
if err != nil {
return tls.Certificate{}, err
}
if err = pem.Encode(caPrivKeyPEM, &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: b,
}); err != nil {
return tls.Certificate{}, err
}
// Create certificate
cert := &x509.Certificate{
//nolint:gosec
SerialNumber: big.NewInt(int64(mrand.Intn(math.MaxInt))),
DNSNames: []string{"*"},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(certExpiryYears, 0, 0),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, err
}
certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
if err != nil {
return tls.Certificate{}, err
}
certPEM := new(bytes.Buffer)
if err = pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
}); err != nil {
return tls.Certificate{}, err
}
certPrivKeyPEM := new(bytes.Buffer)
b, err = x509.MarshalECPrivateKey(certPrivKey)
if err != nil {
return tls.Certificate{}, err
}
if err = pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: b,
}); err != nil {
return tls.Certificate{}, err
}
keyPair, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
if err != nil {
return tls.Certificate{}, err
}
return keyPair, nil
}
func createQueryResolver(
ctx context.Context,
cfg *config.Config,
bootstrap *resolver.Bootstrap,
redisClient *redis.Client,
) (resolver.ChainedResolver, error) {
upstreamTree, utErr := resolver.NewUpstreamTreeResolver(ctx, cfg.Upstreams, bootstrap)
blocking, blErr := resolver.NewBlockingResolver(ctx, cfg.Blocking, redisClient, bootstrap)
clientNames, cnErr := resolver.NewClientNamesResolver(ctx, cfg.ClientLookup, cfg.Upstreams, bootstrap)
condUpstream, cuErr := resolver.NewConditionalUpstreamResolver(ctx, cfg.Conditional, cfg.Upstreams, bootstrap)
hostsFile, hfErr := resolver.NewHostsFileResolver(ctx, cfg.HostsFile, bootstrap)
err := multierror.Append(
multierror.Prefix(utErr, "upstream tree resolver: "),
multierror.Prefix(blErr, "blocking resolver: "),
multierror.Prefix(cnErr, "client names resolver: "),
multierror.Prefix(cuErr, "conditional upstream resolver: "),
2023-04-17 18:21:56 +02:00
multierror.Prefix(hfErr, "hosts file resolver: "),
).ErrorOrNil()
if err != nil {
return nil, err
}
2021-10-13 21:40:18 +02:00
r := resolver.Chain(
2022-04-01 08:58:09 +02:00
resolver.NewFilteringResolver(cfg.Filtering),
resolver.NewFQDNOnlyResolver(cfg.FQDNOnly),
resolver.NewECSResolver(cfg.ECS),
clientNames,
resolver.NewEDEResolver(cfg.EDE),
resolver.NewQueryLoggingResolver(ctx, cfg.QueryLog),
resolver.NewMetricsResolver(cfg.Prometheus),
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
resolver.NewRewriterResolver(cfg.CustomDNS.RewriterConfig, resolver.NewCustomDNSResolver(cfg.CustomDNS)),
2023-04-17 18:21:56 +02:00
hostsFile,
blocking,
resolver.NewCachingResolver(ctx, cfg.Caching, redisClient),
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
resolver.NewRewriterResolver(cfg.Conditional.RewriterConfig, condUpstream),
resolver.NewSpecialUseDomainNamesResolver(cfg.SUDN),
upstreamTree,
)
return r, nil
2020-04-08 23:03:07 +02:00
}
func (s *Server) registerDNSHandlers(ctx context.Context) {
wrappedOnRequest := func(w dns.ResponseWriter, request *dns.Msg) {
ip, proto := resolveClientIPAndProtocol(w.RemoteAddr())
s.OnRequest(ctx, w, ip, proto, request)
}
2021-10-02 22:54:56 +02:00
for _, server := range s.dnsServers {
handler := server.Handler.(*dns.ServeMux)
handler.HandleFunc(".", wrappedOnRequest)
2021-10-02 22:54:56 +02:00
handler.HandleFunc("healthcheck.blocky", s.OnHealthCheck)
}
}
2020-01-12 18:23:35 +01:00
func (s *Server) printConfiguration() {
logger().Info("current configuration:")
Refactoring Redis (#1271) * RedisConfig -> Redis * moved redis config to seperate file * bugfix in config test during parallel processing * implement config.Configurable in Redis config * use Context in GetRedisCache * use Context in New * caching resolver test fix * use Context in PublishEnabled * use Context in getResponse * remove ctx field * bugfix in api interface test * propperly close channels * set ruler for go files from 80 to 111 * line break because function length is to long * only execute redis.New if it is enabled in config * stabilized flaky tests * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * Update config/redis_test.go Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com> * fix ruler * redis test refactoring * vscode setting cleanup * removed else if chain * Update redis_test.go * context race fix * test fail on missing seintinel servers * cleanup context usage * cleanup2 * context fixes * added context util * disabled nil context rule for tests * copy paste error ctxSend -> CtxSend * use util.CtxSend * fixed comment * fixed flaky test * failsafe and tests --------- Co-authored-by: ThinkChaos <ThinkChaos@users.noreply.github.com>
2023-11-27 18:08:31 +01:00
if s.cfg.Redis.IsEnabled() {
logger().Info("Redis:")
log.WithIndent(logger(), " ", s.cfg.Redis.LogConfig)
}
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
resolver.ForEach(s.queryResolver, func(res resolver.Resolver) {
resolver.LogResolverConfig(res, logger())
})
2020-02-10 16:12:01 +01:00
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
logger().Info("listeners:")
log.WithIndent(logger(), " ", s.cfg.Ports.LogConfig)
2020-02-10 16:12:01 +01:00
logger().Info("runtime information:")
// force garbage collector
runtime.GC()
debug.FreeOSMemory()
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
logger().Infof(" numCPU = %d", runtime.NumCPU())
logger().Infof(" numGoroutine = %d", runtime.NumGoroutine())
2020-02-10 16:12:01 +01:00
// gather memory stats
var m runtime.MemStats
runtime.ReadMemStats(&m)
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
logger().Infof(" memory:")
logger().Infof(" heap = %10v MB", toMB(m.HeapAlloc))
logger().Infof(" sys = %10v MB", toMB(m.Sys))
logger().Infof(" numGC = %10v", m.NumGC)
2020-02-10 16:12:01 +01:00
}
func toMB(b uint64) uint64 {
const bytesInKB = 1024
return b / bytesInKB / bytesInKB
2020-01-12 18:23:35 +01:00
}
const (
readHeaderTimeout = 20 * time.Second
readTimeout = 20 * time.Second
writeTimeout = 20 * time.Second
)
// Start starts the server
func (s *Server) Start(ctx context.Context, errCh chan<- error) {
2020-01-12 18:23:35 +01:00
logger().Info("Starting server")
2021-10-02 22:54:56 +02:00
for _, srv := range s.dnsServers {
srv := srv
2020-01-12 18:23:35 +01:00
2021-10-02 22:54:56 +02:00
go func() {
if err := srv.ListenAndServe(); err != nil {
errCh <- fmt.Errorf("start %s listener failed: %w", srv.Net, err)
2021-10-02 22:54:56 +02:00
}
}()
}
2020-01-12 18:23:35 +01:00
for i, listener := range s.httpListeners {
listener := listener
address := s.cfg.Ports.HTTP[i]
go func() {
logger().Infof("http server is up and running on addr/port %s", address)
srv := &http.Server{
ReadTimeout: readTimeout,
ReadHeaderTimeout: readHeaderTimeout,
WriteTimeout: writeTimeout,
Handler: s.httpsMux,
}
if err := srv.Serve(listener); err != nil {
errCh <- fmt.Errorf("start http listener failed: %w", err)
}
}()
}
for i, listener := range s.httpsListeners {
listener := listener
address := s.cfg.Ports.HTTPS[i]
go func() {
logger().Infof("https server is up and running on addr/port %s", address)
server := http.Server{
2022-08-01 23:19:35 +02:00
Handler: s.httpsMux,
ReadTimeout: readTimeout,
2022-08-01 23:19:35 +02:00
ReadHeaderTimeout: readHeaderTimeout,
WriteTimeout: writeTimeout,
//nolint:gosec
TLSConfig: &tls.Config{
MinVersion: uint16(s.cfg.MinTLSServeVer),
CipherSuites: tlsCipherSuites(),
Certificates: []tls.Certificate{s.cert},
},
}
if err := server.ServeTLS(listener, "", ""); err != nil {
errCh <- fmt.Errorf("start https listener failed: %w", err)
}
}()
}
registerPrintConfigurationTrigger(ctx, s)
2020-01-12 18:23:35 +01:00
}
// Stop stops the server
func (s *Server) Stop(ctx context.Context) error {
2020-01-12 18:23:35 +01:00
logger().Info("Stopping server")
2021-10-02 22:54:56 +02:00
for _, server := range s.dnsServers {
if err := server.ShutdownContext(ctx); err != nil {
return fmt.Errorf("stop %s listener failed: %w", server.Net, err)
2021-10-02 22:54:56 +02:00
}
2020-01-12 18:23:35 +01:00
}
return nil
2020-01-12 18:23:35 +01:00
}
func extractClientIDFromHost(hostName string) string {
const clientIDPrefix = "id-"
if strings.HasPrefix(hostName, clientIDPrefix) && strings.Contains(hostName, ".") {
return hostName[len(clientIDPrefix):strings.Index(hostName, ".")]
}
return ""
}
func newRequest(
clientIP net.IP, clientID string,
protocol model.RequestProtocol, request *dns.Msg,
) *model.Request {
return &model.Request{
ClientIP: clientIP,
RequestClientID: clientID,
Protocol: protocol,
Req: request,
Log: log.Log().WithFields(logrus.Fields{
2020-01-12 18:23:35 +01:00
"question": util.QuestionToString(request.Question),
"client_ip": clientIP,
}),
RequestTS: time.Now(),
2020-01-12 18:23:35 +01:00
}
}
// OnRequest will be executed if a new DNS request is received
func (s *Server) OnRequest(
ctx context.Context, w dns.ResponseWriter,
clientIP net.IP, protocol model.RequestProtocol,
request *dns.Msg,
) {
logger().Debug("new request")
var hostName string
con, ok := w.(dns.ConnectionStater)
if ok && con.ConnectionState() != nil {
hostName = con.ConnectionState().ServerName
}
2020-01-12 18:23:35 +01:00
req := newRequest(clientIP, extractClientIDFromHost(hostName), protocol, request)
2020-01-12 18:23:35 +01:00
response, err := s.resolve(ctx, req)
2020-01-12 18:23:35 +01:00
if err != nil {
logger().Error("error on processing request:", err)
2021-03-26 22:46:58 +01:00
m := new(dns.Msg)
m.SetRcode(request, dns.RcodeServerFailure)
err := w.WriteMsg(m)
util.LogOnError("can't write message: ", err)
2020-01-12 18:23:35 +01:00
} else {
err := w.WriteMsg(response.Res)
util.LogOnError("can't write message: ", err)
}
}
2020-01-12 18:23:35 +01:00
func (s *Server) resolve(ctx context.Context, request *model.Request) (*model.Response, error) {
var response *model.Response
contextUpstreamTimeoutMultiplier := 100
timeoutDuration := time.Duration(contextUpstreamTimeoutMultiplier) * s.cfg.Upstreams.Timeout.ToDuration()
ctx, cancel := context.WithTimeout(ctx, timeoutDuration)
defer cancel()
switch {
case len(request.Req.Question) == 0:
m := new(dns.Msg)
m.SetRcode(request.Req, dns.RcodeFormatError)
request.Log.Error("query has no questions")
response = &model.Response{Res: m, RType: model.ResponseTypeCUSTOMDNS, Reason: "CUSTOM DNS"}
default:
var err error
response, err = s.queryResolver.Resolve(ctx, request)
if err != nil {
var upstreamErr *resolver.UpstreamServerError
if errors.As(err, &upstreamErr) {
response = &model.Response{Res: upstreamErr.Msg, RType: model.ResponseTypeRESOLVED, Reason: upstreamErr.Error()}
} else {
return nil, err
}
}
2020-01-12 18:23:35 +01:00
}
response.Res.MsgHdr.RecursionAvailable = request.Req.MsgHdr.RecursionDesired
// truncate if necessary
response.Res.Truncate(getMaxResponseSize(request))
// enable compression
response.Res.Compress = true
return response, nil
2020-01-12 18:23:35 +01:00
}
// returns EDNS UDP size or if not present, 512 for UDP and 64K for TCP
func getMaxResponseSize(req *model.Request) int {
edns := req.Req.IsEdns0()
if edns != nil && edns.UDPSize() > 0 {
return int(edns.UDPSize())
}
if req.Protocol == model.RequestProtocolTCP {
return dns.MaxMsgSize
}
return dns.MinMsgSize
}
// OnHealthCheck Handler for docker health check. Just returns OK code without delegating to resolver chain
2020-02-10 22:39:16 +01:00
func (s *Server) OnHealthCheck(w dns.ResponseWriter, request *dns.Msg) {
resp := new(dns.Msg)
resp.SetReply(request)
resp.Rcode = dns.RcodeSuccess
2021-01-19 21:52:24 +01:00
err := w.WriteMsg(resp)
util.LogOnError("can't write message: ", err)
2020-02-10 22:39:16 +01:00
}
func resolveClientIPAndProtocol(addr net.Addr) (ip net.IP, protocol model.RequestProtocol) {
2020-01-12 18:23:35 +01:00
if t, ok := addr.(*net.UDPAddr); ok {
2021-09-09 22:57:05 +02:00
return t.IP, model.RequestProtocolUDP
2020-01-12 18:23:35 +01:00
} else if t, ok := addr.(*net.TCPAddr); ok {
2021-09-09 22:57:05 +02:00
return t.IP, model.RequestProtocolTCP
2020-01-12 18:23:35 +01:00
}
2021-09-09 22:57:05 +02:00
return nil, model.RequestProtocolUDP
2020-01-12 18:23:35 +01:00
}