diff --git a/core/dnsserver/config_test.go b/core/dnsserver/config_test.go index dc88eacff..1e620ad48 100644 --- a/core/dnsserver/config_test.go +++ b/core/dnsserver/config_test.go @@ -118,6 +118,19 @@ func TestPropagateConfigParamsMaxHTTPSStreams(t *testing.T) { } } +func TestPropagateConfigParamsMaxQUICConnections(t *testing.T) { + n := 7 + first := &Config{MaxQUICConnections: &n} + first.firstConfigInBlock = first + second := &Config{firstConfigInBlock: first} + + propagateConfigParams([]*Config{first, second}) + + if second.MaxQUICConnections == nil || *second.MaxQUICConnections != n { + t.Fatalf("expected MaxQUICConnections to propagate to second config as %d, got %v", n, second.MaxQUICConnections) + } +} + func TestPropagateConfigParamsAllowedOpcodes(t *testing.T) { first := &Config{} first.firstConfigInBlock = first diff --git a/core/dnsserver/register.go b/core/dnsserver/register.go index f7efd27fa..a825cf193 100644 --- a/core/dnsserver/register.go +++ b/core/dnsserver/register.go @@ -320,6 +320,10 @@ func propagateConfigParams(configs []*Config) { // 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 } } diff --git a/plugin/quic/README.md b/plugin/quic/README.md index 63fe56d12..55ac8bfce 100644 --- a/plugin/quic/README.md +++ b/plugin/quic/README.md @@ -15,16 +15,18 @@ This plugin can only be used once per quic Server Block. ```txt quic { max_streams POSITIVE_INTEGER + max_connections NON_NEGATIVE_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. +* `max_connections` limits the number of concurrent QUIC connections accepted by the server. The default value is 200 if not specified. Connections above the configured limit are rejected. Set to 0 to disable the CoreDNS connection limit. * `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): +Enable DNS-over-QUIC with default settings (256 concurrent streams per connection, 200 concurrent connections, 512 worker pool size): ``` quic://.:8853 { @@ -34,13 +36,14 @@ quic://.:8853 { } ``` -Set custom limits for maximum QUIC streams per connection and worker pool size: +Set custom limits for maximum QUIC streams per connection, maximum connections, and worker pool size: ``` quic://.:8853 { tls cert.pem key.pem quic { max_streams 16 + max_connections 50 worker_pool_size 65536 } whoami diff --git a/plugin/quic/setup.go b/plugin/quic/setup.go index 4c49101fd..33faf8c82 100644 --- a/plugin/quic/setup.go +++ b/plugin/quic/setup.go @@ -54,6 +54,22 @@ func parseQuic(c *caddy.Controller) error { return c.Err("max_streams already defined for this server block") } config.MaxQUICStreams = &val + case "max_connections": + args := c.RemainingArgs() + if len(args) != 1 { + return c.ArgErr() + } + val, err := strconv.Atoi(args[0]) + if err != nil { + return c.Errf("invalid max_connections value '%s': %v", args[0], err) + } + if val < 0 { + return c.Errf("max_connections must be a non-negative integer: %d", val) + } + if config.MaxQUICConnections != nil { + return c.Err("max_connections already defined for this server block") + } + config.MaxQUICConnections = &val case "worker_pool_size": args := c.RemainingArgs() if len(args) != 1 { diff --git a/plugin/quic/setup_test.go b/plugin/quic/setup_test.go index eeffb3899..a4c15658a 100644 --- a/plugin/quic/setup_test.go +++ b/plugin/quic/setup_test.go @@ -14,6 +14,7 @@ func TestQuicSetup(t *testing.T) { input string shouldErr bool expectedMaxStreams *int + expectedMaxConnections *int expectedWorkerPoolSize *int expectedErrContent string }{ @@ -39,6 +40,20 @@ func TestQuicSetup(t *testing.T) { expectedMaxStreams: new(100), expectedWorkerPoolSize: nil, }, + { + input: `quic { + max_connections 50 + }`, + shouldErr: false, + expectedMaxConnections: new(50), + }, + { + input: `quic { + max_connections 0 + }`, + shouldErr: false, + expectedMaxConnections: new(0), + }, { input: `quic { worker_pool_size 1000 @@ -50,10 +65,12 @@ func TestQuicSetup(t *testing.T) { { input: `quic { max_streams 100 + max_connections 50 worker_pool_size 1000 }`, shouldErr: false, expectedMaxStreams: new(100), + expectedMaxConnections: new(50), expectedWorkerPoolSize: new(1000), }, { @@ -98,6 +115,43 @@ func TestQuicSetup(t *testing.T) { shouldErr: true, expectedErrContent: "positive integer", }, + { + input: `quic { + max_connections + }`, + shouldErr: true, + expectedErrContent: "Wrong argument count", + }, + { + input: `quic { + max_connections abc + }`, + shouldErr: true, + expectedErrContent: "invalid max_connections value", + }, + { + input: `quic { + max_connections -10 + }`, + shouldErr: true, + expectedErrContent: "non-negative integer", + }, + { + input: `quic { + max_connections 50 + max_connections 100 + }`, + shouldErr: true, + expectedErrContent: "already defined", + expectedMaxConnections: new(50), + }, + { + input: `quic { + max_connections 100 200 + }`, + shouldErr: true, + expectedErrContent: "Wrong argument count", + }, { input: `quic { worker_pool_size @@ -189,6 +243,7 @@ func TestQuicSetup(t *testing.T) { if !test.shouldErr || (test.shouldErr && strings.Contains(test.expectedErrContent, "already defined")) { config := dnsserver.GetConfig(c) assertMaxStreamsValue(t, i, test.input, config.MaxQUICStreams, test.expectedMaxStreams) + assertMaxConnectionsValue(t, i, test.input, config.MaxQUICConnections, test.expectedMaxConnections) assertWorkerPoolSizeValue(t, i, test.input, config.MaxQUICWorkerPoolSize, test.expectedWorkerPoolSize) } } @@ -213,6 +268,25 @@ func assertMaxStreamsValue(t *testing.T, testIndex int, testInput string, actual } } +// assertMaxConnectionsValue compares the actual MaxQUICConnections value with the expected one +func assertMaxConnectionsValue(t *testing.T, testIndex int, testInput string, actual, expected *int) { + t.Helper() + if actual == nil && expected == nil { + return + } + + if (actual == nil) != (expected == nil) { + t.Errorf("Test %d (%s): Expected MaxQUICConnections to be %v, but got %v", + testIndex, testInput, formatNilableInt(expected), formatNilableInt(actual)) + return + } + + if *actual != *expected { + t.Errorf("Test %d (%s): Expected MaxQUICConnections 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) { t.Helper()