doc: clarify usage for ratelimit's excludedIPs

This commit is contained in:
mpl 2021-06-07 17:46:14 +02:00 committed by GitHub
parent c10c7619d3
commit 2560626419
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 62 additions and 28 deletions

View file

@ -325,18 +325,45 @@ http:
##### `ipStrategy.excludedIPs` ##### `ipStrategy.excludedIPs`
`excludedIPs` configures Traefik to scan the `X-Forwarded-For` header and select the first IP not in the list. !!! important "Contrary to what the name might suggest, this option is _not_ about excluding an IP from the rate limiter, and therefore cannot be used to deactivate rate limiting for some IPs."
!!! important "If `depth` is specified, `excludedIPs` is ignored." !!! important "If `depth` is specified, `excludedIPs` is ignored."
!!! example "Example of ExcludedIPs & X-Forwarded-For" `excludedIPs` is meant to address two classes of somewhat distinct use-cases:
| `X-Forwarded-For` | `excludedIPs` | clientIP | 1. Distinguish IPs which are behind the same (set of) reverse-proxies so that each of them contributes, independently to the others,
|-----------------------------------------|-----------------------|--------------| to its own rate-limit "bucket" (cf the [leaky bucket analogy](https://wikipedia.org/wiki/Leaky_bucket)).
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"12.0.0.1,13.0.0.1"` | `"11.0.0.1"` | In this case, `excludedIPs` should be set to match the list of `X-Forwarded-For IPs` that are to be excluded,
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,13.0.0.1"` | `"12.0.0.1"` | in order to find the actual clientIP.
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"10.0.0.1,13.0.0.1"` | `"12.0.0.1"` |
| `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` | !!! example "Each IP as a distinct source"
| X-Forwarded-For | excludedIPs | clientIP |
|--------------------------------|-----------------------|--------------|
| `"10.0.0.1,11.0.0.1,12.0.0.1"` | `"11.0.0.1,12.0.0.1"` | `"10.0.0.1"` |
| `"10.0.0.2,11.0.0.1,12.0.0.1"` | `"11.0.0.1,12.0.0.1"` | `"10.0.0.2"` |
2. Group together a set of IPs (also behind a common set of reverse-proxies) so that they are considered the same source,
and all contribute to the same rate-limit bucket.
!!! example "Group IPs together as same source"
| X-Forwarded-For | excludedIPs | clientIP |
|--------------------------------|--------------|--------------|
| `"10.0.0.1,11.0.0.1,12.0.0.1"` | `"12.0.0.1"` | `"11.0.0.1"` |
| `"10.0.0.2,11.0.0.1,12.0.0.1"` | `"12.0.0.1"` | `"11.0.0.1"` |
| `"10.0.0.3,11.0.0.1,12.0.0.1"` | `"12.0.0.1"` | `"11.0.0.1"` |
For completeness, below are additional examples to illustrate how the matching works.
For a given request the list of `X-Forwarded-For` IPs is checked from most recent to most distant against the `excludedIPs` pool,
and the first IP that is _not_ in the pool (if any) is returned.
!!! example "Matching for clientIP"
| X-Forwarded-For | excludedIPs | clientIP |
|--------------------------------|-----------------------|--------------|
| `"10.0.0.1,11.0.0.1,13.0.0.1"` | `"11.0.0.1"` | `"13.0.0.1"` |
| `"10.0.0.1,11.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` |
| `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` | | `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` |
```yaml tab="Docker" ```yaml tab="Docker"

View file

@ -241,7 +241,7 @@ type IPStrategy struct {
// Get an IP selection strategy. // Get an IP selection strategy.
// If nil return the RemoteAddr strategy // If nil return the RemoteAddr strategy
// else return a strategy base on the configuration using the X-Forwarded-For Header. // else return a strategy based on the configuration using the X-Forwarded-For Header.
// Depth override the ExcludedIPs. // Depth override the ExcludedIPs.
func (s *IPStrategy) Get() (ip.Strategy, error) { func (s *IPStrategy) Get() (ip.Strategy, error) {
if s == nil { if s == nil {
@ -259,7 +259,7 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ip.CheckerStrategy{ return &ip.PoolStrategy{
Checker: checker, Checker: checker,
}, nil }, nil
} }

View file

@ -43,14 +43,16 @@ func (s *DepthStrategy) GetIP(req *http.Request) string {
return strings.TrimSpace(xffs[len(xffs)-s.Depth]) return strings.TrimSpace(xffs[len(xffs)-s.Depth])
} }
// CheckerStrategy a strategy based on an IP Checker // PoolStrategy is a strategy based on an IP Checker.
// allows to check that addresses are in a trusted IPs. // It allows to check whether addresses are in a given pool of IPs.
type CheckerStrategy struct { type PoolStrategy struct {
Checker *Checker Checker *Checker
} }
// GetIP return the selected IP. // GetIP checks the list of Forwarded IPs (most recent first) against the
func (s *CheckerStrategy) GetIP(req *http.Request) string { // Checker pool of IPs. It returns the first IP that is not in the pool, or the
// empty string otherwise.
func (s *PoolStrategy) GetIP(req *http.Request) string {
if s.Checker == nil { if s.Checker == nil {
return "" return ""
} }
@ -60,9 +62,13 @@ func (s *CheckerStrategy) GetIP(req *http.Request) string {
for i := len(xffs) - 1; i >= 0; i-- { for i := len(xffs) - 1; i >= 0; i-- {
xffTrimmed := strings.TrimSpace(xffs[i]) xffTrimmed := strings.TrimSpace(xffs[i])
if len(xffTrimmed) == 0 {
continue
}
if contain, _ := s.Checker.Contains(xffTrimmed); !contain { if contain, _ := s.Checker.Contains(xffTrimmed); !contain {
return xffTrimmed return xffTrimmed
} }
} }
return "" return ""
} }

View file

@ -74,34 +74,35 @@ func TestDepthStrategy_GetIP(t *testing.T) {
} }
} }
func TestExcludedIPsStrategy_GetIP(t *testing.T) { func TestTrustedIPsStrategy_GetIP(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
excludedIPs []string trustedIPs []string
xForwardedFor string xForwardedFor string
expected string expected string
useRemote bool
}{ }{
{ {
desc: "Use excluded all IPs", desc: "Trust all IPs",
excludedIPs: []string{"10.0.0.4", "10.0.0.3", "10.0.0.2", "10.0.0.1"}, trustedIPs: []string{"10.0.0.4", "10.0.0.3", "10.0.0.2", "10.0.0.1"},
xForwardedFor: "10.0.0.4,10.0.0.3,10.0.0.2,10.0.0.1", xForwardedFor: "10.0.0.4,10.0.0.3,10.0.0.2,10.0.0.1",
expected: "", expected: "",
}, },
{ {
desc: "Use excluded IPs", desc: "Do not trust all IPs",
excludedIPs: []string{"10.0.0.2", "10.0.0.1"}, trustedIPs: []string{"10.0.0.2", "10.0.0.1"},
xForwardedFor: "10.0.0.4,10.0.0.3,10.0.0.2,10.0.0.1", xForwardedFor: "10.0.0.4,10.0.0.3,10.0.0.2,10.0.0.1",
expected: "10.0.0.3", expected: "10.0.0.3",
}, },
{ {
desc: "Use excluded IPs CIDR", desc: "Do not trust all IPs with CIDR",
excludedIPs: []string{"10.0.0.1/24"}, trustedIPs: []string{"10.0.0.1/24"},
xForwardedFor: "127.0.0.1,10.0.0.4,10.0.0.3,10.0.0.2,10.0.0.1", xForwardedFor: "127.0.0.1,10.0.0.4,10.0.0.3,10.0.0.2,10.0.0.1",
expected: "127.0.0.1", expected: "127.0.0.1",
}, },
{ {
desc: "Use excluded all IPs CIDR", desc: "Trust all IPs with CIDR",
excludedIPs: []string{"10.0.0.1/24"}, trustedIPs: []string{"10.0.0.1/24"},
xForwardedFor: "10.0.0.4,10.0.0.3,10.0.0.2,10.0.0.1", xForwardedFor: "10.0.0.4,10.0.0.3,10.0.0.2,10.0.0.1",
expected: "", expected: "",
}, },
@ -112,10 +113,10 @@ func TestExcludedIPsStrategy_GetIP(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
checker, err := NewChecker(test.excludedIPs) checker, err := NewChecker(test.trustedIPs)
require.NoError(t, err) require.NoError(t, err)
strategy := CheckerStrategy{Checker: checker} strategy := PoolStrategy{Checker: checker}
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil) req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
req.Header.Set(xForwardedFor, test.xForwardedFor) req.Header.Set(xForwardedFor, test.xForwardedFor)
actual := strategy.GetIP(req) actual := strategy.GetIP(req)