proxyproto: add UDP session tracking for Spectrum PPv2 (#7967)

This commit is contained in:
Minghang Chen
2026-03-28 15:06:36 -07:00
committed by GitHub
parent 12d9457e71
commit 34acf8353f
10 changed files with 398 additions and 19 deletions

View File

@@ -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
}
~~~

View File

@@ -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
}

View File

@@ -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)
}