[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>
This commit is contained in:
parent
b7c9d3e155
commit
cc7a364633
15 changed files with 759 additions and 4 deletions
18
README.md
18
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
|
||||
|
|
60
core/dnsserver/quic.go
Normal file
60
core/dnsserver/quic.go
Normal file
|
@ -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 }
|
20
core/dnsserver/quic_test.go
Normal file
20
core/dnsserver/quic_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
346
core/dnsserver/server_quic.go
Normal file
346
core/dnsserver/server_quic.go
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
8
go.mod
8
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
|
||||
|
|
12
go.sum
12
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=
|
||||
|
|
103
man/coredns-timeouts.7
Normal file
103
man/coredns-timeouts.7
Normal file
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
165
test/quic_test.go
Normal file
165
test/quic_test.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Add table
Reference in a new issue