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 +}