Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add rate limit whitelist #1210

Merged
merged 1 commit into from
Aug 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion controllers/nginx/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,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.
Expand All @@ -221,7 +223,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)

Expand Down
30 changes: 23 additions & 7 deletions controllers/nginx/pkg/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ var (
"buildAuthLocation": buildAuthLocation,
"buildAuthResponseHeaders": buildAuthResponseHeaders,
"buildProxyPass": buildProxyPass,
"buildWhitelistVariable": buildWhitelistVariable,
"buildRateLimitZones": buildRateLimitZones,
"buildRateLimit": buildRateLimit,
"buildResolvers": buildResolvers,
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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)
Expand All @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
59 changes: 59 additions & 0 deletions core/pkg/ingress/annotations/ratelimit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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
}