mirror of
https://github.com/coredns/coredns.git
synced 2026-08-27 07:07:05 -04:00
Keep miekg/dns's default request policy unless a plugin explicitly registers an additional opcode. Aggregate the policy at the listener, then enforce it again after zone routing so mixed server blocks on one socket remain isolated. Apply the same policy to UDP, TCP, and DNS-over-TLS while preserving TSIG verification and the one-question requirement. Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
540 lines
17 KiB
Go
540 lines
17 KiB
Go
// Package dnsserver implements CoreDNS as a Caddy server type.
|
|
//
|
|
// Importing this package registers the "dns" server type with Caddy. Programs
|
|
// embedding CoreDNS can import only the plugins they need, set Directives before
|
|
// starting a server, and pass an in-memory Corefile to [caddy.Start]. They should
|
|
// not call coremain.Run, which provides the command-line program behavior such
|
|
// as flag parsing, signal handling, and blocking until shutdown.
|
|
// Before stopping an embedded instance, run its shutdown callbacks so that
|
|
// plugins can release resources.
|
|
//
|
|
// Directives and Caddy's plugin registry are process-wide. Configure them
|
|
// before starting any servers and do not mutate them while servers are running.
|
|
package dnsserver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"net"
|
|
"runtime/debug"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/coredns/caddy"
|
|
"github.com/coredns/coredns/plugin"
|
|
"github.com/coredns/coredns/plugin/metrics/vars"
|
|
"github.com/coredns/coredns/plugin/pkg/edns"
|
|
"github.com/coredns/coredns/plugin/pkg/log"
|
|
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
|
"github.com/coredns/coredns/plugin/pkg/rcode"
|
|
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
|
"github.com/coredns/coredns/plugin/pkg/trace"
|
|
"github.com/coredns/coredns/plugin/pkg/transport"
|
|
"github.com/coredns/coredns/request"
|
|
|
|
"github.com/miekg/dns"
|
|
ot "github.com/opentracing/opentracing-go"
|
|
"github.com/pires/go-proxyproto"
|
|
)
|
|
|
|
// Server represents an instance of a server, which serves
|
|
// DNS requests at a particular address (host and port). A
|
|
// server is capable of serving numerous zones on
|
|
// the same address and the listener may be stopped for
|
|
// graceful termination (POSIX only).
|
|
type Server struct {
|
|
Addr string // Address we listen on
|
|
IdleTimeout time.Duration // Idle timeout for connection-oriented transports
|
|
ReadTimeout time.Duration // Read timeout for connection-oriented transports
|
|
WriteTimeout time.Duration // Write timeout for connection-oriented transports that support it
|
|
MaxTCPQueries int // Maximum number of queries served on a single TCP/TLS connection. -1 means unlimited.
|
|
|
|
connPolicy proxyproto.ConnPolicyFunc // Proxy Protocol connection policy function
|
|
udpSessionTrackingTTL time.Duration // TTL for UDP PPv2 session tracking (0 = disabled)
|
|
udpSessionTrackingMaxSessions int // LRU cap for UDP session tracking (0 = default)
|
|
|
|
server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case.
|
|
m sync.Mutex // protects the servers
|
|
|
|
zones map[string][]*Config // zones keyed by their address
|
|
graceTimeout time.Duration // the maximum duration of a graceful shutdown
|
|
trace trace.Trace // the trace plugin for the server
|
|
debug bool // disable recover()
|
|
stacktrace bool // enable stacktrace in recover error log
|
|
classChaos bool // allow non-INET class queries
|
|
|
|
tsigSecret map[string]string
|
|
allowedOpcodes map[int]struct{}
|
|
|
|
// udpDecorateWriterFunc is selected in NewServer from the group configs in
|
|
// stable order (last one set wins), so the choice is deterministic when
|
|
// several server blocks share a listener. See Config.UDPDecorateWriterFunc.
|
|
udpDecorateWriterFunc func(*Server) dns.DecorateWriter
|
|
|
|
// Ensure Stop is idempotent when invoked concurrently (e.g., during reload and SIGTERM).
|
|
stopOnce sync.Once
|
|
stopErr error
|
|
}
|
|
|
|
// MetadataCollector is a plugin that can retrieve metadata functions from all metadata providing plugins
|
|
type MetadataCollector interface {
|
|
Collect(context.Context, request.Request) context.Context
|
|
}
|
|
|
|
// NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class
|
|
// queries are blocked unless queries from enableChaos are loaded.
|
|
func NewServer(addr string, group []*Config) (*Server, error) {
|
|
s := &Server{
|
|
Addr: addr,
|
|
zones: make(map[string][]*Config),
|
|
graceTimeout: 5 * time.Second,
|
|
IdleTimeout: 10 * time.Second,
|
|
ReadTimeout: 3 * time.Second,
|
|
WriteTimeout: 5 * time.Second,
|
|
MaxTCPQueries: tcpMaxQueries,
|
|
tsigSecret: make(map[string]string),
|
|
allowedOpcodes: make(map[int]struct{}),
|
|
}
|
|
|
|
for _, site := range group {
|
|
if site.Debug {
|
|
s.debug = true
|
|
log.D.Set()
|
|
}
|
|
s.stacktrace = site.Stacktrace
|
|
|
|
// append the config to the zone's configs
|
|
s.zones[site.Zone] = append(s.zones[site.Zone], site)
|
|
|
|
// set timeouts
|
|
if site.ReadTimeout != 0 {
|
|
s.ReadTimeout = site.ReadTimeout
|
|
}
|
|
if site.WriteTimeout != 0 {
|
|
s.WriteTimeout = site.WriteTimeout
|
|
}
|
|
if site.IdleTimeout != 0 {
|
|
s.IdleTimeout = site.IdleTimeout
|
|
}
|
|
if site.MaxTCPQueries != nil {
|
|
s.MaxTCPQueries = *site.MaxTCPQueries
|
|
}
|
|
|
|
// copy tsig secrets
|
|
maps.Copy(s.tsigSecret, site.TsigSecret)
|
|
maps.Copy(s.allowedOpcodes, site.allowedOpcodes)
|
|
|
|
// compile custom plugin for everything
|
|
var stack plugin.Handler
|
|
for i := len(site.Plugin) - 1; i >= 0; i-- {
|
|
stack = site.Plugin[i](stack)
|
|
|
|
// register the *handler* also
|
|
site.registerHandler(stack)
|
|
|
|
// If the current plugin is a MetadataCollector, bookmark it for later use. This loop traverses the plugin
|
|
// list backwards, so the first MetadataCollector plugin wins.
|
|
if mdc, ok := stack.(MetadataCollector); ok {
|
|
site.metaCollector = mdc
|
|
}
|
|
|
|
if s.trace == nil && stack.Name() == "trace" {
|
|
// we have to stash away the plugin, not the
|
|
// Tracer object, because the Tracer won't be initialized yet
|
|
if t, ok := stack.(trace.Trace); ok {
|
|
s.trace = t
|
|
}
|
|
}
|
|
// Unblock CH class queries when any of these plugins are loaded.
|
|
if _, ok := EnableChaos[stack.Name()]; ok {
|
|
s.classChaos = true
|
|
}
|
|
}
|
|
site.pluginChain = stack
|
|
if site.ProxyProtoConnPolicy != nil {
|
|
s.connPolicy = site.ProxyProtoConnPolicy
|
|
}
|
|
if site.ProxyProtoUDPSessionTrackingTTL > 0 {
|
|
s.udpSessionTrackingTTL = site.ProxyProtoUDPSessionTrackingTTL
|
|
}
|
|
if site.ProxyProtoUDPSessionTrackingMaxSessions > 0 {
|
|
s.udpSessionTrackingMaxSessions = site.ProxyProtoUDPSessionTrackingMaxSessions
|
|
}
|
|
if site.UDPDecorateWriterFunc != nil {
|
|
s.udpDecorateWriterFunc = site.UDPDecorateWriterFunc
|
|
}
|
|
}
|
|
|
|
if !s.debug {
|
|
// When reloading we need to explicitly disable debug logging if it is now disabled.
|
|
log.D.Clear()
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// Compile-time check to ensure Server implements the caddy.GracefulServer interface
|
|
var _ caddy.GracefulServer = &Server{}
|
|
|
|
// Serve starts the server with an existing listener. It blocks until the server stops.
|
|
// This implements caddy.TCPServer interface.
|
|
func (s *Server) Serve(l net.Listener) error {
|
|
s.m.Lock()
|
|
|
|
s.server[tcp] = &dns.Server{Listener: l,
|
|
Net: "tcp",
|
|
TsigSecret: s.tsigSecret,
|
|
MsgAcceptFunc: s.msgAcceptFunc(),
|
|
MaxTCPQueries: s.MaxTCPQueries,
|
|
ReadTimeout: s.ReadTimeout,
|
|
WriteTimeout: s.WriteTimeout,
|
|
IdleTimeout: func() time.Duration {
|
|
return s.IdleTimeout
|
|
},
|
|
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
|
ctx := context.WithValue(context.Background(), Key{}, s)
|
|
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
|
s.ServeDNS(ctx, w, r)
|
|
})}
|
|
|
|
s.m.Unlock()
|
|
|
|
return s.server[tcp].ActivateAndServe()
|
|
}
|
|
|
|
// ServePacket starts the server with an existing packetconn. It blocks until the server stops.
|
|
// This implements caddy.UDPServer interface.
|
|
func (s *Server) ServePacket(p net.PacketConn) error {
|
|
// Use a custom writer decorator if one was configured.
|
|
var dw dns.DecorateWriter
|
|
if s.udpDecorateWriterFunc != nil {
|
|
dw = s.udpDecorateWriterFunc(s)
|
|
}
|
|
s.m.Lock()
|
|
s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
|
ctx := context.WithValue(context.Background(), Key{}, s)
|
|
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
|
s.ServeDNS(ctx, w, r)
|
|
}), TsigSecret: s.tsigSecret, MsgAcceptFunc: s.msgAcceptFunc(), DecorateWriter: dw}
|
|
s.m.Unlock()
|
|
|
|
return s.server[udp].ActivateAndServe()
|
|
}
|
|
|
|
// Listen implements caddy.TCPServer interface.
|
|
func (s *Server) Listen() (net.Listener, error) {
|
|
l, err := reuseport.Listen("tcp", s.Addr[len(transport.DNS+"://"):])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.connPolicy != nil {
|
|
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// WrapListener Listen implements caddy.GracefulServer interface.
|
|
func (s *Server) WrapListener(ln net.Listener) net.Listener {
|
|
return ln
|
|
}
|
|
|
|
// ListenPacket implements caddy.UDPServer interface.
|
|
func (s *Server) ListenPacket() (net.PacketConn, error) {
|
|
p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.DNS+"://"):])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.connPolicy != nil {
|
|
p = &cproxyproto.PacketConn{PacketConn: p, ConnPolicy: s.connPolicy, UDPSessionTrackingTTL: s.udpSessionTrackingTTL, UDPSessionTrackingMaxSessions: s.udpSessionTrackingMaxSessions}
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// Stop attempts to gracefully stop the server.
|
|
// It waits until the server is stopped and its connections are closed,
|
|
// up to a max timeout of a few seconds. If unsuccessful, an error is returned.
|
|
//
|
|
// This implements Caddy.Stopper interface.
|
|
func (s *Server) Stop() error {
|
|
s.stopOnce.Do(func() {
|
|
ctx, cancelCtx := context.WithTimeout(context.Background(), s.graceTimeout)
|
|
defer cancelCtx()
|
|
|
|
var wg sync.WaitGroup
|
|
s.m.Lock()
|
|
for _, s1 := range s.server {
|
|
// We might not have started and initialized the full set of servers
|
|
if s1 == nil {
|
|
continue
|
|
}
|
|
|
|
wg.Go(func() {
|
|
s1.ShutdownContext(ctx)
|
|
})
|
|
}
|
|
s.m.Unlock()
|
|
wg.Wait()
|
|
|
|
s.stopErr = ctx.Err()
|
|
})
|
|
return s.stopErr
|
|
}
|
|
|
|
// Address together with Stop() implement caddy.GracefulServer.
|
|
func (s *Server) Address() string { return s.Addr }
|
|
|
|
// ServeDNS is the entry point for every request to the address that
|
|
// is bound to. It acts as a multiplexer for the requests zonename as
|
|
// defined in the request so that the correct zone
|
|
// (configuration and plugin stack) will handle the request.
|
|
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
|
|
// The default dns.Mux checks the question section size, but we have our
|
|
// own mux here. Check if we have a question section. If not drop them here.
|
|
if r == nil || len(r.Question) == 0 {
|
|
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure)
|
|
return
|
|
}
|
|
|
|
if !s.debug {
|
|
defer func() {
|
|
// In case the user doesn't enable error plugin, we still
|
|
// need to make sure that we stay alive up here
|
|
if rec := recover(); rec != nil {
|
|
if s.stacktrace {
|
|
log.Errorf("Recovered from panic in server: %q %v\n%s", s.Addr, rec, string(debug.Stack()))
|
|
} else {
|
|
log.Errorf("Recovered from panic in server: %q %v", s.Addr, rec)
|
|
}
|
|
vars.Panic.Inc()
|
|
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if !s.classChaos && r.Question[0].Qclass != dns.ClassINET {
|
|
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
|
|
return
|
|
}
|
|
|
|
if m, err := edns.Version(r); err != nil { // Wrong EDNS version, return at once.
|
|
w.WriteMsg(m)
|
|
return
|
|
}
|
|
|
|
// Wrap the response writer in a ScrubWriter so we automatically make the reply fit in the client's buffer.
|
|
w = request.NewScrubWriter(r, w)
|
|
|
|
q := strings.ToLower(r.Question[0].Name)
|
|
var (
|
|
off int
|
|
end bool
|
|
dshandler *Config
|
|
)
|
|
|
|
for {
|
|
if z, ok := s.zones[q[off:]]; ok {
|
|
for _, h := range z {
|
|
if h.pluginChain == nil { // zone defined, but has not got any plugins
|
|
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
|
|
return
|
|
}
|
|
|
|
if h.metaCollector != nil {
|
|
// Collect metadata now, so it can be used before we send a request down the plugin chain.
|
|
ctx = h.metaCollector.Collect(ctx, request.Request{Req: r, W: w})
|
|
}
|
|
|
|
// If all filter funcs pass, use this config.
|
|
if passAllFilterFuncs(ctx, h.FilterFuncs, &request.Request{Req: r, W: w}) {
|
|
if !h.acceptsOpcode(r.Opcode) {
|
|
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeNotImplemented)
|
|
return
|
|
}
|
|
if h.ViewName != "" {
|
|
// if there was a view defined for this Config, set the view name in the context
|
|
ctx = context.WithValue(ctx, ViewKey{}, h.ViewName)
|
|
}
|
|
if r.Opcode != dns.OpcodeQuery || r.Question[0].Qtype != dns.TypeDS {
|
|
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
|
|
if !plugin.ClientWrite(rcode) {
|
|
errorFunc(s.Addr, w, r, rcode)
|
|
}
|
|
return
|
|
}
|
|
// The type is DS, keep the handler, but keep on searching as maybe we are serving
|
|
// the parent as well and the DS should be routed to it - this will probably *misroute* DS
|
|
// queries to a possibly grand parent, but there is no way for us to know at this point
|
|
// if there is an actual delegation from grandparent -> parent -> zone.
|
|
// In all fairness: direct DS queries should not be needed.
|
|
dshandler = h
|
|
}
|
|
}
|
|
}
|
|
off, end = dns.NextLabel(q, off)
|
|
if end {
|
|
break
|
|
}
|
|
}
|
|
|
|
if r.Question[0].Qtype == dns.TypeDS && dshandler != nil && dshandler.pluginChain != nil {
|
|
// DS request, and we found a zone, use the handler for the query.
|
|
rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r)
|
|
if !plugin.ClientWrite(rcode) {
|
|
errorFunc(s.Addr, w, r, rcode)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Wildcard match, if we have found nothing try the root zone as a last resort.
|
|
if z, ok := s.zones["."]; ok {
|
|
for _, h := range z {
|
|
if h.pluginChain == nil {
|
|
continue
|
|
}
|
|
|
|
if h.metaCollector != nil {
|
|
// Collect metadata now, so it can be used before we send a request down the plugin chain.
|
|
ctx = h.metaCollector.Collect(ctx, request.Request{Req: r, W: w})
|
|
}
|
|
|
|
// If all filter funcs pass, use this config.
|
|
if passAllFilterFuncs(ctx, h.FilterFuncs, &request.Request{Req: r, W: w}) {
|
|
if !h.acceptsOpcode(r.Opcode) {
|
|
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeNotImplemented)
|
|
return
|
|
}
|
|
if h.ViewName != "" {
|
|
// if there was a view defined for this Config, set the view name in the context
|
|
ctx = context.WithValue(ctx, ViewKey{}, h.ViewName)
|
|
}
|
|
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
|
|
if !plugin.ClientWrite(rcode) {
|
|
errorFunc(s.Addr, w, r, rcode)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Still here? Error out with REFUSED.
|
|
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
|
|
}
|
|
|
|
// msgAcceptFunc returns nil when no plugin has opted into an additional opcode,
|
|
// preserving miekg/dns's default request policy exactly.
|
|
func (s *Server) msgAcceptFunc() dns.MsgAcceptFunc {
|
|
if len(s.allowedOpcodes) == 0 {
|
|
return nil
|
|
}
|
|
return s.acceptMessage
|
|
}
|
|
|
|
func (s *Server) acceptMessage(header dns.Header) dns.MsgAcceptAction {
|
|
action := dns.DefaultMsgAcceptFunc(header)
|
|
if action != dns.MsgRejectNotImplemented {
|
|
return action
|
|
}
|
|
|
|
opcode := int(header.Bits>>11) & 0xF
|
|
if _, ok := s.allowedOpcodes[opcode]; !ok {
|
|
return action
|
|
}
|
|
if header.Qdcount != 1 {
|
|
return dns.MsgReject
|
|
}
|
|
return dns.MsgAccept
|
|
}
|
|
|
|
func (c *Config) acceptsOpcode(opcode int) bool {
|
|
if opcode == dns.OpcodeQuery || opcode == dns.OpcodeNotify {
|
|
return true
|
|
}
|
|
_, ok := c.allowedOpcodes[opcode]
|
|
return ok
|
|
}
|
|
|
|
// passAllFilterFuncs returns true if all filter funcs evaluate to true for the given request
|
|
func passAllFilterFuncs(ctx context.Context, filterFuncs []FilterFunc, req *request.Request) bool {
|
|
for _, ff := range filterFuncs {
|
|
if !ff(ctx, req) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// OnStartupComplete lists the sites served by this server
|
|
// and any relevant information, assuming Quiet is false.
|
|
func (s *Server) OnStartupComplete() {
|
|
if Quiet {
|
|
return
|
|
}
|
|
|
|
out := startUpZones("", s.Addr, s.zones)
|
|
if out != "" {
|
|
fmt.Print(out)
|
|
}
|
|
}
|
|
|
|
// Tracer returns the tracer in the server if defined.
|
|
func (s *Server) Tracer() ot.Tracer {
|
|
if s.trace == nil {
|
|
return nil
|
|
}
|
|
|
|
return s.trace.Tracer()
|
|
}
|
|
|
|
// errorFunc responds to an DNS request with an error.
|
|
func errorFunc(_server string, w dns.ResponseWriter, r *dns.Msg, rc int) {
|
|
state := request.Request{W: w, Req: r}
|
|
|
|
answer := new(dns.Msg)
|
|
answer.SetRcode(r, rc)
|
|
state.SizeAndDo(answer)
|
|
|
|
w.WriteMsg(answer)
|
|
}
|
|
|
|
func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) {
|
|
state := request.Request{W: w, Req: r}
|
|
|
|
answer := new(dns.Msg)
|
|
answer.SetRcode(r, rc)
|
|
state.SizeAndDo(answer)
|
|
|
|
vars.Report(server, state, vars.Dropped, "", rcode.ToString(rc), "" /* plugin */, answer.Len(), time.Now())
|
|
|
|
w.WriteMsg(answer)
|
|
}
|
|
|
|
const (
|
|
tcp = 0
|
|
udp = 1
|
|
|
|
tcpMaxQueries = -1
|
|
)
|
|
|
|
type (
|
|
// Key is the context key for the current server added to the context.
|
|
Key struct{}
|
|
|
|
// LoopKey is the context key to detect server wide loops.
|
|
LoopKey struct{}
|
|
|
|
// ViewKey is the context key for the current view, if defined
|
|
ViewKey struct{}
|
|
)
|
|
|
|
// EnableChaos is a map with plugin names for which we should open CH class queries as we block these by default.
|
|
var EnableChaos = map[string]struct{}{
|
|
"chaos": {},
|
|
"forward": {},
|
|
"proxy": {},
|
|
}
|
|
|
|
// Quiet mode will not show any informative output on initialization.
|
|
var Quiet bool
|