aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar João Henri <joao.henri.cr@gmail.com> 2023-07-31 16:34:31 -0300
committerGravatar GitHub <noreply@github.com> 2023-07-31 15:34:31 -0400
commitcc7a36463325c8cdb7864c879f20a8259df4e4c3 (patch)
treeee125e857d184ded64abe721d5f258dd3a1739ee
parentb7c9d3e155418cb1dccc6de50e4fddce6137d3cb (diff)
downloadcoredns-cc7a36463325c8cdb7864c879f20a8259df4e4c3.tar.gz
coredns-cc7a36463325c8cdb7864c879f20a8259df4e4c3.tar.zst
coredns-cc7a36463325c8cdb7864c879f20a8259df4e4c3.zip
[RFC-9250]: Add QUIC server support (#6182)
Add DNS-over-QUIC server Signed-off-by: jaehnri <joao.henri.cr@gmail.com> Signed-off-by: João Henri <joao.henri.cr@gmail.com>
-rw-r--r--README.md18
-rw-r--r--core/dnsserver/quic.go60
-rw-r--r--core/dnsserver/quic_test.go20
-rw-r--r--core/dnsserver/register.go9
-rw-r--r--core/dnsserver/server_quic.go346
-rw-r--r--core/dnsserver/server_test.go5
-rw-r--r--go.mod8
-rw-r--r--go.sum12
-rw-r--r--man/coredns-timeouts.7103
-rw-r--r--plugin/metrics/README.md1
-rw-r--r--plugin/metrics/vars/vars.go7
-rw-r--r--plugin/pkg/parse/host.go2
-rw-r--r--plugin/pkg/parse/transport.go4
-rw-r--r--plugin/pkg/transport/transport.go3
-rw-r--r--test/quic_test.go165
15 files changed, 759 insertions, 4 deletions
diff --git a/README.md b/README.md
index 9ec548cf6..c7c08e73d 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,12 @@ CoreDNS is a fast and flexible DNS server. The key word here is *flexible*: with
are able to do what you want with your DNS data by utilizing plugins. If some functionality is not
provided out of the box you can add it by [writing a plugin](https://coredns.io/explugins).
-CoreDNS can listen for DNS requests coming in over UDP/TCP (go'old DNS), TLS ([RFC
-7858](https://tools.ietf.org/html/rfc7858)), also called DoT, DNS over HTTP/2 - DoH -
-([RFC 8484](https://tools.ietf.org/html/rfc8484)) and [gRPC](https://grpc.io) (not a standard).
+CoreDNS can listen for DNS requests coming in over:
+* UDP/TCP (go'old DNS).
+* TLS - DoT ([RFC 7858](https://tools.ietf.org/html/rfc7858)).
+* DNS over HTTP/2 - DoH ([RFC 8484](https://tools.ietf.org/html/rfc8484)).
+* DNS over QUIC - DoQ ([RFC 9250](https://tools.ietf.org/html/rfc9250)).
+* [gRPC](https://grpc.io) (not a standard).
Currently CoreDNS is able to:
@@ -211,6 +214,15 @@ tls://example.org grpc://example.org {
}
~~~
+Similarly, for QUIC (DoQ):
+
+~~~ corefile
+quic://example.org {
+ whoami
+ tls mycert mykey
+}
+~~~
+
And for DNS over HTTP/2 (DoH) use:
~~~ corefile
diff --git a/core/dnsserver/quic.go b/core/dnsserver/quic.go
new file mode 100644
index 000000000..5c2890a72
--- /dev/null
+++ b/core/dnsserver/quic.go
@@ -0,0 +1,60 @@
+package dnsserver
+
+import (
+ "encoding/binary"
+ "net"
+
+ "github.com/miekg/dns"
+ "github.com/quic-go/quic-go"
+)
+
+type DoQWriter struct {
+ localAddr net.Addr
+ remoteAddr net.Addr
+ stream quic.Stream
+ Msg *dns.Msg
+}
+
+func (w *DoQWriter) Write(b []byte) (int, error) {
+ b = AddPrefix(b)
+ return w.stream.Write(b)
+}
+
+func (w *DoQWriter) WriteMsg(m *dns.Msg) error {
+ bytes, err := m.Pack()
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(bytes)
+ if err != nil {
+ return err
+ }
+
+ return w.Close()
+}
+
+// Close sends the STREAM FIN signal.
+// The server MUST send the response(s) on the same stream and MUST
+// indicate, after the last response, through the STREAM FIN
+// mechanism that no further data will be sent on that stream.
+// See https://www.rfc-editor.org/rfc/rfc9250#section-4.2-7
+func (w *DoQWriter) Close() error {
+ return w.stream.Close()
+}
+
+// AddPrefix adds a 2-byte prefix with the DNS message length.
+func AddPrefix(b []byte) (m []byte) {
+ m = make([]byte, 2+len(b))
+ binary.BigEndian.PutUint16(m, uint16(len(b)))
+ copy(m[2:], b)
+
+ return m
+}
+
+// These methods implement the dns.ResponseWriter interface from Go DNS.
+func (w *DoQWriter) TsigStatus() error { return nil }
+func (w *DoQWriter) TsigTimersOnly(b bool) {}
+func (w *DoQWriter) Hijack() {}
+func (w *DoQWriter) LocalAddr() net.Addr { return w.localAddr }
+func (w *DoQWriter) RemoteAddr() net.Addr { return w.remoteAddr }
diff --git a/core/dnsserver/quic_test.go b/core/dnsserver/quic_test.go
new file mode 100644
index 000000000..98d658a9a
--- /dev/null
+++ b/core/dnsserver/quic_test.go
@@ -0,0 +1,20 @@
+package dnsserver
+
+import (
+ "testing"
+)
+
+func TestDoQWriterAddPrefix(t *testing.T) {
+ byteArray := []byte{0x1, 0x2, 0x3}
+
+ byteArrayWithPrefix := AddPrefix(byteArray)
+
+ if len(byteArrayWithPrefix) != 5 {
+ t.Error("Expected byte array with prefix to have length of 5")
+ }
+
+ size := int16(byteArrayWithPrefix[0])<<8 | int16(byteArrayWithPrefix[1])
+ if size != 3 {
+ t.Errorf("Expected prefixed size to be 3, got: %d", size)
+ }
+}
diff --git a/core/dnsserver/register.go b/core/dnsserver/register.go
index b81573ff7..ae001b9f2 100644
--- a/core/dnsserver/register.go
+++ b/core/dnsserver/register.go
@@ -82,6 +82,8 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy
port = Port
case transport.TLS:
port = transport.TLSPort
+ case transport.QUIC:
+ port = transport.QUICPort
case transport.GRPC:
port = transport.GRPCPort
case transport.HTTPS:
@@ -174,6 +176,13 @@ func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
}
servers = append(servers, s)
+ case transport.QUIC:
+ s, err := NewServerQUIC(addr, group)
+ if err != nil {
+ return nil, err
+ }
+ servers = append(servers, s)
+
case transport.GRPC:
s, err := NewServergRPC(addr, group)
if err != nil {
diff --git a/core/dnsserver/server_quic.go b/core/dnsserver/server_quic.go
new file mode 100644
index 000000000..ba7867cfb
--- /dev/null
+++ b/core/dnsserver/server_quic.go
@@ -0,0 +1,346 @@
+package dnsserver
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "net"
+
+ "github.com/coredns/coredns/plugin/metrics/vars"
+ clog "github.com/coredns/coredns/plugin/pkg/log"
+ "github.com/coredns/coredns/plugin/pkg/reuseport"
+ "github.com/coredns/coredns/plugin/pkg/transport"
+
+ "github.com/miekg/dns"
+ "github.com/quic-go/quic-go"
+)
+
+const (
+ // DoQCodeNoError is used when the connection or stream needs to be
+ // closed, but there is no error to signal.
+ DoQCodeNoError quic.ApplicationErrorCode = 0
+
+ // DoQCodeInternalError signals that the DoQ implementation encountered
+ // an internal error and is incapable of pursuing the transaction or the
+ // connection.
+ DoQCodeInternalError quic.ApplicationErrorCode = 1
+
+ // DoQCodeProtocolError signals that the DoQ implementation encountered
+ // a protocol error and is forcibly aborting the connection.
+ DoQCodeProtocolError quic.ApplicationErrorCode = 2
+)
+
+// ServerQUIC represents an instance of a DNS-over-QUIC server.
+type ServerQUIC struct {
+ *Server
+ listenAddr net.Addr
+ tlsConfig *tls.Config
+ quicConfig *quic.Config
+ quicListener *quic.Listener
+}
+
+// NewServerQUIC returns a new CoreDNS QUIC server and compiles all plugin in to it.
+func NewServerQUIC(addr string, group []*Config) (*ServerQUIC, error) {
+ s, err := NewServer(addr, group)
+ if err != nil {
+ return nil, err
+ }
+ // The *tls* plugin must make sure that multiple conflicting
+ // TLS configuration returns an error: it can only be specified once.
+ var tlsConfig *tls.Config
+ for _, z := range s.zones {
+ for _, conf := range z {
+ // Should we error if some configs *don't* have TLS?
+ tlsConfig = conf.TLSConfig
+ }
+ }
+
+ if tlsConfig != nil {
+ tlsConfig.NextProtos = []string{"doq"}
+ }
+
+ var quicConfig *quic.Config
+ quicConfig = &quic.Config{
+ MaxIdleTimeout: s.idleTimeout,
+ MaxIncomingStreams: math.MaxUint16,
+ MaxIncomingUniStreams: math.MaxUint16,
+ // Enable 0-RTT by default for all connections on the server-side.
+ Allow0RTT: true,
+ }
+
+ return &ServerQUIC{Server: s, tlsConfig: tlsConfig, quicConfig: quicConfig}, nil
+}
+
+// ServePacket implements caddy.UDPServer interface.
+func (s *ServerQUIC) ServePacket(p net.PacketConn) error {
+ s.m.Lock()
+ s.listenAddr = s.quicListener.Addr()
+ s.m.Unlock()
+
+ return s.ServeQUIC()
+}
+
+// ServeQUIC listens for incoming QUIC packets.
+func (s *ServerQUIC) ServeQUIC() error {
+ for {
+ conn, err := s.quicListener.Accept(context.Background())
+ if err != nil {
+ if s.isExpectedErr(err) {
+ s.closeQUICConn(conn, DoQCodeNoError)
+ return err
+ }
+
+ s.closeQUICConn(conn, DoQCodeInternalError)
+ return err
+ }
+
+ go s.serveQUICConnection(conn)
+ }
+}
+
+// serveQUICConnection handles a new QUIC connection. It waits for new streams
+// and passes them to serveQUICStream.
+func (s *ServerQUIC) serveQUICConnection(conn quic.Connection) {
+ for {
+ // In DoQ, one query consumes one stream.
+ // The client MUST select the next available client-initiated bidirectional
+ // stream for each subsequent query on a QUIC connection.
+ stream, err := conn.AcceptStream(context.Background())
+ if err != nil {
+ if s.isExpectedErr(err) {
+ s.closeQUICConn(conn, DoQCodeNoError)
+ return
+ }
+
+ s.closeQUICConn(conn, DoQCodeInternalError)
+ return
+ }
+
+ go s.serveQUICStream(stream, conn)
+ }
+}
+
+func (s *ServerQUIC) serveQUICStream(stream quic.Stream, conn quic.Connection) {
+ buf, err := readDOQMessage(stream)
+
+ // io.EOF does not really mean that there's any error, it is just
+ // the STREAM FIN indicating that there will be no data to read
+ // anymore from this stream.
+ if err != nil && err != io.EOF {
+ s.closeQUICConn(conn, DoQCodeProtocolError)
+
+ return
+ }
+
+ req := &dns.Msg{}
+ err = req.Unpack(buf)
+ if err != nil {
+ clog.Debugf("unpacking quic packet: %s", err)
+ s.closeQUICConn(conn, DoQCodeProtocolError)
+
+ return
+ }
+
+ if !validRequest(req) {
+ // If a peer encounters such an error condition, it is considered a
+ // fatal error. It SHOULD forcibly abort the connection using QUIC's
+ // CONNECTION_CLOSE mechanism and SHOULD use the DoQ error code
+ // DOQ_PROTOCOL_ERROR.
+ // See https://www.rfc-editor.org/rfc/rfc9250#section-4.3.3-3
+ s.closeQUICConn(conn, DoQCodeProtocolError)
+
+ return
+ }
+
+ w := &DoQWriter{
+ localAddr: conn.LocalAddr(),
+ remoteAddr: conn.RemoteAddr(),
+ stream: stream,
+ Msg: req,
+ }
+
+ dnsCtx := context.WithValue(stream.Context(), Key{}, s.Server)
+ dnsCtx = context.WithValue(dnsCtx, LoopKey{}, 0)
+ s.ServeDNS(dnsCtx, w, req)
+ s.countResponse(DoQCodeNoError)
+}
+
+// ListenPacket implements caddy.UDPServer interface.
+func (s *ServerQUIC) ListenPacket() (net.PacketConn, error) {
+ p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.QUIC+"://"):])
+ if err != nil {
+ return nil, err
+ }
+
+ s.m.Lock()
+ defer s.m.Unlock()
+
+ s.quicListener, err = quic.Listen(p, s.tlsConfig, s.quicConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return p, nil
+}
+
+// OnStartupComplete lists the sites served by this server
+// and any relevant information, assuming Quiet is false.
+func (s *ServerQUIC) OnStartupComplete() {
+ if Quiet {
+ return
+ }
+
+ out := startUpZones(transport.QUIC+"://", s.Addr, s.zones)
+ if out != "" {
+ fmt.Print(out)
+ }
+}
+
+// Stop stops the server non-gracefully. It blocks until the server is totally stopped.
+func (s *ServerQUIC) Stop() error {
+ s.m.Lock()
+ defer s.m.Unlock()
+
+ if s.quicListener != nil {
+ return s.quicListener.Close()
+ }
+
+ return nil
+}
+
+// Serve implements caddy.TCPServer interface.
+func (s *ServerQUIC) Serve(l net.Listener) error { return nil }
+
+// Listen implements caddy.TCPServer interface.
+func (s *ServerQUIC) Listen() (net.Listener, error) { return nil, nil }
+
+// closeQUICConn quietly closes the QUIC connection.
+func (s *ServerQUIC) closeQUICConn(conn quic.Connection, code quic.ApplicationErrorCode) {
+ if conn == nil {
+ return
+ }
+
+ clog.Debugf("closing quic conn %s with code %d", conn.LocalAddr(), code)
+ err := conn.CloseWithError(code, "")
+ if err != nil {
+ clog.Debugf("failed to close quic connection with code %d: %s", code, err)
+ }
+
+ // DoQCodeNoError metrics are already registered after s.ServeDNS()
+ if code != DoQCodeNoError {
+ s.countResponse(code)
+ }
+}
+
+// validRequest checks for protocol errors in the unpacked DNS message.
+// See https://www.rfc-editor.org/rfc/rfc9250.html#name-protocol-errors
+func validRequest(req *dns.Msg) (ok bool) {
+ // 1. a client or server receives a message with a non-zero Message ID.
+ if req.Id != 0 {
+ return false
+ }
+
+ // 2. an implementation receives a message containing the edns-tcp-keepalive
+ // EDNS(0) Option [RFC7828].
+ if opt := req.IsEdns0(); opt != nil {
+ for _, option := range opt.Option {
+ if option.Option() == dns.EDNS0TCPKEEPALIVE {
+ clog.Debug("client sent EDNS0 TCP keepalive option")
+
+ return false
+ }
+ }
+ }
+
+ // 3. the client or server does not indicate the expected STREAM FIN after
+ // sending requests or responses.
+ //
+ // This is quite problematic to validate this case since this would imply
+ // we have to wait until STREAM FIN is arrived before we start processing
+ // the message. So we're consciously ignoring this case in this
+ // implementation.
+
+ // 4. a server receives a "replayable" transaction in 0-RTT data
+ //
+ // The information necessary to validate this is not exposed by quic-go.
+
+ return true
+}
+
+// readDOQMessage reads a DNS over QUIC (DOQ) message from the given stream
+// and returns the message bytes.
+// Drafts of the RFC9250 did not require the 2-byte prefixed message length.
+// Thus, we are only supporting the official version (DoQ v1).
+func readDOQMessage(r io.Reader) ([]byte, error) {
+ // All DNS messages (queries and responses) sent over DoQ connections MUST
+ // be encoded as a 2-octet length field followed by the message content as
+ // specified in [RFC1035].
+ // See https://www.rfc-editor.org/rfc/rfc9250.html#section-4.2-4
+ sizeBuf := make([]byte, 2)
+ _, err := io.ReadFull(r, sizeBuf)
+ if err != nil {
+ return nil, err
+ }
+
+ size := binary.BigEndian.Uint16(sizeBuf)
+
+ if size == 0 {
+ return nil, fmt.Errorf("message size is 0: probably unsupported DoQ version")
+ }
+
+ buf := make([]byte, size)
+ _, err = io.ReadFull(r, buf)
+
+ // A client or server receives a STREAM FIN before receiving all the bytes
+ // for a message indicated in the 2-octet length field.
+ // See https://www.rfc-editor.org/rfc/rfc9250#section-4.3.3-2.2
+ if size != uint16(len(buf)) {
+ return nil, fmt.Errorf("message size does not match 2-byte prefix")
+ }
+
+ return buf, err
+}
+
+// isExpectedErr returns true if err is an expected error, likely related to
+// the current implementation.
+func (s *ServerQUIC) isExpectedErr(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ // This error is returned when the QUIC listener was closed by us. As
+ // graceful shutdown is not implemented, the connection will be abruptly
+ // closed but there is no error to signal.
+ if errors.Is(err, quic.ErrServerClosed) {
+ return true
+ }
+
+ // This error happens when the connection was closed due to a DoQ
+ // protocol error but there's still something to read in the closed stream.
+ // For example, when the message was sent without the prefixed length.
+ var qAppErr *quic.ApplicationError
+ if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 2 {
+ return true
+ }
+
+ // When a connection hits the idle timeout, quic.AcceptStream() returns
+ // an IdleTimeoutError. In this, case, we should just drop the connection
+ // with DoQCodeNoError.
+ var qIdleErr *quic.IdleTimeoutError
+ return errors.As(err, &qIdleErr)
+}
+
+func (s *ServerQUIC) countResponse(code quic.ApplicationErrorCode) {
+ switch code {
+ case DoQCodeNoError:
+ vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x0").Inc()
+ case DoQCodeInternalError:
+ vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x1").Inc()
+ case DoQCodeProtocolError:
+ vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x2").Inc()
+ }
+}
diff --git a/core/dnsserver/server_test.go b/core/dnsserver/server_test.go
index c52b6a21a..9b9b0a35b 100644
--- a/core/dnsserver/server_test.go
+++ b/core/dnsserver/server_test.go
@@ -48,6 +48,11 @@ func TestNewServer(t *testing.T) {
if err != nil {
t.Errorf("Expected no error for NewServerTLS, got %s", err)
}
+
+ _, err = NewServerQUIC("127.0.0.1:53", []*Config{testConfig("quic", testPlugin{})})
+ if err != nil {
+ t.Errorf("Expected no error for NewServerQUIC, got %s", err)
+ }
}
func TestDebug(t *testing.T) {
diff --git a/go.mod b/go.mod
index f71d5cfeb..364a2574a 100644
--- a/go.mod
+++ b/go.mod
@@ -24,6 +24,7 @@ require (
github.com/prometheus/client_golang v1.16.0
github.com/prometheus/client_model v0.4.0
github.com/prometheus/common v0.44.0
+ github.com/quic-go/quic-go v0.35.1
go.etcd.io/etcd/api/v3 v3.5.9
go.etcd.io/etcd/client/v3 v3.5.9
golang.org/x/crypto v0.11.0
@@ -70,13 +71,16 @@ require (
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
+ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/pprof v0.0.0-20230509042627-b1315fad0c5a // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
@@ -90,12 +94,15 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/onsi/ginkgo/v2 v2.9.1 // indirect
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
github.com/outcaste-io/ristretto v0.2.1 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
+ github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
+ github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tinylib/msgp v1.1.6 // indirect
@@ -106,6 +113,7 @@ require (
go.uber.org/zap v1.17.0 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
+ golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
diff --git a/go.sum b/go.sum
index 9f34ff61c..b01829e46 100644
--- a/go.sum
+++ b/go.sum
@@ -134,6 +134,7 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -168,6 +169,7 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20230509042627-b1315fad0c5a h1:PEOGDI1kkyW37YqPWHLHc+D20D9+87Wt12TCcfTUo5Q=
+github.com/google/pprof v0.0.0-20230509042627-b1315fad0c5a/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -226,9 +228,9 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
-github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
+github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
@@ -264,6 +266,12 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
+github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
+github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
+github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
+github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
+github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
+github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@@ -329,6 +337,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
+golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
diff --git a/man/coredns-timeouts.7 b/man/coredns-timeouts.7
new file mode 100644
index 000000000..a283f6cbd
--- /dev/null
+++ b/man/coredns-timeouts.7
@@ -0,0 +1,103 @@
+.\" Generated by Mmark Markdown Processer - mmark.miek.nl
+.TH "COREDNS-TIMEOUTS" 7 "July 2023" "CoreDNS" "CoreDNS Plugins"
+
+.SH "NAME"
+.PP
+\fItimeouts\fP - allows you to configure the server read, write and idle timeouts for the TCP, TLS and DoH servers.
+
+.SH "DESCRIPTION"
+.PP
+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.
+
+.PP
+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.
+
+.PP
+The \fItimeouts\fP "plugin" allows you to configure CoreDNS server read, write and
+idle timeouts.
+
+.SH "SYNTAX"
+.PP
+.RS
+
+.nf
+timeouts {
+ read DURATION
+ write DURATION
+ idle DURATION
+}
+
+.fi
+.RE
+
+.PP
+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.
+
+.SH "EXAMPLES"
+.PP
+Start a DNS-over-TLS server that picks up incoming DNS-over-TLS queries on port
+5553 and uses the nameservers defined in \fB\fC/etc/resolv.conf\fR 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.
+
+.PP
+.RS
+
+.nf
+tls://.:5553 {
+ tls cert.pem key.pem ca.pem
+ timeouts {
+ read 10s
+ write 20s
+ idle 60s
+ }
+ forward . /etc/resolv.conf
+}
+
+.fi
+.RE
+
+.PP
+Start a DNS-over-HTTPS server that is similar to the previous example. Only the
+read timeout has been configured for 1 minute.
+
+.PP
+.RS
+
+.nf
+https://. {
+ tls cert.pem key.pem ca.pem
+ timeouts {
+ read 1m
+ }
+ forward . /etc/resolv.conf
+}
+
+.fi
+.RE
+
+.PP
+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.
+
+.PP
+.RS
+
+.nf
+\&.:1053 {
+ timeouts {
+ read 15s
+ write 30s
+ }
+ forward . /etc/resolv.conf
+}
+
+.fi
+.RE
+
diff --git a/plugin/metrics/README.md b/plugin/metrics/README.md
index ec5da10d0..144a5d1c6 100644
--- a/plugin/metrics/README.md
+++ b/plugin/metrics/README.md
@@ -21,6 +21,7 @@ the following metrics are exported:
* `coredns_dns_response_size_bytes{server, zone, view, proto}` - response size in bytes.
* `coredns_dns_responses_total{server, zone, view, rcode, plugin}` - response per zone, rcode and plugin.
* `coredns_dns_https_responses_total{server, status}` - responses per server and http status code.
+* `coredns_dns_quic_responses_total{server, status}` - responses per server and QUIC application code.
* `coredns_plugin_enabled{server, zone, view, name}` - indicates whether a plugin is enabled on per server, zone and view basis.
Almost each counter has a label `zone` which is the zonename used for the request/response.
diff --git a/plugin/metrics/vars/vars.go b/plugin/metrics/vars/vars.go
index f0cf829c9..6de75c044 100644
--- a/plugin/metrics/vars/vars.go
+++ b/plugin/metrics/vars/vars.go
@@ -72,6 +72,13 @@ var (
Name: "https_responses_total",
Help: "Counter of DoH responses per server and http status code.",
}, []string{"server", "status"})
+
+ QUICResponsesCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "quic_responses_total",
+ Help: "Counter of DoQ responses per server and QUIC application code.",
+ }, []string{"server", "status"})
)
const (
diff --git a/plugin/pkg/parse/host.go b/plugin/pkg/parse/host.go
index c396dc853..f90e4fc77 100644
--- a/plugin/pkg/parse/host.go
+++ b/plugin/pkg/parse/host.go
@@ -61,6 +61,8 @@ func HostPortOrFile(s ...string) ([]string, error) {
ss = net.JoinHostPort(host, transport.Port)
case transport.TLS:
ss = transport.TLS + "://" + net.JoinHostPort(host, transport.TLSPort)
+ case transport.QUIC:
+ ss = transport.QUIC + "://" + net.JoinHostPort(host, transport.QUICPort)
case transport.GRPC:
ss = transport.GRPC + "://" + net.JoinHostPort(host, transport.GRPCPort)
case transport.HTTPS:
diff --git a/plugin/pkg/parse/transport.go b/plugin/pkg/parse/transport.go
index 0da640856..f0cf1c249 100644
--- a/plugin/pkg/parse/transport.go
+++ b/plugin/pkg/parse/transport.go
@@ -19,6 +19,10 @@ func Transport(s string) (trans string, addr string) {
s = s[len(transport.DNS+"://"):]
return transport.DNS, s
+ case strings.HasPrefix(s, transport.QUIC+"://"):
+ s = s[len(transport.QUIC+"://"):]
+ return transport.QUIC, s
+
case strings.HasPrefix(s, transport.GRPC+"://"):
s = s[len(transport.GRPC+"://"):]
return transport.GRPC, s
diff --git a/plugin/pkg/transport/transport.go b/plugin/pkg/transport/transport.go
index e23b6d647..cdb2c79b7 100644
--- a/plugin/pkg/transport/transport.go
+++ b/plugin/pkg/transport/transport.go
@@ -4,6 +4,7 @@ package transport
const (
DNS = "dns"
TLS = "tls"
+ QUIC = "quic"
GRPC = "grpc"
HTTPS = "https"
UNIX = "unix"
@@ -15,6 +16,8 @@ const (
Port = "53"
// TLSPort is the default port for DNS-over-TLS.
TLSPort = "853"
+ // QUICPort is the default port for DNS-over-QUIC.
+ QUICPort = "853"
// GRPCPort is the default port for DNS-over-gRPC.
GRPCPort = "443"
// HTTPSPort is the default port for DNS-over-HTTPS.
diff --git a/test/quic_test.go b/test/quic_test.go
new file mode 100644
index 000000000..002d232a9
--- /dev/null
+++ b/test/quic_test.go
@@ -0,0 +1,165 @@
+package test
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/binary"
+ "errors"
+ "io"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ ctls "github.com/coredns/coredns/plugin/pkg/tls"
+
+ "github.com/miekg/dns"
+ "github.com/quic-go/quic-go"
+)
+
+var quicCorefile = `quic://.:0 {
+ tls ../plugin/tls/test_cert.pem ../plugin/tls/test_key.pem ../plugin/tls/test_ca.pem
+ whoami
+ }`
+
+func TestQUIC(t *testing.T) {
+ q, udp, _, err := CoreDNSServerAndPorts(quicCorefile)
+ if err != nil {
+ t.Fatalf("Could not get CoreDNS serving instance: %s", err)
+ }
+ defer q.Stop()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ conn, err := quic.DialAddr(ctx, convertAddress(udp), generateTLSConfig(), nil)
+ if err != nil {
+ t.Fatalf("Expected no error but got: %s", err)
+ }
+
+ m := createTestMsg()
+
+ streamSync, err := conn.OpenStreamSync(ctx)
+ if err != nil {
+ t.Errorf("Expected no error but got: %s", err)
+ }
+
+ _, err = streamSync.Write(m)
+ if err != nil {
+ t.Errorf("Expected no error but got: %s", err)
+ }
+ _ = streamSync.Close()
+
+ sizeBuf := make([]byte, 2)
+ _, err = io.ReadFull(streamSync, sizeBuf)
+ if err != nil {
+ t.Errorf("Expected no error but got: %s", err)
+ }
+
+ size := binary.BigEndian.Uint16(sizeBuf)
+ buf := make([]byte, size)
+ _, err = io.ReadFull(streamSync, buf)
+ if err != nil {
+ t.Errorf("Expected no error but got: %s", err)
+ }
+
+ d := new(dns.Msg)
+ err = d.Unpack(buf)
+ if err != nil {
+ t.Errorf("Expected no error but got: %s", err)
+ }
+
+ if d.Rcode != dns.RcodeSuccess {
+ t.Errorf("Expected success but got %d", d.Rcode)
+ }
+
+ if len(d.Extra) != 2 {
+ t.Errorf("Expected 2 RRs in additional section, but got %d", len(d.Extra))
+ }
+}
+
+func TestQUICProtocolError(t *testing.T) {
+ q, udp, _, err := CoreDNSServerAndPorts(quicCorefile)
+ if err != nil {
+ t.Fatalf("Could not get CoreDNS serving instance: %s", err)
+ }
+ defer q.Stop()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ conn, err := quic.DialAddr(ctx, convertAddress(udp), generateTLSConfig(), nil)
+ if err != nil {
+ t.Fatalf("Expected no error but got: %s", err)
+ }
+
+ m := createInvalidDOQMsg()
+
+ streamSync, err := conn.OpenStreamSync(ctx)
+ if err != nil {
+ t.Errorf("Expected no error but got: %s", err)
+ }
+
+ _, err = streamSync.Write(m)
+ if err != nil {
+ t.Errorf("Expected no error but got: %s", err)
+ }
+ _ = streamSync.Close()
+
+ errorBuf := make([]byte, 2)
+ _, err = io.ReadFull(streamSync, errorBuf)
+ if err == nil {
+ t.Errorf("Expected protocol error but got: %s", errorBuf)
+ }
+
+ if !isProtocolErr(err) {
+ t.Errorf("Expected \"Application Error 0x2\" but got: %s", err)
+ }
+}
+
+func isProtocolErr(err error) bool {
+ var qAppErr *quic.ApplicationError
+ return errors.As(err, &qAppErr) && qAppErr.ErrorCode == 2
+}
+
+// convertAddress transforms the address given in CoreDNSServerAndPorts to a format
+// that quic.DialAddr can read. It is unable to use [::]:61799, see:
+// "INTERNAL_ERROR (local): write udp [::]:50676->[::]:61799: sendmsg: no route to host"
+// So it transforms it to localhost:61799.
+func convertAddress(address string) string {
+ if strings.HasPrefix(address, "[::]") {
+ address = strings.Replace(address, "[::]", "localhost", 1)
+ }
+ return address
+}
+
+func generateTLSConfig() *tls.Config {
+ tlsConfig, err := ctls.NewTLSConfig(
+ "../plugin/tls/test_cert.pem",
+ "../plugin/tls/test_key.pem",
+ "../plugin/tls/test_ca.pem")
+
+ if err != nil {
+ panic(err)
+ }
+
+ tlsConfig.NextProtos = []string{"doq"}
+ tlsConfig.InsecureSkipVerify = true
+
+ return tlsConfig
+}
+
+func createTestMsg() []byte {
+ m := new(dns.Msg)
+ m.SetQuestion("whoami.example.org.", dns.TypeA)
+ m.Id = 0
+ msg, _ := m.Pack()
+ return dnsserver.AddPrefix(msg)
+}
+
+func createInvalidDOQMsg() []byte {
+ m := new(dns.Msg)
+ m.SetQuestion("whoami.example.org.", dns.TypeA)
+ msg, _ := m.Pack()
+ return msg
+}