Merge pull request #1210 from sethpollack/whitelist

add rate limit whitelist
This commit is contained in:
Manuel Alejandro de Brito Fontes 2017-08-22 08:23:45 -04:00 committed by GitHub
commit def5155aa6
4 changed files with 98 additions and 9 deletions

View file

@ -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.
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.
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.
@ -239,7 +241,7 @@ To configure this setting globally for all Ingress rules, the `limit-rate-after`
The annotation `ingress.kubernetes.io/ssl-passthrough` allows to configure TLS termination in the pod and not in NGINX.
**Important:**
**Important:**
- Using the annotation `ingress.kubernetes.io/ssl-passthrough` invalidates all the other available annotations. This is because SSL Passthrough works in L4 (TCP).
- The use of this annotation requires the flag `--enable-ssl-passthrough` (By default it is disabled)

View file

@ -132,6 +132,7 @@ var (
"buildAuthLocation": buildAuthLocation,
"buildAuthResponseHeaders": buildAuthResponseHeaders,
"buildProxyPass": buildProxyPass,
"buildWhitelistVariable": buildWhitelistVariable,
"buildRateLimitZones": buildRateLimitZones,
"buildRateLimit": buildRateLimit,
"buildResolvers": buildResolvers,
@ -335,10 +336,23 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string {
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
// 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
func buildRateLimitZones(variable string, input interface{}) []string {
func buildRateLimitZones(input interface{}) []string {
zones := sets.String{}
servers, ok := input.([]*ingress.Server)
@ -349,9 +363,11 @@ func buildRateLimitZones(variable string, input interface{}) []string {
for _, server := range servers {
for _, loc := range server.Locations {
whitelistVar := buildWhitelistVariable(loc.RateLimit.Name)
if loc.RateLimit.Connections.Limit > 0 {
zone := fmt.Sprintf("limit_conn_zone %v zone=%v:%vm;",
variable,
zone := fmt.Sprintf("limit_conn_zone $%s_limit zone=%v:%vm;",
whitelistVar,
loc.RateLimit.Connections.Name,
loc.RateLimit.Connections.SharedSize)
if !zones.Has(zone) {
@ -360,8 +376,8 @@ func buildRateLimitZones(variable string, input interface{}) []string {
}
if loc.RateLimit.RPM.Limit > 0 {
zone := fmt.Sprintf("limit_req_zone %v zone=%v:%vm rate=%vr/m;",
variable,
zone := fmt.Sprintf("limit_req_zone $%s_limit zone=%v:%vm rate=%vr/m;",
whitelistVar,
loc.RateLimit.RPM.Name,
loc.RateLimit.RPM.SharedSize,
loc.RateLimit.RPM.Limit)
@ -371,8 +387,8 @@ func buildRateLimitZones(variable string, input interface{}) []string {
}
if loc.RateLimit.RPS.Limit > 0 {
zone := fmt.Sprintf("limit_req_zone %v zone=%v:%vm rate=%vr/s;",
variable,
zone := fmt.Sprintf("limit_req_zone $%s_limit zone=%v:%vm rate=%vr/s;",
whitelistVar,
loc.RateLimit.RPS.Name,
loc.RateLimit.RPS.SharedSize,
loc.RateLimit.RPS.Limit)

View file

@ -288,12 +288,24 @@ http {
}
{{ 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 }}
{{/* 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 */}}
{{ range $zone := (buildRateLimitZones $cfg.LimitConnZoneVariable $servers) }}
{{ range $zone := (buildRateLimitZones $servers) }}
{{ $zone }}
{{ end }}

View file

@ -18,11 +18,14 @@ package ratelimit
import (
"fmt"
"sort"
"strings"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/resolver"
"k8s.io/ingress/core/pkg/net"
)
const (
@ -31,6 +34,7 @@ const (
limitRPM = "ingress.kubernetes.io/limit-rpm"
limitRATE = "ingress.kubernetes.io/limit-rate"
limitRATEAFTER = "ingress.kubernetes.io/limit-rate-after"
limitWhitelist = "ingress.kubernetes.io/limit-whitelist"
// allow 5 times the specified limit as burst
defBurst = 5
@ -55,6 +59,10 @@ type RateLimit struct {
LimitRate int `json:"limit-rate"`
LimitRateAfter int `json:"limit-rate-after"`
Name string `json:"name"`
Whitelist []string `json:"whitelist"`
}
// Equal tests for equality between two RateLimit types
@ -80,6 +88,22 @@ func (rt1 *RateLimit) Equal(rt2 *RateLimit) bool {
if rt1.LimitRateAfter != rt2.LimitRateAfter {
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
}
@ -144,6 +168,13 @@ func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) {
rps, _ := parser.GetIntAnnotation(limitRPS, 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 {
return &RateLimit{
Connections: Zone{},
@ -177,5 +208,33 @@ func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) {
},
LimitRate: lr,
LimitRateAfter: lra,
Name: zoneName,
Whitelist: cidrs,
}, 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
}