mirror of
https://github.com/coredns/coredns.git
synced 2026-10-16 23:35:43 -04:00
The proxyproto plugin parsed PROXY protocol v2 headers only to recover the real client address; the TLV vector was discarded, so downstream plugins had no way to read values such as the AWS VPC endpoint ID, SSL client CN, or vendor-specific custom TLVs. Add an opt-in `tlv_metadata` option. When set, the connection's raw PPv2 TLV vector is captured and the plugin, acting as a metadata.Provider, publishes it under the `proxyproto/tlv` label as a hex-encoded string. Consumers decode and split it with proxyproto.SplitTLVs. The option is off by default, so existing deployments skip the extra per-connection processing and behave exactly as before. PROXY protocol v1 connections, which carry no TLVs, are unaffected. Capture spans all transports: dns:// (UDP and TCP), tls:// (DoT), https:// (DoH), grpc://, quic:// (DoQ), and https3:// (DoH3). QUIC-based transports require udp_session_tracking, since the PPv2 header arrives as a standalone first datagram ahead of the QUIC handshake. Includes unit tests for capture, the metadata provider (direct and through the real metadata chain), v1 compatibility, and option parsing, plus README and man page updates. Fixes #8633 Signed-off-by: Mekias Yohannes <mekias@amazon.com> Co-authored-by: Mekias Yohannes <mekias@amazon.com>
311 lines
9.4 KiB
Go
311 lines
9.4 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"
|
|
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/pires/go-proxyproto"
|
|
"golang.org/x/net/netutil"
|
|
)
|
|
|
|
const (
|
|
// DefaultHTTPSMaxConnections is the default maximum number of concurrent connections.
|
|
DefaultHTTPSMaxConnections = 200
|
|
|
|
// DefaultHTTPSMaxStreams is the default maximum number of concurrent HTTP/2 streams
|
|
// per connection, used when max_streams is not specified.
|
|
DefaultHTTPSMaxStreams = 250
|
|
)
|
|
|
|
// 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{}
|
|
|
|
// connContextKey is the context key under which the accepted net.Conn is stashed
|
|
// via http.Server.ConnContext, so ServeHTTP can recover PPv2 TLVs from it.
|
|
type connContextKey struct{}
|
|
|
|
// NewServerHTTPS returns a new CoreDNS HTTPS server and compiles all plugins in to it.
|
|
func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) {
|
|
tlsConfig, err := sharedTLSConfig(addr, group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s, err := NewServer(addr, group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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),
|
|
}
|
|
// max_streams limits the number of concurrent HTTP/2 streams per connection. When unset,
|
|
// DefaultHTTPSMaxStreams is applied; a value of 0 leaves the underlying HTTP/2 transport
|
|
// default in place; a positive value sets the limit explicitly. The chosen value is
|
|
// advertised in the server's SETTINGS frame. Resolve across the whole group since blocks
|
|
// sharing a listener share one HTTP/2 server; conflicting explicit values are rejected.
|
|
maxStreams := DefaultHTTPSMaxStreams
|
|
var resolved *int
|
|
for _, conf := range group {
|
|
if conf == nil || conf.MaxHTTPSStreams == nil {
|
|
continue
|
|
}
|
|
if resolved != nil && *resolved != *conf.MaxHTTPSStreams {
|
|
return nil, fmt.Errorf("conflicting max_streams values for shared HTTPS listener %s: %d and %d",
|
|
addr, *resolved, *conf.MaxHTTPSStreams)
|
|
}
|
|
resolved = conf.MaxHTTPSStreams
|
|
}
|
|
if resolved != nil {
|
|
maxStreams = *resolved
|
|
}
|
|
if maxStreams > 0 {
|
|
srv.HTTP2 = &http.HTTP2Config{
|
|
MaxConcurrentStreams: maxStreams,
|
|
}
|
|
}
|
|
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
|
|
|
|
// When PPv2 TLV metadata is enabled, stash the accepted connection in the
|
|
// request context so ServeHTTP can recover the TLVs captured by the listener.
|
|
if s.proxyProtoTLVMetadata {
|
|
sh.httpsServer.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
|
|
return context.WithValue(ctx, connContextKey{}, c)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if s.proxyProtoTLVMetadata {
|
|
l = cproxyproto.NewTLVListener(l, s.connPolicy)
|
|
} else {
|
|
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 != "" {
|
|
printStartup(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 the listener captured PPv2 TLVs, surface them via RemoteAddr so the
|
|
// proxyproto metadata provider exposes them, same as the other transports.
|
|
if conn, ok := r.Context().Value(connContextKey{}).(net.Conn); ok {
|
|
if a := cproxyproto.AddrFromConn(conn); a != nil {
|
|
dw.raddr = a
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|