mirror of
https://github.com/coredns/coredns.git
synced 2026-07-10 17:50:12 -04:00
The DoH (HTTP/2) and DoH3 (HTTP/3) handlers wrote the raw parse error
returned by doh.RequestToMsgWire directly into the HTTP 400 response
body via http.Error(w, err.Error(), ...).
When the request body read fails mid-stream (for example the server's
ReadTimeout firing on a slow client, or a connection reset),
io.ReadAll(http.MaxBytesReader(...)) returns a *net.OpError whose text
embeds the server's own listen socket, e.g.
read tcp 10.0.0.1:5443->10.0.0.2:48418: i/o timeout
That discloses the server's internal bind address and port to any
unauthenticated remote caller who can trigger a body-read error.
Log the underlying error at debug level and return a generic
"invalid request" body to the client instead, matching the fixed-string
responses already used for the 404 and 500 cases in the same handlers.
Add tests asserting the response body no longer contains the raw read
error or an internal address.
Signed-off-by: zongqi-wang <wangzongqi@msn.com>
262 lines
7.5 KiB
Go
262 lines
7.5 KiB
Go
package dnsserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
stdlog "log"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/coredns/caddy"
|
|
"github.com/coredns/coredns/plugin/metrics/vars"
|
|
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
|
"github.com/coredns/coredns/plugin/pkg/doh"
|
|
clog "github.com/coredns/coredns/plugin/pkg/log"
|
|
"github.com/coredns/coredns/plugin/pkg/response"
|
|
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
|
"github.com/coredns/coredns/plugin/pkg/transport"
|
|
|
|
"github.com/miekg/dns"
|
|
"github.com/pires/go-proxyproto"
|
|
"golang.org/x/net/netutil"
|
|
)
|
|
|
|
const (
|
|
// DefaultHTTPSMaxConnections is the default maximum number of concurrent connections.
|
|
DefaultHTTPSMaxConnections = 200
|
|
)
|
|
|
|
// ServerHTTPS represents an instance of a DNS-over-HTTPS server.
|
|
type ServerHTTPS struct {
|
|
*Server
|
|
httpsServer *http.Server
|
|
listenAddr net.Addr
|
|
tlsConfig *tls.Config
|
|
validRequest func(*http.Request) bool
|
|
maxConnections int
|
|
}
|
|
|
|
// loggerAdapter is a simple adapter around CoreDNS logger made to implement io.Writer in order to log errors from HTTP server
|
|
type loggerAdapter struct {
|
|
}
|
|
|
|
func (l *loggerAdapter) Write(p []byte) (n int, err error) {
|
|
clog.Debug(string(p))
|
|
return len(p), nil
|
|
}
|
|
|
|
// HTTPRequestKey is the context key for the HTTP request when processing DNS-over-HTTPS.
|
|
// Plugins can access the original HTTP request to retrieve headers, client IP, and metadata.
|
|
type HTTPRequestKey struct{}
|
|
|
|
// NewServerHTTPS returns a new CoreDNS HTTPS server and compiles all plugins in to it.
|
|
func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, 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
|
|
}
|
|
}
|
|
|
|
// http/2 is recommended when using DoH. We need to specify it in next protos
|
|
// or the upgrade won't happen.
|
|
if tlsConfig != nil {
|
|
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
|
|
}
|
|
|
|
// Use a custom request validation func or use the standard DoH path check.
|
|
var validator func(*http.Request) bool
|
|
for _, z := range s.zones {
|
|
for _, conf := range z {
|
|
validator = conf.HTTPRequestValidateFunc
|
|
}
|
|
}
|
|
if validator == nil {
|
|
validator = func(r *http.Request) bool { return r.URL.Path == doh.Path }
|
|
}
|
|
|
|
srv := &http.Server{
|
|
ReadTimeout: s.ReadTimeout,
|
|
WriteTimeout: s.WriteTimeout,
|
|
IdleTimeout: s.IdleTimeout,
|
|
ErrorLog: stdlog.New(&loggerAdapter{}, "", 0),
|
|
}
|
|
maxConnections := DefaultHTTPSMaxConnections
|
|
if len(group) > 0 && group[0] != nil && group[0].MaxHTTPSConnections != nil {
|
|
maxConnections = *group[0].MaxHTTPSConnections
|
|
}
|
|
|
|
sh := &ServerHTTPS{
|
|
Server: s,
|
|
tlsConfig: tlsConfig,
|
|
httpsServer: srv,
|
|
validRequest: validator,
|
|
maxConnections: maxConnections,
|
|
}
|
|
sh.httpsServer.Handler = sh
|
|
|
|
return sh, nil
|
|
}
|
|
|
|
// Compile-time check to ensure ServerHTTPS implements the caddy.GracefulServer interface
|
|
var _ caddy.GracefulServer = &ServerHTTPS{}
|
|
|
|
// Serve implements caddy.TCPServer interface.
|
|
func (s *ServerHTTPS) Serve(l net.Listener) error {
|
|
s.m.Lock()
|
|
s.listenAddr = l.Addr()
|
|
s.m.Unlock()
|
|
|
|
// Wrap listener to limit concurrent connections (before TLS)
|
|
if s.maxConnections > 0 {
|
|
l = netutil.LimitListener(l, s.maxConnections)
|
|
}
|
|
|
|
if s.tlsConfig != nil {
|
|
l = tls.NewListener(l, s.tlsConfig)
|
|
}
|
|
|
|
return s.httpsServer.Serve(l)
|
|
}
|
|
|
|
// ServePacket implements caddy.UDPServer interface.
|
|
func (s *ServerHTTPS) ServePacket(_p net.PacketConn) error { return nil }
|
|
|
|
// Listen implements caddy.TCPServer interface.
|
|
func (s *ServerHTTPS) Listen() (net.Listener, error) {
|
|
l, err := reuseport.Listen("tcp", s.Addr[len(transport.HTTPS+"://"):])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.connPolicy != nil {
|
|
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// ListenPacket implements caddy.UDPServer interface.
|
|
func (s *ServerHTTPS) ListenPacket() (net.PacketConn, error) { return nil, nil }
|
|
|
|
// OnStartupComplete lists the sites served by this server
|
|
// and any relevant information, assuming Quiet is false.
|
|
func (s *ServerHTTPS) OnStartupComplete() {
|
|
if Quiet {
|
|
return
|
|
}
|
|
|
|
out := startUpZones(transport.HTTPS+"://", s.Addr, s.zones)
|
|
if out != "" {
|
|
fmt.Print(out)
|
|
}
|
|
}
|
|
|
|
// Stop stops the server. It blocks until the server is totally stopped.
|
|
func (s *ServerHTTPS) Stop() error {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
if s.httpsServer != nil {
|
|
s.httpsServer.Shutdown(context.Background())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// localAddr returns the per-connection local address, or s.listenAddr as fallback.
|
|
func (s *ServerHTTPS) localAddr(r *http.Request) net.Addr {
|
|
if addr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
|
|
return addr
|
|
}
|
|
return s.listenAddr
|
|
}
|
|
|
|
// ServeHTTP is the handler that gets the HTTP request and converts to the dns format, calls the plugin
|
|
// chain, converts it back and write it to the client.
|
|
func (s *ServerHTTPS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if !s.validRequest(r) {
|
|
http.Error(w, "", http.StatusNotFound)
|
|
s.countResponse(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
msg, raw, err := doh.RequestToMsgWire(r)
|
|
if err != nil {
|
|
clog.Debugf("DoH request could not be parsed: %v", err)
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
s.countResponse(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Create a DoHWriter with the correct addresses in it.
|
|
h, p, _ := net.SplitHostPort(r.RemoteAddr)
|
|
port, _ := strconv.Atoi(p)
|
|
dw := &DoHWriter{
|
|
laddr: s.localAddr(r),
|
|
raddr: &net.TCPAddr{IP: net.ParseIP(h), Port: port},
|
|
request: r,
|
|
}
|
|
|
|
if tsig := msg.IsTsig(); tsig != nil {
|
|
if s.tsigSecret == nil {
|
|
dw.tsigStatus = dns.ErrSecret
|
|
} else if secret, ok := s.tsigSecret[tsig.Hdr.Name]; !ok {
|
|
dw.tsigStatus = dns.ErrSecret
|
|
} else {
|
|
dw.tsigStatus = dns.TsigVerify(raw, secret, "", false)
|
|
}
|
|
}
|
|
|
|
// We just call the normal chain handler - all error handling is done there.
|
|
// We should expect a packet to be returned that we can send to the client.
|
|
|
|
// Propagate HTTP request context to DNS processing chain. This ensures that
|
|
// HTTP request timeouts, cancellations, and other context values are properly
|
|
// inherited by the DNS processing pipeline.
|
|
ctx := context.WithValue(r.Context(), Key{}, s.Server)
|
|
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
|
ctx = context.WithValue(ctx, HTTPRequestKey{}, r)
|
|
s.ServeDNS(ctx, dw, msg)
|
|
|
|
// See section 4.2.1 of RFC 8484.
|
|
// We are using code 500 to indicate an unexpected situation when the chain
|
|
// handler has not provided any response message.
|
|
if dw.Msg == nil {
|
|
http.Error(w, "No response", http.StatusInternalServerError)
|
|
s.countResponse(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
buf, _ := dw.Msg.Pack()
|
|
|
|
mt, _ := response.Typify(dw.Msg, time.Now().UTC())
|
|
age := dnsutil.MinimalTTLWithMaximum(dw.Msg, mt, dnsutil.MaximumDefaultTTL)
|
|
|
|
w.Header().Set("Content-Type", doh.MimeType)
|
|
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", uint32(age.Seconds())))
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(buf)))
|
|
w.WriteHeader(http.StatusOK)
|
|
s.countResponse(http.StatusOK)
|
|
|
|
w.Write(buf)
|
|
}
|
|
|
|
func (s *ServerHTTPS) countResponse(status int) {
|
|
vars.HTTPSResponsesCount.WithLabelValues(s.Addr, strconv.Itoa(status)).Inc()
|
|
}
|
|
|
|
// Shutdown stops the server (non gracefully).
|
|
func (s *ServerHTTPS) Shutdown() error {
|
|
if s.httpsServer != nil {
|
|
s.httpsServer.Shutdown(context.Background())
|
|
}
|
|
return nil
|
|
}
|