Skip to content

Commit

Permalink
plugin/timeouts - Allow ability to configure listening server timeouts (
Browse files Browse the repository at this point in the history
  • Loading branch information
rlees85 authored Dec 28, 2022
1 parent 6c9b49f commit e7ad486
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 14 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# only add build artifacts concerning coredns - no editor related files
coredns
coredns.exe
Corefile
build/
release/
vendor/
6 changes: 6 additions & 0 deletions core/dnsserver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"fmt"
"net/http"
"time"

"github.com/coredns/caddy"
"github.com/coredns/coredns/plugin"
Expand Down Expand Up @@ -53,6 +54,11 @@ type Config struct {
// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS).
TLSConfig *tls.Config

// Timeouts for TCP, TLS and HTTPS servers.
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration

// TSIG secrets, [name]key.
TsigSecret map[string]string

Expand Down
6 changes: 5 additions & 1 deletion core/dnsserver/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ func (h *dnsContext) MakeServers() ([]caddy.Server, error) {

// Fork TLSConfig for each encrypted connection
c.TLSConfig = c.firstConfigInBlock.TLSConfig.Clone()
c.ReadTimeout = c.firstConfigInBlock.ReadTimeout
c.WriteTimeout = c.firstConfigInBlock.WriteTimeout
c.IdleTimeout = c.firstConfigInBlock.IdleTimeout
c.TsigSecret = c.firstConfigInBlock.TsigSecret
}

Expand Down Expand Up @@ -223,7 +226,8 @@ func (c *Config) AddPlugin(m plugin.Plugin) {
}

// registerHandler adds a handler to a site's handler registration. Handlers
// use this to announce that they exist to other plugin.
//
// use this to announce that they exist to other plugin.
func (c *Config) registerHandler(h plugin.Handler) {
if c.registry == nil {
c.registry = make(map[string]plugin.Handler)
Expand Down
40 changes: 35 additions & 5 deletions core/dnsserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ type Server struct {
debug bool // disable recover()
stacktrace bool // enable stacktrace in recover error log
classChaos bool // allow non-INET class queries
idleTimeout time.Duration // Idle timeout for TCP
readTimeout time.Duration // Read timeout for TCP
writeTimeout time.Duration // Write timeout for TCP

tsigSecret map[string]string
}
Expand All @@ -60,6 +63,9 @@ func NewServer(addr string, group []*Config) (*Server, error) {
Addr: addr,
zones: make(map[string][]*Config),
graceTimeout: 5 * time.Second,
idleTimeout: 10 * time.Second,
readTimeout: 3 * time.Second,
writeTimeout: 5 * time.Second,
tsigSecret: make(map[string]string),
}

Expand All @@ -81,6 +87,17 @@ func NewServer(addr string, group []*Config) (*Server, error) {
// append the config to the zone's configs
s.zones[site.Zone] = append(s.zones[site.Zone], site)

// set timeouts
if site.ReadTimeout != 0 {
s.readTimeout = site.ReadTimeout
}
if site.WriteTimeout != 0 {
s.writeTimeout = site.WriteTimeout
}
if site.IdleTimeout != 0 {
s.idleTimeout = site.IdleTimeout
}

// copy tsig secrets
for key, secret := range site.TsigSecret {
s.tsigSecret[key] = secret
Expand Down Expand Up @@ -130,11 +147,22 @@ var _ caddy.GracefulServer = &Server{}
// This implements caddy.TCPServer interface.
func (s *Server) Serve(l net.Listener) error {
s.m.Lock()
s.server[tcp] = &dns.Server{Listener: l, Net: "tcp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
ctx := context.WithValue(context.Background(), Key{}, s)
ctx = context.WithValue(ctx, LoopKey{}, 0)
s.ServeDNS(ctx, w, r)
}), TsigSecret: s.tsigSecret}

s.server[tcp] = &dns.Server{Listener: l,
Net: "tcp",
TsigSecret: s.tsigSecret,
MaxTCPQueries: tcpMaxQueries,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
IdleTimeout: func() time.Duration {
return s.idleTimeout
},
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
ctx := context.WithValue(context.Background(), Key{}, s)
ctx = context.WithValue(ctx, LoopKey{}, 0)
s.ServeDNS(ctx, w, r)
})}

s.m.Unlock()

return s.server[tcp].ActivateAndServe()
Expand Down Expand Up @@ -404,6 +432,8 @@ func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int
const (
tcp = 0
udp = 1

tcpMaxQueries = -1
)

type (
Expand Down
6 changes: 3 additions & 3 deletions core/dnsserver/server_https.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) {
}

srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
IdleTimeout: s.idleTimeout,
ErrorLog: stdlog.New(&loggerAdapter{}, "", 0),
}
sh := &ServerHTTPS{
Expand Down
24 changes: 19 additions & 5 deletions core/dnsserver/server_tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"fmt"
"net"
"time"

"github.com/coredns/caddy"
"github.com/coredns/coredns/plugin/pkg/reuseport"
Expand Down Expand Up @@ -50,11 +51,20 @@ func (s *ServerTLS) Serve(l net.Listener) error {
}

// Only fill out the TCP server for this one.
s.server[tcp] = &dns.Server{Listener: l, Net: "tcp-tls", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
ctx := context.WithValue(context.Background(), Key{}, s.Server)
ctx = context.WithValue(ctx, LoopKey{}, 0)
s.ServeDNS(ctx, w, r)
})}
s.server[tcp] = &dns.Server{Listener: l,
Net: "tcp-tls",
MaxTCPQueries: tlsMaxQueries,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
IdleTimeout: func() time.Duration {
return s.idleTimeout
},
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
ctx := context.WithValue(context.Background(), Key{}, s.Server)
ctx = context.WithValue(ctx, LoopKey{}, 0)
s.ServeDNS(ctx, w, r)
})}

s.m.Unlock()

return s.server[tcp].ActivateAndServe()
Expand Down Expand Up @@ -87,3 +97,7 @@ func (s *ServerTLS) OnStartupComplete() {
fmt.Print(out)
}
}

const (
tlsMaxQueries = -1
)
1 change: 1 addition & 0 deletions core/dnsserver/zdirectives.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var Directives = []string{
"geoip",
"cancel",
"tls",
"timeouts",
"reload",
"nsid",
"bufsize",
Expand Down
1 change: 1 addition & 0 deletions core/plugin/zplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
_ "github.com/coredns/coredns/plugin/secondary"
_ "github.com/coredns/coredns/plugin/sign"
_ "github.com/coredns/coredns/plugin/template"
_ "github.com/coredns/coredns/plugin/timeouts"
_ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/trace"
_ "github.com/coredns/coredns/plugin/transfer"
Expand Down
1 change: 1 addition & 0 deletions plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ metadata:metadata
geoip:geoip
cancel:cancel
tls:tls
timeouts:timeouts
reload:reload
nsid:nsid
bufsize:bufsize
Expand Down
26 changes: 26 additions & 0 deletions plugin/pkg/durations/durations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package durations

import (
"fmt"
"strconv"
"time"
)

// NewDurationFromArg returns a time.Duration from a configuration argument
// (string) which has come from the Corefile. The argument has some basic
// validation applied before returning a time.Duration. If the argument has no
// time unit specified and is numeric the argument will be treated as seconds
// rather than GO's default of nanoseconds.
func NewDurationFromArg(arg string) (time.Duration, error) {
_, err := strconv.Atoi(arg)
if err == nil {
arg = arg + "s"
}

d, err := time.ParseDuration(arg)
if err != nil {
return 0, fmt.Errorf("failed to parse duration '%s'", arg)
}

return d, nil
}
51 changes: 51 additions & 0 deletions plugin/pkg/durations/durations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package durations

import (
"testing"
"time"
)

func TestNewDurationFromArg(t *testing.T) {
tests := []struct {
name string
arg string
wantErr bool
want time.Duration
}{
{
name: "valid GO duration - seconds",
arg: "30s",
want: 30 * time.Second,
},
{
name: "valid GO duration - minutes",
arg: "2m",
want: 2 * time.Minute,
},
{
name: "number - fallback to seconds",
arg: "30",
want: 30 * time.Second,
},
{
name: "invalid duration",
arg: "twenty seconds",
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := NewDurationFromArg(test.arg)
if test.wantErr && err == nil {
t.Error("error was expected")
}
if !test.wantErr && err != nil {
t.Error("error was not expected")
}

if test.want != actual {
t.Errorf("expected '%v' got '%v'", test.want, actual)
}
})
}
}
76 changes: 76 additions & 0 deletions plugin/timeouts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# timeouts

## Name

*timeouts* - allows you to configure the server read, write and idle timeouts for the TCP, TLS and DoH servers.

## Description

CoreDNS is configured with sensible timeouts for server connections by default.
However in some cases for example where CoreDNS is serving over a slow mobile
data connection the default timeouts are not optimal.

Additionally some routers hold open connections when using DNS over TLS or DNS
over HTTPS. Allowing a longer idle timeout helps performance and reduces issues
with such routers.

The *timeouts* "plugin" allows you to configure CoreDNS server read, write and
idle timeouts.

## Syntax

~~~ txt
timeouts {
read DURATION
write DURATION
idle DURATION
}
~~~

For any timeouts that are not provided, default values are used which may vary
depending on the server type. At least one timeout must be specified otherwise
the entire timeouts block should be omitted.

## Examples

Start a DNS-over-TLS server that picks up incoming DNS-over-TLS queries on port
5553 and uses the nameservers defined in `/etc/resolv.conf` to resolve the
query. This proxy path uses plain old DNS. A 10 second read timeout, 20
second write timeout and a 60 second idle timeout have been configured.

~~~
tls://.:5553 {
tls cert.pem key.pem ca.pem
timeouts {
read 10s
write 20s
idle 60s
}
forward . /etc/resolv.conf
}
~~~

Start a DNS-over-HTTPS server that is similar to the previous example. Only the
read timeout has been configured for 1 minute.

~~~
https://. {
tls cert.pem key.pem ca.pem
timeouts {
read 1m
}
forward . /etc/resolv.conf
}
~~~

Start a standard TCP/UDP server on port 1053. A read and write timeout has been
configured. The timeouts are only applied to the TCP side of the server.
~~~
.:1053 {
timeouts {
read 15s
write 30s
}
forward . /etc/resolv.conf
}
~~~
Loading

0 comments on commit e7ad486

Please sign in to comment.