Add support for ipv6 subnet in ipStrategy
This commit is contained in:
parent
a398536688
commit
312ebb17ab
17 changed files with 544 additions and 12 deletions
57
pkg/config/dynamic/middleware_test.go
Normal file
57
pkg/config/dynamic/middleware_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package dynamic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetStrategy_ipv6Subnet(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
expectError bool
|
||||
ipv6Subnet *int
|
||||
}{
|
||||
{
|
||||
desc: "Nil subnet",
|
||||
},
|
||||
{
|
||||
desc: "Zero subnet",
|
||||
expectError: true,
|
||||
ipv6Subnet: intPtr(0),
|
||||
},
|
||||
{
|
||||
desc: "Subnet greater that 128",
|
||||
expectError: true,
|
||||
ipv6Subnet: intPtr(129),
|
||||
},
|
||||
{
|
||||
desc: "Valid subnet",
|
||||
ipv6Subnet: intPtr(128),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
strategy := IPStrategy{
|
||||
IPv6Subnet: test.ipv6Subnet,
|
||||
}
|
||||
|
||||
get, err := strategy.Get()
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, get)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, get)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func intPtr(value int) *int {
|
||||
return &value
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package dynamic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
@ -405,6 +406,8 @@ type IPStrategy struct {
|
|||
Depth int `json:"depth,omitempty" toml:"depth,omitempty" yaml:"depth,omitempty" export:"true"`
|
||||
// ExcludedIPs configures Traefik to scan the X-Forwarded-For header and select the first IP not in the list.
|
||||
ExcludedIPs []string `json:"excludedIPs,omitempty" toml:"excludedIPs,omitempty" yaml:"excludedIPs,omitempty"`
|
||||
// IPv6Subnet configures Traefik to consider all IPv6 addresses from the defined subnet as originating from the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
|
||||
IPv6Subnet *int `json:"ipv6Subnet,omitempty" toml:"ipv6Subnet,omitempty" yaml:"ipv6Subnet,omitempty"`
|
||||
// TODO(mpl): I think we should make RemoteAddr an explicit field. For one thing, it would yield better documentation.
|
||||
}
|
||||
|
||||
|
@ -418,8 +421,13 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
|
|||
}
|
||||
|
||||
if s.Depth > 0 {
|
||||
if s.IPv6Subnet != nil && (*s.IPv6Subnet <= 0 || *s.IPv6Subnet > 128) {
|
||||
return nil, fmt.Errorf("invalid IPv6 subnet %d value, should be greater to 0 and lower or equal to 128", *s.IPv6Subnet)
|
||||
}
|
||||
|
||||
return &ip.DepthStrategy{
|
||||
Depth: s.Depth,
|
||||
Depth: s.Depth,
|
||||
IPv6Subnet: s.IPv6Subnet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -433,7 +441,13 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
return &ip.RemoteAddrStrategy{}, nil
|
||||
if s.IPv6Subnet != nil && (*s.IPv6Subnet <= 0 || *s.IPv6Subnet > 128) {
|
||||
return nil, fmt.Errorf("invalid IPv6 subnet %d value, should be greater to 0 and lower or equal to 128", *s.IPv6Subnet)
|
||||
}
|
||||
|
||||
return &ip.RemoteAddrStrategy{
|
||||
IPv6Subnet: s.IPv6Subnet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
|
|
@ -704,6 +704,11 @@ func (in *IPStrategy) DeepCopyInto(out *IPStrategy) {
|
|||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.IPv6Subnet != nil {
|
||||
in, out := &in.IPv6Subnet, &out.IPv6Subnet
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -90,10 +90,12 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
"traefik.http.middlewares.Middleware8.headers.stsseconds": "42",
|
||||
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.depth": "42",
|
||||
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.excludedips": "foobar, fiibar",
|
||||
"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.ipv6subnet": "42",
|
||||
"traefik.http.middlewares.Middleware9.ipallowlist.sourcerange": "foobar, fiibar",
|
||||
"traefik.http.middlewares.Middleware10.inflightreq.amount": "42",
|
||||
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.depth": "42",
|
||||
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.excludedips": "foobar, fiibar",
|
||||
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.ipv6subnet": "42",
|
||||
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requestheadername": "foobar",
|
||||
"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requesthost": "true",
|
||||
"traefik.http.middlewares.Middleware11.passtlsclientcert.info.notafter": "true",
|
||||
|
@ -123,6 +125,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.requesthost": "true",
|
||||
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.depth": "42",
|
||||
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.excludedips": "foobar, foobar",
|
||||
"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.ipv6subnet": "42",
|
||||
"traefik.http.middlewares.Middleware13.redirectregex.permanent": "true",
|
||||
"traefik.http.middlewares.Middleware13.redirectregex.regex": "foobar",
|
||||
"traefik.http.middlewares.Middleware13.redirectregex.replacement": "foobar",
|
||||
|
@ -392,6 +395,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
IPStrategy: &dynamic.IPStrategy{
|
||||
Depth: 42,
|
||||
ExcludedIPs: []string{"foobar", "fiibar"},
|
||||
IPv6Subnet: intPtr(42),
|
||||
},
|
||||
RequestHeaderName: "foobar",
|
||||
RequestHost: true,
|
||||
|
@ -437,6 +441,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
IPStrategy: &dynamic.IPStrategy{
|
||||
Depth: 42,
|
||||
ExcludedIPs: []string{"foobar", "foobar"},
|
||||
IPv6Subnet: intPtr(42),
|
||||
},
|
||||
RequestHeaderName: "foobar",
|
||||
RequestHost: true,
|
||||
|
@ -648,6 +653,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
IPv6Subnet: intPtr(42),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -913,6 +919,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
IPStrategy: &dynamic.IPStrategy{
|
||||
Depth: 42,
|
||||
ExcludedIPs: []string{"foobar", "fiibar"},
|
||||
IPv6Subnet: intPtr(42),
|
||||
},
|
||||
RequestHeaderName: "foobar",
|
||||
RequestHost: true,
|
||||
|
@ -957,6 +964,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
IPStrategy: &dynamic.IPStrategy{
|
||||
Depth: 42,
|
||||
ExcludedIPs: []string{"foobar", "foobar"},
|
||||
IPv6Subnet: intPtr(42),
|
||||
},
|
||||
RequestHeaderName: "foobar",
|
||||
RequestHost: true,
|
||||
|
@ -1176,6 +1184,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
IPv6Subnet: intPtr(42),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1338,11 +1347,13 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.Depth": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.ExcludedIPs": "foobar, fiibar",
|
||||
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.IPv6Subnet": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.RejectStatusCode": "0",
|
||||
"traefik.HTTP.Middlewares.Middleware9.IPAllowList.SourceRange": "foobar, fiibar",
|
||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.Amount": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.Depth": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.ExcludedIPs": "foobar, fiibar",
|
||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.IPv6Subnet": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHeaderName": "foobar",
|
||||
"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHost": "true",
|
||||
"traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.NotAfter": "true",
|
||||
|
@ -1372,6 +1383,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.RequestHost": "true",
|
||||
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.Depth": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.ExcludedIPs": "foobar, foobar",
|
||||
"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.IPv6Subnet": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Regex": "foobar",
|
||||
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Replacement": "foobar",
|
||||
"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Permanent": "true",
|
||||
|
@ -1486,3 +1498,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
}
|
||||
assert.Equal(t, expected, labels)
|
||||
}
|
||||
|
||||
func intPtr(value int) *int {
|
||||
return &value
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package ip
|
|||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -16,7 +17,10 @@ type Strategy interface {
|
|||
}
|
||||
|
||||
// RemoteAddrStrategy a strategy that always return the remote address.
|
||||
type RemoteAddrStrategy struct{}
|
||||
type RemoteAddrStrategy struct {
|
||||
// IPv6Subnet instructs the strategy to return the first IP of the subnet where IP belongs.
|
||||
IPv6Subnet *int
|
||||
}
|
||||
|
||||
// GetIP returns the selected IP.
|
||||
func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
|
||||
|
@ -24,15 +28,22 @@ func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
|
|||
if err != nil {
|
||||
return req.RemoteAddr
|
||||
}
|
||||
|
||||
if s.IPv6Subnet != nil {
|
||||
return getIPv6SubnetIP(ip, *s.IPv6Subnet)
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
// DepthStrategy a strategy based on the depth inside the X-Forwarded-For from right to left.
|
||||
type DepthStrategy struct {
|
||||
Depth int
|
||||
// IPv6Subnet instructs the strategy to return the first IP of the subnet where IP belongs.
|
||||
IPv6Subnet *int
|
||||
}
|
||||
|
||||
// GetIP return the selected IP.
|
||||
// GetIP returns the selected IP.
|
||||
func (s *DepthStrategy) GetIP(req *http.Request) string {
|
||||
xff := req.Header.Get(xForwardedFor)
|
||||
xffs := strings.Split(xff, ",")
|
||||
|
@ -40,7 +51,14 @@ func (s *DepthStrategy) GetIP(req *http.Request) string {
|
|||
if len(xffs) < s.Depth {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(xffs[len(xffs)-s.Depth])
|
||||
|
||||
ip := strings.TrimSpace(xffs[len(xffs)-s.Depth])
|
||||
|
||||
if s.IPv6Subnet != nil {
|
||||
return getIPv6SubnetIP(ip, *s.IPv6Subnet)
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
// PoolStrategy is a strategy based on an IP Checker.
|
||||
|
@ -72,3 +90,23 @@ func (s *PoolStrategy) GetIP(req *http.Request) string {
|
|||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getIPv6SubnetIP returns the IPv6 subnet IP.
|
||||
// It returns the original IP when it is not an IPv6, or if parsing the IP has failed with an error.
|
||||
func getIPv6SubnetIP(ip string, ipv6Subnet int) string {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
if !addr.Is6() {
|
||||
return ip
|
||||
}
|
||||
|
||||
prefix, err := addr.Prefix(ipv6Subnet)
|
||||
if err != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
return prefix.Addr().String()
|
||||
}
|
||||
|
|
|
@ -9,23 +9,81 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv6Basic = "::abcd:ffff:c0a8:1"
|
||||
ipv6BracketsPort = "[::abcd:ffff:c0a8:1]:80"
|
||||
ipv6BracketsZonePort = "[::abcd:ffff:c0a8:1%1]:80"
|
||||
)
|
||||
|
||||
func TestRemoteAddrStrategy_GetIP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
expected string
|
||||
desc string
|
||||
expected string
|
||||
remoteAddr string
|
||||
ipv6Subnet *int
|
||||
}{
|
||||
// Valid IP format
|
||||
{
|
||||
desc: "Use RemoteAddr",
|
||||
desc: "Use RemoteAddr, ipv4",
|
||||
expected: "192.0.2.1",
|
||||
},
|
||||
{
|
||||
desc: "Use RemoteAddr, ipv6 brackets with port, no IPv6 subnet",
|
||||
remoteAddr: ipv6BracketsPort,
|
||||
expected: "::abcd:ffff:c0a8:1",
|
||||
},
|
||||
{
|
||||
desc: "Use RemoteAddr, ipv6 brackets with zone and port, no IPv6 subnet",
|
||||
remoteAddr: ipv6BracketsZonePort,
|
||||
expected: "::abcd:ffff:c0a8:1%1",
|
||||
},
|
||||
|
||||
// Invalid IPv6 format
|
||||
{
|
||||
desc: "Use RemoteAddr, ipv6 basic, missing brackets, no IPv6 subnet",
|
||||
remoteAddr: ipv6Basic,
|
||||
expected: ipv6Basic,
|
||||
},
|
||||
|
||||
// Valid IP format with subnet
|
||||
{
|
||||
desc: "Use RemoteAddr, ipv4, ignore subnet",
|
||||
expected: "192.0.2.1",
|
||||
ipv6Subnet: intPtr(24),
|
||||
},
|
||||
{
|
||||
desc: "Use RemoteAddr, ipv6 brackets with port, subnet",
|
||||
remoteAddr: ipv6BracketsPort,
|
||||
expected: "::abcd:0:0:0",
|
||||
ipv6Subnet: intPtr(80),
|
||||
},
|
||||
{
|
||||
desc: "Use RemoteAddr, ipv6 brackets with zone and port, subnet",
|
||||
remoteAddr: ipv6BracketsZonePort,
|
||||
expected: "::abcd:0:0:0",
|
||||
ipv6Subnet: intPtr(80),
|
||||
},
|
||||
|
||||
// Valid IP, invalid subnet
|
||||
{
|
||||
desc: "Use RemoteAddr, ipv6 brackets with port, invalid subnet",
|
||||
remoteAddr: ipv6BracketsPort,
|
||||
expected: "::abcd:ffff:c0a8:1",
|
||||
ipv6Subnet: intPtr(500),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
strategy := RemoteAddrStrategy{}
|
||||
strategy := RemoteAddrStrategy{
|
||||
IPv6Subnet: test.ipv6Subnet,
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
||||
if test.remoteAddr != "" {
|
||||
req.RemoteAddr = test.remoteAddr
|
||||
}
|
||||
actual := strategy.GetIP(req)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
|
@ -38,6 +96,7 @@ func TestDepthStrategy_GetIP(t *testing.T) {
|
|||
depth int
|
||||
xForwardedFor string
|
||||
expected string
|
||||
ipv6Subnet *int
|
||||
}{
|
||||
{
|
||||
desc: "Use depth",
|
||||
|
@ -57,13 +116,30 @@ func TestDepthStrategy_GetIP(t *testing.T) {
|
|||
xForwardedFor: "10.0.0.2,10.0.0.1",
|
||||
expected: "10.0.0.2",
|
||||
},
|
||||
{
|
||||
desc: "Use depth with IPv4 subnet",
|
||||
depth: 2,
|
||||
xForwardedFor: "10.0.0.3,10.0.0.2,10.0.0.1",
|
||||
expected: "10.0.0.2",
|
||||
ipv6Subnet: intPtr(80),
|
||||
},
|
||||
{
|
||||
desc: "Use depth with IPv6 subnet",
|
||||
depth: 2,
|
||||
xForwardedFor: "10.0.0.3," + ipv6Basic + ",10.0.0.1",
|
||||
expected: "::abcd:0:0:0",
|
||||
ipv6Subnet: intPtr(80),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
strategy := DepthStrategy{Depth: test.depth}
|
||||
strategy := DepthStrategy{
|
||||
Depth: test.depth,
|
||||
IPv6Subnet: test.ipv6Subnet,
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
||||
req.Header.Set(xForwardedFor, test.xForwardedFor)
|
||||
actual := strategy.GetIP(req)
|
||||
|
@ -121,3 +197,7 @@ func TestTrustedIPsStrategy_GetIP(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func intPtr(value int) *int {
|
||||
return &value
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue