33. DDoS, Rate Limiting и базовая защита сетевых сервисов
Введение
В компьютерной безопасности существует фундаментальный закон асимметричной войны: лучшая линия обороны — это та, которая заставляет атакующего тратить в десятки раз больше ресурсов, чем защитника.
Сетевой сервис в Go, несмотря на высокую производительность и эффективную модель конкурентности, не застрахован от атак на доступность. Скорость рантайма Go часто делает его более привлекательной мишенью для злоумышленников: атака может исчерпать ресурсы приложения быстрее, чем в медленных интерпретируемых языках. Если Go-сервер способен мгновенно порождать десятки тысяч горутин, атакующий может использовать эту силу против самого сервиса. Защита бэкенда строится не на одном инструменте, а на многослойном подходе, где каждый уровень компенсирует недостатки предыдущего.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph MultiLayerDefense ["Многослойная эшелонированная оборона"]
L3L4["L3/L4 Защита: Anycast, Cloudflare, SYN Cookies, iptables/nftables"]:::process
L7Edge["L7 Прокси / WAF: Envoy, Nginx, фильтрация заголовков, TLS"]:::network
DistLimit["Распределенный Rate Limiter: Redis + Lua Scripts"]:::data
GoRuntime["Go Runtime: http.Server таймауты, MaxBytesReader, worker pools"]:::network
AppLogic["Бизнес-логика: капчи, circuit breakers, деградация"]:::process
L3L4 --> L7Edge --> DistLimit --> GoRuntime --> AppLogic
end
Уровни DDoS-атак и векторы поражения
Атаки на отказ в обслуживании (DDoS) классифицируются по уровню сетевой модели, на котором они наносят максимальный ущерб.
L3-L4 (Сетевой и Транспортный уровни)
Включают volumetric-атаки (завал трафика) и protocol-атаки (SYN flood, UDP amplification). На этом уровне нагрузка ложится на сетевую карту ОС и стек TCP/IP. В Linux для защиты от SYN-флуда используются SYN cookies и настройка net.ipv4.tcp_syncookies.
В классическом режиме ядро выделяет память под запись полуоткрытого соединения в структуре request_sock. При SYN-флуде очередь syn_backlog переполняется за миллисекунды. При включенных SYN cookies ядро вообще не выделяет память в момент прихода SYN: оно кодирует состояние соединения в начальный порядковый номер (Initial Sequence Number, ISN) с помощью криптографического хэша от 4-tuple и секрета ядра. Если клиент легитимен, он вернет этот номер в ACK, и только тогда ядро создаст сокет. Если очередь syn_backlog переполняется, ядро начинает отбрасывать новые соединения без выделения ресурсов под полные TCP-структуры.
L7 (Прикладной уровень)
Наиболее опасны для Go-сервисов. Злоумышленники имитируют легитимный трафик: отправляют тяжелые JSON-пейлоады, запускают долгие запросы (Slowloris), или генерируют тысячи уникальных запросов, которые вынуждают сервис обращаться к БД или кэшу. В отличие от PHP, где каждый запрос изолирован в отдельном процессе и умирает после завершения, Go держит горутины и объекты в памяти до явного завершения. При атаке L7 это приводит к быстрому исчерпанию памяти (OOM) или файлового дескриптора.
При атаке Slowloris атакующий медленно отправляет HTTP-заголовки (по 1 байту каждые 10 секунд). Сервер не закрывает соединение, так как клиент формально передает данные. В результате тысячи сокетов висят открытыми, блокируя дескрипторы и пулы соединений.
Rate Limiting: Алгоритмы и реализация под капотом
Rate Limiting (ограничение скорости) — механизм контроля частоты выполнения операций. В Go его реализация требует понимания компромисса между точностью, производительностью и распределённостью.
Алгоритмы
- Token Bucket (Корзина токенов): Позволяет делать burst (всплески) запросов. Токены добавляются в корзину с фиксированной скоростью до максимальной емкости. Приходящий запрос забирает токен. Если очередь пуста — запрос отклоняется. Идеален для HTTP API, так как прощает естественные кратковременные всплески активности пользователей.
- Sliding Window Counter: Делит время на интервалы и считает запросы. Проще в реализации, но даёт ложные срабатывания на границах окон.
- Sliding Window Log: Хранит timestamp каждого запроса. Максимально точен, но требует
O(N)памяти и медленнее работает при высоких нагрузках.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph TokenBucket ["Алгоритм Token Bucket"]
Refill["Пополнение: R токенов/сек"]:::data --> Bucket["Корзина: Емкость B токенов"]
Req["Входящий запрос"]:::entry --> Check{"Есть токен?"}
Bucket -.-> Check
Check -- "Да: -1 токен" --> Process["Обработка запроса (200 OK)"]
Check -- "Нет токенов" --> Reject["429 Too Many Requests"]
end
Идиоматичная реализация в Go
Для production-систем рекомендуется использовать пакет golang.org/x/time/rate или кастомные реализации на atomic для снижения давления на sync.Mutex. Ниже пример высокопроизводительного лимитера с поддержкой burst:
package ratelimit
import (
"sync"
"time"
"golang.org/x/time/rate"
)
type ipEntry struct {
lim *rate.Limiter
lastSeen time.Time
}
// Limiter объединяет глобальный лимитер и лимитеры по IP.
type Limiter struct {
global *rate.Limiter
perIPLimit rate.Limit
perIPBurst int
mu sync.Mutex
limiters map[string]*ipEntry
}
// NewLimiter создает глобальный и per-IP лимитеры.
func NewLimiter(globalLimit rate.Limit, globalBurst int, perIPLimit rate.Limit, perIPBurst int) *Limiter {
return &Limiter{
global: rate.NewLimiter(globalLimit, globalBurst),
perIPLimit: perIPLimit,
perIPBurst: perIPBurst,
limiters: make(map[string]*ipEntry),
}
}
// Allow проверяет доступность токенов. Возвращает false, если лимит исчерпан.
func (rl *Limiter) Allow(ip string) bool {
// Сначала per-IP: один злоупотребляющий адрес не должен тратить глобальный бюджет.
if !rl.ipLimiter(ip).Allow() {
return false
}
// rate.Limiter внутренне защищен мьютексом
return rl.global.Allow()
}
func (rl *Limiter) ipLimiter(ip string) *rate.Limiter {
rl.mu.Lock()
defer rl.mu.Unlock()
e, ok := rl.limiters[ip]
if !ok {
e = &ipEntry{lim: rate.NewLimiter(rl.perIPLimit, rl.perIPBurst)}
rl.limiters[ip] = e
}
e.lastSeen = time.Now()
return e.lim
}
// Cleanup удаляет лимитеры адресов, неактивных дольше ttl.
// Вызывайте периодически (например, из time.Ticker), иначе карта растёт без границ.
func (rl *Limiter) Cleanup(ttl time.Duration) {
cutoff := time.Now().Add(-ttl)
rl.mu.Lock()
defer rl.mu.Unlock()
for ip, e := range rl.limiters {
if e.lastSeen.Before(cutoff) {
delete(rl.limiters, ip)
}
}
}
Распределенный Rate Limiting и проблемы синхронизации
В кластере из N инстансов в-memory лимитеры бесполезны. Суммарный лимит будет в N раз больше заданного. Решение — распределённое состояние (Redis, Memcached).
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Client["Клиент"]:::entry --> LB["Load Balancer"]
LB --> Pod1["Go Pod 1"]
LB --> Pod2["Go Pod 2"]
LB --> PodN["Go Pod N"]
Pod1 -->|"EVAL ratelimit.lua"| Redis["Централизованный Redis / Cluster"]
Pod2 -->|"EVAL ratelimit.lua"| Redis
PodN -->|"EVAL ratelimit.lua"| Redis
Ключевые проблемы:
- Сетевая задержка: Каждый вызов
rateLimit.Check()добавляет RTT (round-trip time). При 1000 RPS это значит 1000 сетевых вызовов в секунду. - Атомарность: Чтение-модификация-запись в Redis должно быть атомарным. Используется Lua-скрипты (
EVAL), которые выполняются в одном потоке Redis, исключая race conditions. - Синхронизация часов: Если инстансы используют локальное время для window-алгоритмов, границы окон рассинхронизируются. В Redis-решениях обычно используется время сервера или логика "last access + TTL".
Базовая защита сетевых сервисов в Go
Помимо rate limiting, необходимо жестко ограничивать ресурсы на уровне net/http:
srv := &http.Server{
Addr: ":8080",
Handler: middleware.Chain(router),
MaxHeaderBytes: 1 << 20, // 1 MB заголовков. Защита от Slowloris и buffer overflow.
IdleTimeout: 120 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
Критические настройки:
MaxConnsPerHostиMaxIdleConns: Ограничивают количество соединений в пулеhttp.Client(для исходящих запросов).http.MaxBytesReader: Ограничивает тело запроса. Без этого атакующий может отправить 10 ГБ JSON, убив память сервиса.tls.Config: Отключение слабых шифров (TLS_RSA_WITH_RC4_128_SHA), принуждение кTLS 1.3, настройкаMinVersion.
// Защита обработчика от гигантских пейлоадов
func protectedHandler(w http.ResponseWriter, r *http.Request) {
// Ограничиваем чтение тела максимум 1 мегабайтом
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var data RequestPayload
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, "Payload too large or malformed", http.StatusRequestEntityTooLarge)
return
}
}
Mechanical Sympathy и влияние на рантайм Go
Защита от DDoS напрямую влияет на работу рантайма Go и ОС:
- GC Pressure: Высокая частота запросов генерирует аллокации (новые
context,map,[]byte). При атаке GC переходит в режим "stop-the-world" чаще, увеличивая p99 latency. Решение:sync.Poolдля повторного использования буферов, избежание аллокаций в hot path. - Goroutine Scheduling: Каждая горутина требует стека. При исчерпании лимита дескрипторов (
ulimit -nпо умолчанию 1024 в Docker)net.Listenначнёт падать сtoo many open files. ЧислоP(processor) в планировщике Go задаётсяGOMAXPROCSи к дескрипторам не относится; нехватка файловых дескрипторов проявляется ошибкамиaccept/dial(too many open files). С Go 1.19 рантайм при старте сам поднимает мягкий лимитRLIMIT_NOFILEдо жёсткого. - TCP Backlog: При SYN-флуде
net.Listen(backlog)упирается вnet.core.somaxconn. Если очередь заполнена, ядро начинает отбрасывать SYN-пакеты. Go не может это обойти на уровне приложения. - Context Propagation: В Go контекст передается по ссылке. При атаке с миллионами запросов создание
context.WithTimeoutдля каждого соединения создает нагрузку на GC. Пула контекстов в Go нет, а убирать таймаут нельзя: без него запрос может висеть бесконечно. Созданиеcontext.WithTimeoutдёшево; если профиль показывает, что это узкое место, ограничивайте число одновременных запросов (семафор), а не отказывайтесь от дедлайнов.
Вопросы на собеседовании (Middle+/Senior)
Итоги
Защита сетевых сервисов в Go строится на принципах ограничения ресурсов на каждом уровне:
- OS/Network: SYN cookies,
somaxconn, лимиты дескрипторов. - Proxy/WAF: Фильтрация L3-L4, DDoS scrubbing, IP reputation.
- Go Runtime:
http.Serverтаймауты,MaxHeaderBytes,http.MaxBytesReader, ограничение горутин через worker pools илиsemaphore. - Application: Распределённый rate limiting (Redis + Lua), валидация входных данных, пулинг объектов для снижения GC pressure.
Go не защищает вас от атак автоматически. Его мощь в эффективности требует дисциплины в управлении ресурсами. Без явного ограничения соединений, размеров тел запросов и частоты вызовов, высокопроизводительный сервис станет мишенью №1 для исчерпания ресурсов.
В следующей статье мы разберем инструменты, которые позволят вам видеть то, что скрывает Go: 34. Наблюдаемость сети. tcpdump, Wireshark, netstat, ss.