mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 00:04:15 -04:00
feat: limit concurrent DoQ streams and goroutines (#7296)
This commit is contained in:
48
plugin/quic/README.md
Normal file
48
plugin/quic/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# quic
|
||||
|
||||
## Name
|
||||
|
||||
*quic* - configures DNS-over-QUIC (DoQ) server options.
|
||||
|
||||
## Description
|
||||
|
||||
The *quic* plugin allows you to configure parameters for the DNS-over-QUIC (DoQ) server to fine-tune the security posture and performance of the server.
|
||||
|
||||
This plugin can only be used once per quic Server Block.
|
||||
|
||||
## Syntax
|
||||
|
||||
```txt
|
||||
quic {
|
||||
max_streams POSITIVE_INTEGER
|
||||
worker_pool_size POSITIVE_INTEGER
|
||||
}
|
||||
```
|
||||
|
||||
* `max_streams` limits the number of concurrent QUIC streams per connection. This helps prevent DoS attacks where an attacker could open many streams on a single connection, exhausting server resources. The default value is 256 if not specified.
|
||||
* `worker_pool_size` defines the size of the worker pool for processing QUIC streams across all connections. The default value is 512 if not specified. This limits the total number of concurrent streams that can be processed across all connections.
|
||||
|
||||
## Examples
|
||||
|
||||
Enable DNS-over-QUIC with default settings (256 concurrent streams per connection, 512 worker pool size):
|
||||
|
||||
```
|
||||
quic://.:8853 {
|
||||
tls cert.pem key.pem
|
||||
quic
|
||||
whoami
|
||||
}
|
||||
```
|
||||
|
||||
Set custom limits for maximum QUIC streams per connection and worker pool size:
|
||||
|
||||
```
|
||||
quic://.:8853 {
|
||||
tls cert.pem key.pem
|
||||
quic {
|
||||
max_streams 16
|
||||
worker_pool_size 65536
|
||||
}
|
||||
whoami
|
||||
}
|
||||
```
|
||||
79
plugin/quic/setup.go
Normal file
79
plugin/quic/setup.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("quic", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
err := parseQuic(c)
|
||||
if err != nil {
|
||||
return plugin.Error("quic", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseQuic(c *caddy.Controller) error {
|
||||
config := dnsserver.GetConfig(c)
|
||||
|
||||
// Skip the "quic" directive itself
|
||||
c.Next()
|
||||
|
||||
// Get any arguments on the "quic" line
|
||||
args := c.RemainingArgs()
|
||||
if len(args) > 0 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
|
||||
// Process all nested directives in the block
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "max_streams":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
val, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return c.Errf("invalid max_streams value '%s': %v", args[0], err)
|
||||
}
|
||||
if val <= 0 {
|
||||
return c.Errf("max_streams must be a positive integer: %d", val)
|
||||
}
|
||||
if config.MaxQUICStreams != nil {
|
||||
return c.Err("max_streams already defined for this server block")
|
||||
}
|
||||
config.MaxQUICStreams = &val
|
||||
case "worker_pool_size":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
val, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return c.Errf("invalid worker_pool_size value '%s': %v", args[0], err)
|
||||
}
|
||||
if val <= 0 {
|
||||
return c.Errf("worker_pool_size must be a positive integer: %d", val)
|
||||
}
|
||||
if config.MaxQUICWorkerPoolSize != nil {
|
||||
return c.Err("worker_pool_size already defined for this server block")
|
||||
}
|
||||
config.MaxQUICWorkerPoolSize = &val
|
||||
default:
|
||||
return c.Errf("unknown property '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
242
plugin/quic/setup_test.go
Normal file
242
plugin/quic/setup_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
)
|
||||
|
||||
func TestQuicSetup(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedMaxStreams *int
|
||||
expectedWorkerPoolSize *int
|
||||
expectedErrContent string
|
||||
}{
|
||||
// Valid configurations
|
||||
{
|
||||
input: `quic`,
|
||||
shouldErr: false,
|
||||
expectedMaxStreams: nil,
|
||||
expectedWorkerPoolSize: nil,
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
}`,
|
||||
shouldErr: false,
|
||||
expectedMaxStreams: nil,
|
||||
expectedWorkerPoolSize: nil,
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
max_streams 100
|
||||
}`,
|
||||
shouldErr: false,
|
||||
expectedMaxStreams: pint(100),
|
||||
expectedWorkerPoolSize: nil,
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
worker_pool_size 1000
|
||||
}`,
|
||||
shouldErr: false,
|
||||
expectedMaxStreams: nil,
|
||||
expectedWorkerPoolSize: pint(1000),
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
max_streams 100
|
||||
worker_pool_size 1000
|
||||
}`,
|
||||
shouldErr: false,
|
||||
expectedMaxStreams: pint(100),
|
||||
expectedWorkerPoolSize: pint(1000),
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
# Comment
|
||||
}`,
|
||||
shouldErr: false,
|
||||
expectedMaxStreams: nil,
|
||||
expectedWorkerPoolSize: nil,
|
||||
},
|
||||
// Invalid configurations
|
||||
{
|
||||
input: `quic arg`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "Wrong argument count",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
max_streams
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "Wrong argument count",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
max_streams abc
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "invalid max_streams value",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
max_streams 0
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "positive integer",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
max_streams -10
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "positive integer",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
worker_pool_size
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "Wrong argument count",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
worker_pool_size abc
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "invalid worker_pool_size value",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
worker_pool_size 0
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "positive integer",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
worker_pool_size -10
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "positive integer",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
max_streams 100
|
||||
max_streams 200
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "already defined",
|
||||
expectedMaxStreams: pint(100),
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
worker_pool_size 1000
|
||||
worker_pool_size 2000
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "already defined",
|
||||
expectedWorkerPoolSize: pint(1000),
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
unknown_directive
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "unknown property",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
max_streams 100 200
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "Wrong argument count",
|
||||
},
|
||||
{
|
||||
input: `quic {
|
||||
worker_pool_size 1000 2000
|
||||
}`,
|
||||
shouldErr: true,
|
||||
expectedErrContent: "Wrong argument count",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
err := setup(c)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d (%s): Expected error but found none", i, test.input)
|
||||
continue
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d (%s): Expected no error but found: %v", i, test.input, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if test.shouldErr && !strings.Contains(err.Error(), test.expectedErrContent) {
|
||||
t.Errorf("Test %d (%s): Expected error containing '%s', but got: %v",
|
||||
i, test.input, test.expectedErrContent, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !test.shouldErr || (test.shouldErr && strings.Contains(test.expectedErrContent, "already defined")) {
|
||||
config := dnsserver.GetConfig(c)
|
||||
assertMaxStreamsValue(t, i, test.input, config.MaxQUICStreams, test.expectedMaxStreams)
|
||||
assertWorkerPoolSizeValue(t, i, test.input, config.MaxQUICWorkerPoolSize, test.expectedWorkerPoolSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assertMaxStreamsValue compares the actual MaxQUICStreams value with the expected one
|
||||
func assertMaxStreamsValue(t *testing.T, testIndex int, testInput string, actual, expected *int) {
|
||||
if actual == nil && expected == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if (actual == nil) != (expected == nil) {
|
||||
t.Errorf("Test %d (%s): Expected MaxQUICStreams to be %v, but got %v",
|
||||
testIndex, testInput, formatNilableInt(expected), formatNilableInt(actual))
|
||||
return
|
||||
}
|
||||
|
||||
if *actual != *expected {
|
||||
t.Errorf("Test %d (%s): Expected MaxQUICStreams to be %d, but got %d",
|
||||
testIndex, testInput, *expected, *actual)
|
||||
}
|
||||
}
|
||||
|
||||
// assertWorkerPoolSizeValue compares the actual MaxQUICWorkerPoolSize value with the expected one
|
||||
func assertWorkerPoolSizeValue(t *testing.T, testIndex int, testInput string, actual, expected *int) {
|
||||
if actual == nil && expected == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if (actual == nil) != (expected == nil) {
|
||||
t.Errorf("Test %d (%s): Expected MaxQUICWorkerPoolSize to be %v, but got %v",
|
||||
testIndex, testInput, formatNilableInt(expected), formatNilableInt(actual))
|
||||
return
|
||||
}
|
||||
|
||||
if *actual != *expected {
|
||||
t.Errorf("Test %d (%s): Expected MaxQUICWorkerPoolSize to be %d, but got %d",
|
||||
testIndex, testInput, *expected, *actual)
|
||||
}
|
||||
}
|
||||
|
||||
func formatNilableInt(v *int) string {
|
||||
if v == nil {
|
||||
return "nil"
|
||||
}
|
||||
return fmt.Sprintf("%d", *v)
|
||||
}
|
||||
|
||||
func pint(i int) *int {
|
||||
return &i
|
||||
}
|
||||
Reference in New Issue
Block a user