Merge pull request #1210 from sethpollack/whitelist
add rate limit whitelist
This commit is contained in:
commit
def5155aa6
4 changed files with 98 additions and 9 deletions
|
@ -225,6 +225,8 @@ The annotations `ingress.kubernetes.io/limit-connections`, `ingress.kubernetes.i
|
||||||
|
|
||||||
`ingress.kubernetes.io/limit-rpm`: number of connections that may be accepted from a given IP each minute.
|
`ingress.kubernetes.io/limit-rpm`: number of connections that may be accepted from a given IP each minute.
|
||||||
|
|
||||||
|
You can specify the client IP source ranges to be excluded from rate-limiting through the `ingress.kubernetes.io/limit-whitelist` annotation. The value is a comma separated list of CIDRs.
|
||||||
|
|
||||||
If you specify multiple annotations in a single Ingress rule, `limit-rpm`, and then `limit-rps` takes precedence.
|
If you specify multiple annotations in a single Ingress rule, `limit-rpm`, and then `limit-rps` takes precedence.
|
||||||
|
|
||||||
The annotation `ingress.kubernetes.io/limit-rate`, `ingress.kubernetes.io/limit-rate-after` define a limit the rate of response transmission to a client. The rate is specified in bytes per second. The zero value disables rate limiting. The limit is set per a request, and so if a client simultaneously opens two connections, the overall rate will be twice as much as the specified limit.
|
The annotation `ingress.kubernetes.io/limit-rate`, `ingress.kubernetes.io/limit-rate-after` define a limit the rate of response transmission to a client. The rate is specified in bytes per second. The zero value disables rate limiting. The limit is set per a request, and so if a client simultaneously opens two connections, the overall rate will be twice as much as the specified limit.
|
||||||
|
|
|
@ -132,6 +132,7 @@ var (
|
||||||
"buildAuthLocation": buildAuthLocation,
|
"buildAuthLocation": buildAuthLocation,
|
||||||
"buildAuthResponseHeaders": buildAuthResponseHeaders,
|
"buildAuthResponseHeaders": buildAuthResponseHeaders,
|
||||||
"buildProxyPass": buildProxyPass,
|
"buildProxyPass": buildProxyPass,
|
||||||
|
"buildWhitelistVariable": buildWhitelistVariable,
|
||||||
"buildRateLimitZones": buildRateLimitZones,
|
"buildRateLimitZones": buildRateLimitZones,
|
||||||
"buildRateLimit": buildRateLimit,
|
"buildRateLimit": buildRateLimit,
|
||||||
"buildResolvers": buildResolvers,
|
"buildResolvers": buildResolvers,
|
||||||
|
@ -335,10 +336,23 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string {
|
||||||
return defProxyPass
|
return defProxyPass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
whitelistVarMap = map[string]string{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildWhitelistVariable(s string) string {
|
||||||
|
if _, ok := whitelistVarMap[s]; !ok {
|
||||||
|
str := base64.URLEncoding.EncodeToString([]byte(s))
|
||||||
|
whitelistVarMap[s] = strings.Replace(str, "=", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return whitelistVarMap[s]
|
||||||
|
}
|
||||||
|
|
||||||
// buildRateLimitZones produces an array of limit_conn_zone in order to allow
|
// buildRateLimitZones produces an array of limit_conn_zone in order to allow
|
||||||
// rate limiting of request. Each Ingress rule could have up to two zones, one
|
// rate limiting of request. Each Ingress rule could have up to two zones, one
|
||||||
// for connection limit by IP address and other for limiting request per second
|
// for connection limit by IP address and other for limiting request per second
|
||||||
func buildRateLimitZones(variable string, input interface{}) []string {
|
func buildRateLimitZones(input interface{}) []string {
|
||||||
zones := sets.String{}
|
zones := sets.String{}
|
||||||
|
|
||||||
servers, ok := input.([]*ingress.Server)
|
servers, ok := input.([]*ingress.Server)
|
||||||
|
@ -349,9 +363,11 @@ func buildRateLimitZones(variable string, input interface{}) []string {
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
for _, loc := range server.Locations {
|
for _, loc := range server.Locations {
|
||||||
|
|
||||||
|
whitelistVar := buildWhitelistVariable(loc.RateLimit.Name)
|
||||||
|
|
||||||
if loc.RateLimit.Connections.Limit > 0 {
|
if loc.RateLimit.Connections.Limit > 0 {
|
||||||
zone := fmt.Sprintf("limit_conn_zone %v zone=%v:%vm;",
|
zone := fmt.Sprintf("limit_conn_zone $%s_limit zone=%v:%vm;",
|
||||||
variable,
|
whitelistVar,
|
||||||
loc.RateLimit.Connections.Name,
|
loc.RateLimit.Connections.Name,
|
||||||
loc.RateLimit.Connections.SharedSize)
|
loc.RateLimit.Connections.SharedSize)
|
||||||
if !zones.Has(zone) {
|
if !zones.Has(zone) {
|
||||||
|
@ -360,8 +376,8 @@ func buildRateLimitZones(variable string, input interface{}) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if loc.RateLimit.RPM.Limit > 0 {
|
if loc.RateLimit.RPM.Limit > 0 {
|
||||||
zone := fmt.Sprintf("limit_req_zone %v zone=%v:%vm rate=%vr/m;",
|
zone := fmt.Sprintf("limit_req_zone $%s_limit zone=%v:%vm rate=%vr/m;",
|
||||||
variable,
|
whitelistVar,
|
||||||
loc.RateLimit.RPM.Name,
|
loc.RateLimit.RPM.Name,
|
||||||
loc.RateLimit.RPM.SharedSize,
|
loc.RateLimit.RPM.SharedSize,
|
||||||
loc.RateLimit.RPM.Limit)
|
loc.RateLimit.RPM.Limit)
|
||||||
|
@ -371,8 +387,8 @@ func buildRateLimitZones(variable string, input interface{}) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if loc.RateLimit.RPS.Limit > 0 {
|
if loc.RateLimit.RPS.Limit > 0 {
|
||||||
zone := fmt.Sprintf("limit_req_zone %v zone=%v:%vm rate=%vr/s;",
|
zone := fmt.Sprintf("limit_req_zone $%s_limit zone=%v:%vm rate=%vr/s;",
|
||||||
variable,
|
whitelistVar,
|
||||||
loc.RateLimit.RPS.Name,
|
loc.RateLimit.RPS.Name,
|
||||||
loc.RateLimit.RPS.SharedSize,
|
loc.RateLimit.RPS.SharedSize,
|
||||||
loc.RateLimit.RPS.Limit)
|
loc.RateLimit.RPS.Limit)
|
||||||
|
|
|
@ -288,12 +288,24 @@ http {
|
||||||
}
|
}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if ne $location.RateLimit.Name "" }}
|
||||||
|
geo ${{ buildWhitelistVariable $location.RateLimit.Name }}_whitelist {
|
||||||
|
default 0;
|
||||||
|
{{ range $ip := $location.RateLimit.Whitelist }}
|
||||||
|
{{ $ip }} 1;{{ end }}
|
||||||
|
}
|
||||||
|
|
||||||
|
map ${{ buildWhitelistVariable $location.RateLimit.Name }}_whitelist ${{ buildWhitelistVariable $location.RateLimit.Name }}_limit {
|
||||||
|
0 {{ $cfg.LimitConnZoneVariable }};
|
||||||
|
1 "";
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{/* build all the required rate limit zones. Each annotation requires a dedicated zone */}}
|
{{/* build all the required rate limit zones. Each annotation requires a dedicated zone */}}
|
||||||
{{/* 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states */}}
|
{{/* 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states */}}
|
||||||
{{ range $zone := (buildRateLimitZones $cfg.LimitConnZoneVariable $servers) }}
|
{{ range $zone := (buildRateLimitZones $servers) }}
|
||||||
{{ $zone }}
|
{{ $zone }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,14 @@ package ratelimit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
extensions "k8s.io/api/extensions/v1beta1"
|
extensions "k8s.io/api/extensions/v1beta1"
|
||||||
|
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
|
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
|
||||||
"k8s.io/ingress/core/pkg/ingress/resolver"
|
"k8s.io/ingress/core/pkg/ingress/resolver"
|
||||||
|
"k8s.io/ingress/core/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -31,6 +34,7 @@ const (
|
||||||
limitRPM = "ingress.kubernetes.io/limit-rpm"
|
limitRPM = "ingress.kubernetes.io/limit-rpm"
|
||||||
limitRATE = "ingress.kubernetes.io/limit-rate"
|
limitRATE = "ingress.kubernetes.io/limit-rate"
|
||||||
limitRATEAFTER = "ingress.kubernetes.io/limit-rate-after"
|
limitRATEAFTER = "ingress.kubernetes.io/limit-rate-after"
|
||||||
|
limitWhitelist = "ingress.kubernetes.io/limit-whitelist"
|
||||||
|
|
||||||
// allow 5 times the specified limit as burst
|
// allow 5 times the specified limit as burst
|
||||||
defBurst = 5
|
defBurst = 5
|
||||||
|
@ -55,6 +59,10 @@ type RateLimit struct {
|
||||||
LimitRate int `json:"limit-rate"`
|
LimitRate int `json:"limit-rate"`
|
||||||
|
|
||||||
LimitRateAfter int `json:"limit-rate-after"`
|
LimitRateAfter int `json:"limit-rate-after"`
|
||||||
|
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
Whitelist []string `json:"whitelist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equal tests for equality between two RateLimit types
|
// Equal tests for equality between two RateLimit types
|
||||||
|
@ -80,6 +88,22 @@ func (rt1 *RateLimit) Equal(rt2 *RateLimit) bool {
|
||||||
if rt1.LimitRateAfter != rt2.LimitRateAfter {
|
if rt1.LimitRateAfter != rt2.LimitRateAfter {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if rt1.Name != rt2.Name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r1l := range rt1.Whitelist {
|
||||||
|
found := false
|
||||||
|
for _, rl2 := range rt2.Whitelist {
|
||||||
|
if r1l == rl2 {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -144,6 +168,13 @@ func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||||
rps, _ := parser.GetIntAnnotation(limitRPS, ing)
|
rps, _ := parser.GetIntAnnotation(limitRPS, ing)
|
||||||
conn, _ := parser.GetIntAnnotation(limitIP, ing)
|
conn, _ := parser.GetIntAnnotation(limitIP, ing)
|
||||||
|
|
||||||
|
val, _ := parser.GetStringAnnotation(limitWhitelist, ing)
|
||||||
|
|
||||||
|
cidrs, err := parseCIDRs(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if rpm == 0 && rps == 0 && conn == 0 {
|
if rpm == 0 && rps == 0 && conn == 0 {
|
||||||
return &RateLimit{
|
return &RateLimit{
|
||||||
Connections: Zone{},
|
Connections: Zone{},
|
||||||
|
@ -177,5 +208,33 @@ func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||||
},
|
},
|
||||||
LimitRate: lr,
|
LimitRate: lr,
|
||||||
LimitRateAfter: lra,
|
LimitRateAfter: lra,
|
||||||
|
Name: zoneName,
|
||||||
|
Whitelist: cidrs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseCIDRs(s string) ([]string, error) {
|
||||||
|
if s == "" {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
values := strings.Split(s, ",")
|
||||||
|
|
||||||
|
ipnets, ips, err := net.ParseIPNets(values...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cidrs := []string{}
|
||||||
|
for k := range ipnets {
|
||||||
|
cidrs = append(cidrs, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range ips {
|
||||||
|
cidrs = append(cidrs, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(cidrs)
|
||||||
|
|
||||||
|
return cidrs, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue