mirror of
https://github.com/coredns/coredns.git
synced 2026-04-05 19:55:32 -04:00
proxyproto: add UDP session tracking for Spectrum PPv2 (#7967)
This commit is contained in:
@@ -16,6 +16,7 @@ client's IP address and port information.
|
||||
proxyproto {
|
||||
allow <CIDR...>
|
||||
default <use|ignore|reject|skip>
|
||||
udp_session_tracking <duration> [max_sessions]
|
||||
}
|
||||
~~~
|
||||
|
||||
@@ -23,15 +24,26 @@ If `allow` is unspecified, PROXY protocol headers are accepted from all IP addre
|
||||
The `default` option controls how connections from sources not listed in `allow` are handled.
|
||||
If `default` is unspecified, it defaults to `ignore`.
|
||||
The possible values are:
|
||||
|
||||
- `use`: accept and use PROXY protocol headers from these sources
|
||||
- `ignore`: accept and ignore PROXY protocol headers from other sources
|
||||
- `reject`: reject connections with PROXY protocol headers from other sources
|
||||
- `skip`: skip PROXY protocol processing for connections from other sources, treating them as normal connections preserving the PROXY protocol headers.
|
||||
|
||||
The `udp_session_tracking <duration> [max_sessions]` option enables UDP session state tracking
|
||||
for Cloudflare Spectrum's PROXY Protocol v2 over UDP. Spectrum sends the PPv2 header as a
|
||||
standalone first datagram (with no DNS payload). Subsequent datagrams from the same client arrive
|
||||
without any header. When this option is set to a positive duration, the real client address from
|
||||
the header-only datagram is cached (keyed by the Spectrum-side remote address) for that duration
|
||||
and automatically applied to all subsequent headerless datagrams within that window. The TTL is
|
||||
refreshed on each matching packet. The optional `max_sessions` argument caps the number of
|
||||
concurrent sessions in the LRU cache (default: 10240). This option has no effect for TCP
|
||||
connections.
|
||||
|
||||
## Examples
|
||||
|
||||
In this configuration, we allow PROXY protocol connections from all IP addresses:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxyproto
|
||||
@@ -41,6 +53,7 @@ In this configuration, we allow PROXY protocol connections from all IP addresses
|
||||
|
||||
In this configuration, we only allow PROXY protocol connections from the specified CIDR ranges
|
||||
and ignore proxy protocol headers from other sources:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxyproto {
|
||||
@@ -52,6 +65,7 @@ and ignore proxy protocol headers from other sources:
|
||||
|
||||
In this configuration, we only allow PROXY protocol headers from the specified CIDR ranges and reject
|
||||
connections without valid PROXY protocol headers from those sources:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxyproto {
|
||||
@@ -61,3 +75,29 @@ connections without valid PROXY protocol headers from those sources:
|
||||
forward . /etc/resolv.conf
|
||||
}
|
||||
~~~
|
||||
|
||||
In this configuration, we enable UDP session tracking for Cloudflare Spectrum's PPv2-over-UDP
|
||||
with a 28-second TTL (slightly shorter than Spectrum's 30-second UDP idle timeout) and the
|
||||
default session cap of 10240:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxyproto {
|
||||
allow 192.168.1.1/32
|
||||
udp_session_tracking 28s
|
||||
}
|
||||
forward . /etc/resolv.conf
|
||||
}
|
||||
~~~
|
||||
|
||||
In this configuration, the session cap is raised to 20480:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxyproto {
|
||||
allow 192.168.1.1/32
|
||||
udp_session_tracking 28s 20000
|
||||
}
|
||||
forward . /etc/resolv.conf
|
||||
}
|
||||
~~~
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
@@ -21,8 +23,10 @@ func setup(c *caddy.Controller) error {
|
||||
return plugin.Error("proxyproto", errors.New("proxy protocol already configured for this server instance"))
|
||||
}
|
||||
var (
|
||||
allowedIPNets []*net.IPNet
|
||||
policy = proxyproto.IGNORE
|
||||
allowedIPNets []*net.IPNet
|
||||
policy = proxyproto.IGNORE
|
||||
sessionTrackingTTL time.Duration
|
||||
sessionTrackingMaxSessions int
|
||||
)
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
@@ -56,6 +60,23 @@ func setup(c *caddy.Controller) error {
|
||||
default:
|
||||
return plugin.Error("proxyproto", c.ArgErr())
|
||||
}
|
||||
case "udp_session_tracking":
|
||||
v := c.RemainingArgs()
|
||||
if len(v) < 1 || len(v) > 2 {
|
||||
return plugin.Error("proxyproto", c.ArgErr())
|
||||
}
|
||||
d, err := time.ParseDuration(v[0])
|
||||
if err != nil {
|
||||
return plugin.Error("proxyproto", fmt.Errorf("udp_session_tracking: invalid duration %q: %w", v[0], err))
|
||||
}
|
||||
sessionTrackingTTL = d
|
||||
if len(v) == 2 {
|
||||
n, err := strconv.Atoi(v[1])
|
||||
if err != nil || n <= 0 {
|
||||
return plugin.Error("proxyproto", fmt.Errorf("udp_session_tracking: invalid max sessions %q: must be a positive integer", v[1]))
|
||||
}
|
||||
sessionTrackingMaxSessions = n
|
||||
}
|
||||
default:
|
||||
return c.Errf("unknown option '%s'", c.Val())
|
||||
}
|
||||
@@ -77,5 +98,7 @@ func setup(c *caddy.Controller) error {
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
config.ProxyProtoUDPSessionTrackingTTL = sessionTrackingTTL
|
||||
config.ProxyProtoUDPSessionTrackingMaxSessions = sessionTrackingMaxSessions
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package proxyproto
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
@@ -10,23 +11,41 @@ import (
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedRoot string // expected root, set to the controller. Empty for negative cases.
|
||||
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
||||
config bool
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedRoot string // expected root, set to the controller. Empty for negative cases.
|
||||
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
||||
config bool
|
||||
sessionTrackingTTL time.Duration
|
||||
sessionTrackingMaxSessions int
|
||||
}{
|
||||
// positive
|
||||
{"proxyproto", false, "", "", true},
|
||||
{"proxyproto {\nallow 127.0.0.1/8 ::1/128\n}", false, "", "", true},
|
||||
{"proxyproto {\nallow 127.0.0.1/8 ::1/128\ndefault ignore\n}", false, "", "", true},
|
||||
{"proxyproto", false, "", "", true, 0, 0},
|
||||
{"proxyproto {\nallow 127.0.0.1/8 ::1/128\n}", false, "", "", true, 0, 0},
|
||||
{"proxyproto {\nallow 127.0.0.1/8 ::1/128\ndefault ignore\n}", false, "", "", true, 0, 0},
|
||||
// Allow without any IPs is also valid
|
||||
{"proxyproto {\nallow\n}", false, "", "", true},
|
||||
{"proxyproto {\nallow\n}", false, "", "", true, 0, 0},
|
||||
// udp_session_tracking with TTL only (max sessions gonna use package default)
|
||||
{"proxyproto {\nudp_session_tracking 28s\n}", false, "", "", true, 28 * time.Second, 0},
|
||||
{"proxyproto {\nallow 10.0.0.0/8\nudp_session_tracking 1m\n}", false, "", "", true, time.Minute, 0},
|
||||
// udp_session_tracking with explicit max sessions
|
||||
{"proxyproto {\nudp_session_tracking 28s 20000\n}", false, "", "", true, 28 * time.Second, 20000},
|
||||
{"proxyproto {\nallow 10.0.0.0/8\nudp_session_tracking 1m 500\n}", false, "", "", true, time.Minute, 500},
|
||||
// negative
|
||||
{"proxyproto {\nunknown\n}", true, "", "unknown option", false},
|
||||
{"proxyproto extra_arg", true, "", "Wrong argument", false},
|
||||
{"proxyproto {\nallow invalid_ip\n}", true, "", "invalid CIDR address", false},
|
||||
{"proxyproto {\nallow 127.0.0.1/8\ndefault invalid_policy\n}", true, "", "Wrong argument", false},
|
||||
{"proxyproto {\nunknown\n}", true, "", "unknown option", false, 0, 0},
|
||||
{"proxyproto extra_arg", true, "", "Wrong argument", false, 0, 0},
|
||||
{"proxyproto {\nallow invalid_ip\n}", true, "", "invalid CIDR address", false, 0, 0},
|
||||
{"proxyproto {\nallow 127.0.0.1/8\ndefault invalid_policy\n}", true, "", "Wrong argument", false, 0, 0},
|
||||
// udp_session_tracking: missing TTL
|
||||
{"proxyproto {\nudp_session_tracking\n}", true, "", "Wrong argument", false, 0, 0},
|
||||
// udp_session_tracking: too many arguments
|
||||
{"proxyproto {\nudp_session_tracking 28s 100 extra\n}", true, "", "Wrong argument", false, 0, 0},
|
||||
// udp_session_tracking: bad TTL
|
||||
{"proxyproto {\nudp_session_tracking notaduration\n}", true, "", "invalid duration", false, 0, 0},
|
||||
// udp_session_tracking: bad max sessions
|
||||
{"proxyproto {\nudp_session_tracking 28s notanumber\n}", true, "", "invalid max sessions", false, 0, 0},
|
||||
{"proxyproto {\nudp_session_tracking 28s 0\n}", true, "", "invalid max sessions", false, 0, 0},
|
||||
{"proxyproto {\nudp_session_tracking 28s -1\n}", true, "", "invalid max sessions", false, 0, 0},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
@@ -40,6 +59,16 @@ func TestSetup(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected ProxyProtoConnPolicy to NOT be configured for input %s", i, test.input)
|
||||
}
|
||||
|
||||
if cfg.ProxyProtoUDPSessionTrackingTTL != test.sessionTrackingTTL {
|
||||
t.Errorf("Test %d: Expected ProxyProtoUDPSessionTrackingTTL %v, got %v for input %s",
|
||||
i, test.sessionTrackingTTL, cfg.ProxyProtoUDPSessionTrackingTTL, test.input)
|
||||
}
|
||||
|
||||
if cfg.ProxyProtoUDPSessionTrackingMaxSessions != test.sessionTrackingMaxSessions {
|
||||
t.Errorf("Test %d: Expected ProxyProtoUDPSessionTrackingMaxSessions %d, got %d for input %s",
|
||||
i, test.sessionTrackingMaxSessions, cfg.ProxyProtoUDPSessionTrackingMaxSessions, test.input)
|
||||
}
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user