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>
235 lines
6.2 KiB
Go
235 lines
6.2 KiB
Go
package dnsserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"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"
|
|
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
|
"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/quic-go/quic-go"
|
|
"github.com/quic-go/quic-go/http3"
|
|
)
|
|
|
|
const (
|
|
// DefaultHTTPS3MaxStreams is the default maximum number of concurrent QUIC streams per connection.
|
|
DefaultHTTPS3MaxStreams = 256
|
|
// DefaultHTTPS3MaxHeaderBytes limits HTTP/3 header memory before requests reach the DoH handler.
|
|
DefaultHTTPS3MaxHeaderBytes = 16 << 10 // 16 KiB
|
|
)
|
|
|
|
// ServerHTTPS3 represents a DNS-over-HTTP/3 server.
|
|
type ServerHTTPS3 struct {
|
|
*Server
|
|
httpsServer *http3.Server
|
|
listenAddr net.Addr
|
|
tlsConfig *tls.Config
|
|
quicConfig *quic.Config
|
|
validRequest func(*http.Request) bool
|
|
maxStreams int
|
|
}
|
|
|
|
// NewServerHTTPS3 builds the HTTP/3 (DoH3) server.
|
|
func NewServerHTTPS3(addr string, group []*Config) (*ServerHTTPS3, error) {
|
|
s, err := NewServer(addr, group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Extract TLS config (CoreDNS guarantees it is consistent)
|
|
var tlsConfig *tls.Config
|
|
for _, z := range s.zones {
|
|
for _, conf := range z {
|
|
tlsConfig = conf.TLSConfig
|
|
}
|
|
}
|
|
if tlsConfig == nil {
|
|
return nil, fmt.Errorf("DoH3 requires TLS, no TLS config found")
|
|
}
|
|
|
|
// HTTP/3 requires ALPN "h3"
|
|
tlsConfig.NextProtos = []string{"h3"}
|
|
|
|
// Request validator
|
|
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 }
|
|
}
|
|
|
|
maxStreams := DefaultHTTPS3MaxStreams
|
|
if len(group) > 0 && group[0] != nil && group[0].MaxHTTPS3Streams != nil {
|
|
maxStreams = *group[0].MaxHTTPS3Streams
|
|
}
|
|
|
|
// QUIC transport config with stream limits (0 means use QUIC default)
|
|
qconf := &quic.Config{
|
|
MaxIdleTimeout: s.IdleTimeout,
|
|
Allow0RTT: true,
|
|
}
|
|
if maxStreams > 0 {
|
|
qconf.MaxIncomingStreams = int64(maxStreams)
|
|
qconf.MaxIncomingUniStreams = int64(maxStreams)
|
|
}
|
|
|
|
h3srv := &http3.Server{
|
|
Handler: nil, // set after constructing ServerHTTPS3
|
|
TLSConfig: tlsConfig,
|
|
EnableDatagrams: true,
|
|
QUICConfig: qconf,
|
|
MaxHeaderBytes: DefaultHTTPS3MaxHeaderBytes,
|
|
// Logger: stdlog.New(&loggerAdapter{}, "", 0), TODO: Fix it
|
|
}
|
|
|
|
sh := &ServerHTTPS3{
|
|
Server: s,
|
|
tlsConfig: tlsConfig,
|
|
httpsServer: h3srv,
|
|
quicConfig: qconf,
|
|
validRequest: validator,
|
|
maxStreams: maxStreams,
|
|
}
|
|
|
|
h3srv.Handler = sh
|
|
|
|
return sh, nil
|
|
}
|
|
|
|
var _ caddy.GracefulServer = &ServerHTTPS3{}
|
|
|
|
// ListenPacket opens the UDP socket for QUIC.
|
|
func (s *ServerHTTPS3) ListenPacket() (net.PacketConn, error) {
|
|
p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.HTTPS3+"://"):])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.connPolicy != nil {
|
|
p = &cproxyproto.PacketConn{PacketConn: p, ConnPolicy: s.connPolicy}
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// ServePacket starts serving QUIC+HTTP/3 on an existing UDP socket.
|
|
func (s *ServerHTTPS3) ServePacket(pc net.PacketConn) error {
|
|
s.m.Lock()
|
|
s.listenAddr = pc.LocalAddr()
|
|
s.m.Unlock()
|
|
// Serve HTTP/3 over QUIC
|
|
return s.httpsServer.Serve(pc)
|
|
}
|
|
|
|
// Listen function not used in HTTP/3, but defined for compatibility
|
|
func (s *ServerHTTPS3) Listen() (net.Listener, error) { return nil, nil }
|
|
func (s *ServerHTTPS3) Serve(_l net.Listener) error { return nil }
|
|
|
|
// OnStartupComplete lists the sites served by this server
|
|
// and any relevant information, assuming Quiet is false.
|
|
func (s *ServerHTTPS3) OnStartupComplete() {
|
|
if Quiet {
|
|
return
|
|
}
|
|
out := startUpZones(transport.HTTPS3+"://", s.Addr, s.zones)
|
|
if out != "" {
|
|
fmt.Print(out)
|
|
}
|
|
}
|
|
|
|
// Stop graceful shutdown. It blocks until the server is totally stopped.
|
|
func (s *ServerHTTPS3) Stop() error {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
if s.httpsServer != nil {
|
|
return s.httpsServer.Shutdown(context.Background())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Shutdown stops the server (non gracefully).
|
|
func (s *ServerHTTPS3) Shutdown() error {
|
|
if s.httpsServer != nil {
|
|
s.httpsServer.Shutdown(context.Background())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ServeHTTP is the handler for the DoH3 requests
|
|
func (s *ServerHTTPS3) 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("DoH3 request could not be parsed: %v", err)
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
s.countResponse(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// from HTTP request → DNS writer
|
|
h, p, _ := net.SplitHostPort(r.RemoteAddr)
|
|
port, _ := strconv.Atoi(p)
|
|
dw := &DoHWriter{
|
|
laddr: s.listenAddr,
|
|
raddr: &net.UDPAddr{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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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 *ServerHTTPS3) countResponse(status int) {
|
|
vars.HTTPS3ResponsesCount.WithLabelValues(s.Addr, strconv.Itoa(status)).Inc()
|
|
}
|