Files
coredns/core/dnsserver/register.go
Aradhya Jain 45ac7fe659 plugin/quic: add max_connections directive (#8610)
* plugin/quic: add max_connections directive

Brings quic to parity with https and https3, which both already expose
max_connections. The server-side plumbing already exists and is already
wired up: core/dnsserver/config.go declares MaxQUICConnections *int, and
server_quic.go already consumes it (DefaultQUICMaxConnections = 200,
gates accepted connections on a semaphore sized from it). It was just
never parsed from a Corefile directive for the quic plugin itself, so
setting it today fails with "unknown property 'max_connections'".

Adds the max_connections case to parseQuic, mirroring https3's exact
validation (non-negative integer, 0 disables the limit, duplicate
definition rejected) since the two share a config field. #8187 did the
same shaped fix for https3 reaching parity with https; this does the
equivalent for quic, the one remaining sibling without it.

Extends TestQuicSetup with max_connections coverage (valid value, 0,
missing arg, non-numeric, negative, duplicate definition, extra arg).
Verified it fails without the setup.go change (unknown property error)
and passes with it.

Signed-off-by: stackedbyaradhya <aradhyaj736@gmail.com>

* core/dnsserver: propagate MaxQUICConnections across server block keys

Addresses review feedback on this PR: Caddy only runs directive setup
for the first key in a server block, and the QUIC server reads
MaxQUICConnections from group[0]. propagateConfigParams copies
listener-wide settings from the first config in a block to the others,
but it did not propagate this field. In a multi-transport block where
the quic key isn't first, e.g.:

    .:53 quic://.:8853 {
        quic {
            max_connections 1
        }
    }

the value was stored on the first (DNS) config while the QUIC config
kept MaxQUICConnections nil, so NewServerQUIC silently fell back to the
default limit of 200 instead of the configured one.

Adds the propagation line, mirroring the existing MaxHTTPSStreams entry
(and its comment) added for the equivalent key-order issue on https.

Added TestPropagateConfigParamsMaxQUICConnections, mirroring the
existing TestPropagateConfigParamsMaxHTTPSStreams. Verified it fails
without the register.go change and passes with it.

Signed-off-by: stackedbyaradhya <aradhyaj736@gmail.com>

---------

Signed-off-by: stackedbyaradhya <aradhyaj736@gmail.com>
2026-10-08 05:24:25 -07:00

427 lines
14 KiB
Go

package dnsserver
import (
"fmt"
"net"
"slices"
"sync"
"time"
"github.com/coredns/caddy"
"github.com/coredns/caddy/caddyfile"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/parse"
"github.com/coredns/coredns/plugin/pkg/transport"
"github.com/miekg/dns"
)
const serverType = "dns"
// Register registers the DNS server type with Caddy. Repeated calls return the
// result of the first call without registering again. An existing server type
// registered by another caller is left unchanged and causes an error.
//
// Default builds call Register automatically. When built with the
// coredns_manual_registration tag, an embedding host must call Register before
// starting Caddy. Register neither registers plugins nor starts listeners.
//
// Concurrent calls to Register are safe, but the first call must not run
// concurrently with other Caddy configuration or startup operations.
func Register() error { return registerServerType() }
var registerServerType = sync.OnceValue(func() error {
if slices.Contains(caddy.ListPlugins()["server_types"], serverType) {
return fmt.Errorf("dnsserver: server type %q already registered", serverType)
}
caddy.RegisterServerType(serverType, caddy.ServerType{
Directives: func() []string { return Directives },
DefaultInput: func() caddy.Input {
return caddy.CaddyfileInput{
Filepath: "Corefile",
Contents: []byte(".:" + Port + " {\nwhoami\nlog\n}\n"),
ServerTypeName: serverType,
}
},
NewContext: newContext,
})
return nil
})
func newContext(_i *caddy.Instance) caddy.Context {
return &dnsContext{keysToConfigs: make(map[string]*Config)}
}
type dnsContext struct {
keysToConfigs map[string]*Config
// configs is the master list of all site configs.
configs []*Config
}
func (h *dnsContext) saveConfig(key string, cfg *Config) {
h.configs = append(h.configs, cfg)
h.keysToConfigs[key] = cfg
}
// Compile-time check to ensure dnsContext implements the caddy.Context interface
var _ caddy.Context = &dnsContext{}
// InspectServerBlocks make sure that everything checks out before
// executing directives and otherwise prepares the directives to
// be parsed and executed.
func (h *dnsContext) InspectServerBlocks(_sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
// Normalize and check all the zone names and check for duplicates
for ib, s := range serverBlocks {
// Walk the s.Keys and expand any reverse address in their proper DNS in-addr zones. If the expansions leads for
// more than one reverse zone, replace the current value and add the rest to s.Keys.
zoneAddrs := []zoneAddr{}
for ik, k := range s.Keys {
trans, k1 := parse.Transport(k) // get rid of any dns:// or other scheme.
hosts, port, err := plugin.SplitHostPort(k1)
// We need to make this a fully qualified domain name to catch all errors here and not later when
// plugin.Normalize is called again on these strings, with the prime difference being that the domain
// name is fully qualified. This was found by fuzzing where "ȶ" is deemed OK, but "ȶ." is not (might be a
// bug in miekg/dns actually). But here we were checking ȶ, which is OK, and later we barf in ȶ. leading to
// "index out of range".
for ih := range hosts {
_, _, err := plugin.SplitHostPort(dns.Fqdn(hosts[ih]))
if err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}
if port == "" {
switch trans {
case transport.DNS:
port = Port
case transport.TLS:
port = transport.TLSPort
case transport.QUIC:
port = transport.QUICPort
case transport.GRPC:
port = transport.GRPCPort
case transport.HTTPS:
port = transport.HTTPSPort
case transport.HTTPS3:
port = transport.HTTPSPort
}
}
if len(hosts) > 1 {
s.Keys[ik] = hosts[0] + ":" + port // replace for the first
for _, h := range hosts[1:] { // add the rest
s.Keys = append(s.Keys, h+":"+port)
}
}
for i := range hosts {
zoneAddrs = append(zoneAddrs, zoneAddr{Zone: plugin.Name(hosts[i]).Normalize(), Port: port, Transport: trans})
}
}
serverBlocks[ib].Keys = s.Keys // important to save back the new keys that are potentially created here.
var firstConfigInBlock *Config
for ik := range s.Keys {
za := zoneAddrs[ik]
s.Keys[ik] = za.String()
// Save the config to our master list, and key it for lookups.
cfg := &Config{
Zone: za.Zone,
ListenHosts: []string{""},
Port: za.Port,
Transport: za.Transport,
}
// Set reference to the first config in the current block.
// This is used later by MakeServers to share a single plugin list
// for all zones in a server block.
if ik == 0 {
firstConfigInBlock = cfg
}
cfg.firstConfigInBlock = firstConfigInBlock
keyConfig := keyForConfig(ib, ik)
h.saveConfig(keyConfig, cfg)
}
}
return serverBlocks, nil
}
// MakeServers uses the newly-created siteConfigs to create and return a list of server instances.
func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
// Copy parameters from first config in the block to all other config in the same block
propagateConfigParams(h.configs)
// we must map (group) each config to a bind address
groups, err := groupConfigsByListenAddr(h.configs)
if err != nil {
return nil, err
}
// then we create a server for each group
var servers []caddy.Server
for addr, group := range groups {
serversForGroup, err := makeServersForGroup(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, serversForGroup...)
}
// For each server config, check for View Filter plugins
for _, c := range h.configs {
// Add filters in the plugin.cfg order for consistent filter func evaluation order.
for _, d := range Directives {
if vf, ok := c.registry[d].(Viewer); ok {
if c.ViewName != "" {
return nil, fmt.Errorf("multiple views defined in server block")
}
c.ViewName = vf.ViewName()
c.FilterFuncs = append(c.FilterFuncs, vf.Filter)
}
}
}
// Verify that there is no overlap on the zones and listen addresses
// for unfiltered server configs
errValid := h.validateZonesAndListeningAddresses()
if errValid != nil {
return nil, errValid
}
return servers, nil
}
// AddPlugin adds a plugin to a site's plugin stack.
func (c *Config) AddPlugin(m plugin.Plugin) {
c.Plugin = append(c.Plugin, m)
}
// AllowOpcode permits a non-default DNS opcode to reach this config's plugin chain
// on UDP, TCP, and DNS-over-TLS listeners. Plugins should call it during setup.
// The listener still requires exactly one question, and configs that do not opt in
// continue to reject the opcode.
func (c *Config) AllowOpcode(opcode int) {
if c.allowedOpcodes == nil {
c.allowedOpcodes = make(map[int]struct{})
}
c.allowedOpcodes[opcode] = struct{}{}
}
// registerHandler adds a handler to a site's handler registration. Handlers
//
// use this to announce that they exist to other plugin.
func (c *Config) registerHandler(h plugin.Handler) {
if c.registry == nil {
c.registry = make(map[string]plugin.Handler)
}
// Just overwrite...
c.registry[h.Name()] = h
}
// Handler returns the plugin handler that has been added to the config under its name.
// This is useful to inspect if a certain plugin is active in this server.
// Note that this is order dependent and the order is defined in directives.go, i.e. if your plugin
// comes before the plugin you are checking; it will not be there (yet).
func (c *Config) Handler(name string) plugin.Handler {
if c.registry == nil {
return nil
}
if h, ok := c.registry[name]; ok {
return h
}
return nil
}
// Handlers returns a slice of plugins that have been registered. This can be used to
// inspect and interact with registered plugins but cannot be used to remove or add plugins.
// Note that this is order dependent and the order is defined in directives.go, i.e. if your plugin
// comes before the plugin you are checking; it will not be there (yet).
func (c *Config) Handlers() []plugin.Handler {
if c.registry == nil {
return nil
}
hs := make([]plugin.Handler, 0, len(c.registry))
for _, k := range Directives {
registry := c.Handler(k)
if registry != nil {
hs = append(hs, registry)
}
}
return hs
}
func (h *dnsContext) validateZonesAndListeningAddresses() error {
//Validate Zone and addresses
checker := newOverlapZone()
for _, conf := range h.configs {
for _, h := range conf.ListenHosts {
// Validate the overlapping of ZoneAddr
akey := zoneAddr{Transport: conf.Transport, Zone: conf.Zone, Address: h, Port: conf.Port}
var existZone, overlapZone *zoneAddr
if len(conf.FilterFuncs) > 0 {
// This config has filters (e.g. view plugin). It is allowed to
// share a zone/port with an unfiltered server block, so we only
// check without registering and skip the "already defined" error.
_, overlapZone = checker.check(akey)
} else {
// This config has no filters. Check for overlap with other
// unfiltered configs and register the zone.
existZone, overlapZone = checker.registerAndCheck(akey)
}
if existZone != nil {
return fmt.Errorf("cannot serve %s - it is already defined", akey.String())
}
if overlapZone != nil {
return fmt.Errorf("cannot serve %s - zone overlap listener capacity with %v", akey.String(), overlapZone.String())
}
}
}
return nil
}
// propagateConfigParams copies the necessary parameters from first config in the block
// to all other config in the same block. Doing this results in zones
// sharing the same plugin instances and settings as other zones in
// the same block.
func propagateConfigParams(configs []*Config) {
for _, c := range configs {
c.Plugin = c.firstConfigInBlock.Plugin
c.ListenHosts = c.firstConfigInBlock.ListenHosts
c.Debug = c.firstConfigInBlock.Debug
c.Stacktrace = c.firstConfigInBlock.Stacktrace
c.NumSockets = c.firstConfigInBlock.NumSockets
// Fork TLSConfig for each encrypted connection while preserving the
// listener-wide policy identity used to compare dynamic callbacks.
c.TLSConfig = c.firstConfigInBlock.TLSConfig.Clone()
c.tlsConfigIdentity = c.firstConfigInBlock.tlsConfigIdentity
c.ReadTimeout = c.firstConfigInBlock.ReadTimeout
c.WriteTimeout = c.firstConfigInBlock.WriteTimeout
c.IdleTimeout = c.firstConfigInBlock.IdleTimeout
c.MaxTCPQueries = c.firstConfigInBlock.MaxTCPQueries
c.TsigSecret = c.firstConfigInBlock.TsigSecret
c.allowedOpcodes = c.firstConfigInBlock.allowedOpcodes
// Propagate HTTPRequestValidateFunc so that custom path validators work in
// multi-transport blocks. Otherwise HTTPS 404s on non-"/dns-query" paths.
c.HTTPRequestValidateFunc = c.firstConfigInBlock.HTTPRequestValidateFunc
// Propagate UDPDecorateWriterFunc so a decorator configured once in a
// server block applies to the block's UDP listener(s).
c.UDPDecorateWriterFunc = c.firstConfigInBlock.UDPDecorateWriterFunc
// Propagate MaxHTTPSStreams so a `https { max_streams N }` set once in a
// server block applies to the block's HTTPS key regardless of key order.
c.MaxHTTPSStreams = c.firstConfigInBlock.MaxHTTPSStreams
// Propagate MaxQUICConnections so a `quic { max_connections N }` set once
// in a server block applies to the block's QUIC key regardless of key order.
c.MaxQUICConnections = c.firstConfigInBlock.MaxQUICConnections
}
}
// groupConfigsByListenAddr groups site configs by their listen
// (bind) address, so sites that use the same listener can be served
// on the same server instance. The return value maps the listen
// address (what you pass into net.Listen) to the list of site configs.
// This function does NOT vet the configs to ensure they are compatible.
func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
groups := make(map[string][]*Config)
for _, conf := range configs {
for _, h := range conf.ListenHosts {
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(h, conf.Port))
if err != nil {
return nil, err
}
addrstr := conf.Transport + "://" + addr.String()
groups[addrstr] = append(groups[addrstr], conf)
}
}
return groups, nil
}
// makeServersForGroup creates servers for a specific transport and group.
// It creates as many servers as specified in the NumSockets configuration.
// If the NumSockets param is not specified, one server is created by default.
func makeServersForGroup(addr string, group []*Config) ([]caddy.Server, error) {
// that is impossible, but better to check
if len(group) == 0 {
return nil, fmt.Errorf("no configs for group defined")
}
// create one server by default if no NumSockets specified
numSockets := 1
if group[0].NumSockets > 0 {
numSockets = group[0].NumSockets
}
var servers []caddy.Server
for range numSockets {
// switch on addr
switch tr, _ := parse.Transport(addr); tr {
case transport.DNS:
s, err := NewServer(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
case transport.TLS:
s, err := NewServerTLS(addr, group)
if err != nil {
return nil, err
}
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 {
return nil, err
}
servers = append(servers, s)
case transport.HTTPS:
s, err := NewServerHTTPS(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
case transport.HTTPS3:
s, err := NewServerHTTPS3(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
}
}
return servers, nil
}
// DefaultPort is the default port.
const DefaultPort = transport.Port
// These "soft defaults" are configurable by
// command line flags, etc.
var (
// Port is the port we listen on by default.
Port = DefaultPort
// GracefulTimeout is the maximum duration of a graceful shutdown.
GracefulTimeout time.Duration
)