Ограничение скорости HTTP-запросов (через промежуточное программное обеспечение http.HandlerFunc)

Я хочу написать небольшую часть промежуточного программного обеспечения для ограничения скорости, которое:

  • Позволяет установить разумную скорость (например, 10 req/s) на один удаленный IP
  • Возможно (но это не обязательно) допускать всплески
  • Удаляет (закрывает?) соединения, которые превышают скорость и возвращает HTTP 429

Затем я могу обернуть это вокруг маршрутов аутентификации или других маршрутов, которые могут быть уязвимы для атак с грубой силой (т.е. пароля reset с использованием токена, который истекает и т.д.). Шансы на то, что кто-то грубо забивает маркер 16 или 24 байта, действительно низки, но это не помешает сделать этот дополнительный шаг.

Я посмотрел https://code.google.com/p/go-wiki/wiki/RateLimiting, но не уверен, как смириться с http.Request(s). Кроме того, я не уверен, как мы будем "отслеживать" запросы от определенного IP-адреса в течение любого периода времени.

В идеале я получаю что-то вроде этого, отмечая, что я за обратным прокси (nginx), поэтому мы проверяем HTTP-заголовок REMOTE_ADDR вместо использования r.RemoteAddr:

// Rate-limiting middleware
func rateLimit(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {

        remoteIP := r.Header.Get("REMOTE_ADDR")
        for req := range (what here?) {
            // what here?
            // w.WriteHeader(429) and close the request if it exceeds the limit
            // else pass to the next handler in the chain
            h.ServeHTTP(w, r)
        }
}

// Example routes
r.HandleFunc("/login", use(loginForm, rateLimit, csrf)
r.HandleFunc("/form", use(editHandler, rateLimit, csrf)

// Middleware wrapper, for context
func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
    for _, m := range middleware {
        h = m(h)
    }

    return h
}

Я был бы признателен за некоторые рекомендации здесь.

Ответ 1

Пример ограничения скорости, с которым вы связались, является общим. Он использует диапазон, потому что он получает запросы по каналу.

Это другая история с HTTP-запросами, но здесь нет ничего сложного. Обратите внимание, что вы не перебираете канал запросов или что-либо - ваш HandlerFunc вызывается для каждого входящего запроса отдельно.

func rateLimit(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        remoteIP := r.Header.Get("REMOTE_ADDR")
        if exceededTheLimit(remoteIP) {
            w.WriteHeader(429)
            // it then returns, not passing the request down the chain
        } else {
            h.ServeHTTP(w, r);
        }
    }       
}

Теперь выбор места для хранения счетчиков ограничения скорости зависит от вас. Одним из решений было бы просто использовать глобальную карту (не забудьте безопасный одновременный доступ), которая будет сопоставлять IP-адреса с их счетчиками запросов. Однако вам нужно будет знать, как давно были сделаны запросы.

Серджио предложил использовать Redis. Его ключевая ценность идеально подходит для простых структур, подобных этому, и вы получаете бесплатное освобождение.

Ответ 2

Вы можете сохранить данные в redis. Здесь очень полезная команда, которая даже упоминает приложение ограничения скорости в своей документации: INCR. Redis также обработает очистку старых данных (по истечении срока действия старых ключей).

Кроме того, если redis является хранилищем ограничителей скорости, вы можете использовать несколько интерфейсных процессов, которые совместно используют это центральное хранилище.

Некоторые утверждают, что переход к внешнему процессу каждый раз дорого. Но страница с паролем reset - это не та страница, которая абсолютно требует лучшей производительности. Кроме того, если вы поместите redis на один и тот же компьютер, латентность должна быть довольно низкой.

Ответ 3

Сегодня утром я сделал что-то простое и похожее, я думаю, это может помочь вашему делу.

package main

import (
    "log"
    "net/http"
    "strings"
    "time"
)

func main() {
    fs := http.FileServer(http.Dir("./html/"))
    http.Handle("/", fs)
    log.Println("Listening..")
    go clearLastRequestsIPs()
    go clearBlockedIPs()
    err := http.ListenAndServe(":8080", middleware(nil))
    if err != nil {
        log.Fatalln(err)
    }
}

// Stores last requests IPs
var lastRequestsIPs []string

// Block IP for 6 hours
var blockedIPs []string

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ipAddr := strings.Split(r.RemoteAddr, ":")[0]
        if existsBlockedIP(ipAddr) {
            http.Error(w, "", http.StatusTooManyRequests)
            return
        }
        // how many requests the current IP made in last 5 mins
        requestCounter := 0
        for _, ip := range lastRequestsIPs {
            if ip == ipAddr {
                requestCounter++
            }
        }
        if requestCounter >= 1000 {
            blockedIPs = append(blockedIPs, ipAddr)
            http.Error(w, "", http.StatusTooManyRequests)
            return
        }
        lastRequestsIPs = append(lastRequestsIPs, ipAddr)

        // Don't cut the chain of middlewares
        if next == nil {
            http.DefaultServeMux.ServeHTTP(w, r)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func existsBlockedIP(ipAddr string) bool {
    for _, ip := range blockedIPs {
        if ip == ipAddr {
            return true
        }
    }
    return false
}

func existsLastRequest(ipAddr string) bool {
    for _, ip := range lastRequestsIPs {
        if ip == ipAddr {
            return true
        }
    }
    return false
}

// Clears lastRequestsIPs array every 5 mins
func clearLastRequestsIPs() {
    for {
        lastRequestsIPs = []string{}
        time.Sleep(time.Minute * 5)
    }
}

// Clears blockedIPs array every 6 hours
func clearBlockedIPs() {
    for {
        blockedIPs = []string{}
        time.Sleep(time.Hour * 6)
    }
}
Тем не менее, он все еще не является точным, однако это поможет в качестве простого примера ограничения скорости. вы можете улучшить его, добавив запрашиваемый путь, метод http и даже проверку подлинности в качестве факторов, чтобы решить, является ли поток атакой или нет.