Перейти к содержанию

Техническая глубина

Укрепление Linux-сервера против DDoS: все параметры sysctl, conntrack и nftables

Обновлено: август 2026 г. · Каждый параметр sysctl и nftables, его значение и проверка · Время чтения ~22 мин

Строение, окружённое тремя концентрическими стенами: большая часть входящих амбровых брызг гаснет на внешней стене, остаток — на второй; внутренний двор спокоен под ровным бирюзово-зелёным светом.

На Linux-сервере укрепление против DDoS защищает три конечных ресурса: очереди SYN и accept, таблицу conntrack и файловые дескрипторы. У каждого параметра есть значение по умолчанию, функция и счётчик для проверки, и укрепление начинается со знания всех трёх. Значения выбираются по вашей базовой линии; скопированное значение либо бесполезно, либо режет ваших же пользователей.

Это руководство отвечает на один вопрос: как укрепить сам Linux-сервер, без всякого устройства перед ним, против той части DDoS-атаки, что исчерпывает состояние. Промежуточные слои защиты мы здесь не обсуждаем. Речь идёт о собственных настройках ядра сервера.

Большинство текстов на эту тему — это список sysctl: двадцать строк без объяснений. Список как будто работает, потому что значения безобидны. В день атаки команда, не знающая, что делает каждая строка, не знает и на какой счётчик смотреть. Ниже каждый параметр идёт вместе со своим значением, своей функцией и своей командой проверки. То, что атака может исчерпать на хосте, конечно и счётно. Каждому ресурсу отведён свой раздел.

Исчерпаемые ресурсы и команды их проверки
УстройствоРесурсСимптомКоманда проверки
Очередь SYN / acceptНовые соединения молча отваливаются по таймаутуnstat -az | grep -i listen
Таблица conntrackdmesg: 'nf_conntrack: table full, dropping packet'conntrack -C; conntrack -S
Файловые дескрипторыaccept() возвращает EMFILE; порт слушает, но никого не принимаетcat /proc/<pid>/limits; cat /proc/net/sockstat
TIME_WAIT / осиротевшие сокетыИсчерпаны эфемерные порты или память; 'Out of socket memory' в логеss -tan state time-wait | wc -l; cat /proc/net/sockstat
Буферы сокетовЛегитимный трафик теряется под нагрузкойnetstat -s | grep -i prune; cat /proc/net/softnet_stat

Все пять строк — предмет этой статьи, и все пять измеряются на хосте. Насыщение процессора по числу пакетов (softirq) — отдельный слой, он разобран в руководстве по сетевому стеку.

0. Сначала базовая линия: нельзя настроить то, что не измерил

Перед укреплением запишите цифры обычной недели. Каждый порог ниже выбирается относительно них.

# Snapshot of sockets (established, syn-recv, time-wait breakdown)
ss -s

# Socket counters: tcp inuse / orphan / tw, tcp mem
cat /proc/net/sockstat

# conntrack occupancy and ceiling
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

# Cumulative TCP events — handshake, queue drops
nstat -az | grep -Ei 'syn|listen|drop|overflow'

# To watch over time
watch -n1 'cat /proc/net/sockstat; echo; nstat | grep -Ei "listen|syncookie"'

Вывод этих пяти команд — ваша базовая линия. Неделю спустя вы запускаете те же команды и видите, что изменилось. Беда скопированного порога в том, что он этот шаг пропускает.

1. Сторона SYN: очередей две, а не одна

Ядро держит две очереди на каждый слушающий сокет. Полуоткрытая очередь держит соединения с незавершённым рукопожатием (SYN_RECV); её размер — tcp_max_syn_backlog. Очередь accept держит завершённые рукопожатия в ожидании accept(); её потолок — меньшее из somaxconn и backlog вызова listen() в приложении.

Из-за этого разделения поднятие одного лишь sysctl часто ничего не меняет: если приложение слушает с backlog 128, то что вы записали в ядро — неважно.

# /etc/sysctl.d/90-ddos.conf — SYN layer
net.ipv4.tcp_syncookies = 1          # cookie при переполнении очереди; современное значение по умолчанию
net.ipv4.tcp_max_syn_backlog = 8192  # размер полуоткрытой очереди
net.core.somaxconn = 8192            # потолок очереди accept
net.ipv4.tcp_synack_retries = 2      # сокращает жизнь неотвеченного SYN_RECV
net.ipv4.tcp_syn_retries = 3         # для исходящих соединений; сторона клиента

Поднимите backlog приложения одновременно, иначе один somaxconn ничего не даст:

# nginx: listen backlog выравнивается по somaxconn
listen 443 ssl backlog=8192;

Проверка. Читаем сокеты в SYN_RECV и сбросы очереди:

# How many connections are half-open right now?
ss -tan state syn-recv | wc -l

# Accept queue occupancy: Recv-Q is the current backlog, Send-Q the ceiling
ss -ltn

# Queue overflow counters — if these leave zero, the accept chain is narrow
nstat -az | grep -E 'TcpExtListenDrops|TcpExtListenOverflows'

# Did SYN cookies actually engage?
nstat -az | grep -i syncookie

Если SyncookiesSent больше нуля, ваша очередь переполнялась хотя бы раз; это сигнал поднять backlog или задействовать вышестоящий слой. Пока SYN cookies активны, а метки времени TCP выключены, для тех соединений теряются масштабирование окна и SACK. Поэтому не полагайтесь на cookie, а стремитесь к очереди, которая не переполняется вовсе.

tcp_abort_on_overflow при переполнении очереди accept шлёт RST вместо молчаливого сброса. Значение по умолчанию (0) — молчаливый сброс, и обычно оно верное: клиенту остаётся место для повторной попытки. Если за сервером стоит балансировщик со своей логикой повторов, значение 1 — более честный сигнал:

sysctl -w net.ipv4.tcp_abort_on_overflow=1   # only with a reason

2. conntrack: как только таблица полна, новый поток не пройдёт

Отслеживание соединений netfilter держит запись на каждый поток. Когда таблица заполняется, ядро сбрасывает пакет нового потока. Симптом коварен: существующие сессии живут, все новые остаются снаружи, а график говорит «трафик в норме».

# Current occupancy and ceiling
conntrack -C
cat /proc/sys/net/netfilter/nf_conntrack_max

# How many flows in which state? (a SYN_SENT flood shows up here)
conntrack -L 2>/dev/null | awk '{print $4}' | sort | uniq -c | sort -rn

# Have drops started?
dmesg | grep -i 'nf_conntrack: table full'
conntrack -S | grep -Eo 'drop=[0-9]+'

Рычагов три. Размер, время жизни и вовсе не отслеживать.

# /etc/sysctl.d/90-ddos.conf — conntrack
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 3600   # default 432000 (5 days)
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 30        # clear half-open fast
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_loose = 0                    # do not track one-way flows

Размер хеш-таблицы (buckets) задаётся параметром модуля, а не sysctl; держа его хотя бы на уровне одной восьмой потолка, вы снижаете число коллизий:

echo 262144 > /sys/module/nf_conntrack/parameters/hashsize
# persistent: in /etc/modprobe.d/nf_conntrack.conf
#   options nf_conntrack hashsize=262144

Третий рычаг держит бесстатусный высокообъёмный трафик полностью вне таблицы. Raw-цепочка nftables:

table inet raw {
  chain prerouting {
    type filter hook prerouting priority raw; policy accept;
    udp dport 53 notrack
    tcp dport 53 notrack
  }
}

Бесстатусная служба вроде авторитетного DNS этим правилом становится невосприимчива к conntrack-флуду. Цена очевидна: правила ct state к этому трафику не применяются.

Порог тревоги: заведите в мониторинг занятость на уровне семидесяти процентов потолка.

awk -v max="$(cat /proc/sys/net/netfilter/nf_conntrack_max)" \
    -v cur="$(cat /proc/sys/net/netfilter/nf_conntrack_count)" \
    'BEGIN{ printf "conntrack %d/%d = %.0f%%\n", cur, max, 100*cur/max }'

3. Файловые дескрипторы: побеждает самое короткое звено цепи

Каждое открытое соединение — это файловый дескриптор. Когда они кончаются, accept() возвращает EMFILE; служба поднята, порт слушает, но не принимает никого. Атаки медленными соединениями метят именно сюда.

Ограничение — это цепь, и правит самое нижнее звено:

# System-wide ceiling
cat /proc/sys/fs/file-max
# Per-process absolute ceiling
cat /proc/sys/fs/nr_open
# How many descriptors are open now / what is the ceiling?
cat /proc/sys/fs/file-nr
# /etc/sysctl.d/90-ddos.conf — system wide
fs.file-max = 2097152
fs.nr_open = 1048576

Настоящее узкое место почти всегда — лимит юнита службы, потому что на большинстве дистрибутивов он остаётся низким. Долговременное решение — drop-in systemd; не воюйте с ulimit, который описывает вашу оболочку, а не службу:

# /etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=262144
systemctl daemon-reload
systemctl restart nginx

# The service's REAL limit (not the shell's):
systemctl show nginx -p LimitNOFILE
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'

# Overall socket usage:
cat /proc/net/sockstat   # sockets: used ...; TCP: inuse ...

4. TIME_WAIT и осиротевшие сокеты: тихое исчерпание

Флуд короткоживущими соединениями исчерпывает два ресурса через накопление TIME_WAIT и осиротевшие сокеты: диапазон эфемерных портов и память TCP-сокетов. Симптом — строка TCP: out of memory или Out of socket memory в dmesg.

# State distribution
ss -tan state time-wait | wc -l
ss -tan state fin-wait-1 | wc -l

# Orphan / tw counts and tcp mem pressure
cat /proc/net/sockstat
# tcp_mem: low pressure high  (third number is the hard ceiling in pages)
cat /proc/sys/net/ipv4/tcp_mem
# /etc/sysctl.d/90-ddos.conf — TIME_WAIT / orphan
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 1440000
net.ipv4.tcp_max_orphans = 262144
net.ipv4.tcp_tw_reuse = 1            # safe for outbound connections
net.ipv4.ip_local_port_range = 1024 65535

tcp_tw_reuse безопасен только для исходящих соединений и при включённых метках времени; не используйте старый tcp_tw_recycle, который ломает клиентов за NAT и из современных ядер всё равно удалён. При превышении tcp_max_orphans ядро закрывает лишнее через RST и пишет too many orphaned sockets в dmesg; это защитное поведение, поэтому держите лимит выше вашей легитимной нагрузки.

5. Буферы сокетов и очередь приёма

Этот слой не останавливает атаку; он снижает подавление легитимного трафика во время неё. Если очередь на пути приёма переполняется, хороший пакет теряется вместе с плохим.

# /etc/sysctl.d/90-ddos.conf — buffers and receive queue
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.netdev_max_backlog = 16384      # NIC → kernel queue
net.core.netdev_budget = 600             # packets processed per softirq
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

Проверка — терял ли пакеты путь приёма, исчерпан ли бюджет softirq:

# softnet_stat: col 1 processed, col 2 DROPPED, col 3 left by budget/time-limit
cat /proc/net/softnet_stat

# TCP-side prune / collapse (a sign of buffer pressure)
netstat -s | grep -Ei 'prune|collapse|out of'
nstat -az | grep -Ei 'TcpExtTCPRcvQDrop|PruneCalled'

Если второй столбец softnet_stat растёт, поднимите netdev_max_backlog; если растёт третий — поднимите netdev_budget. Хронический рост третьего столбца — заодно и ранний признак того, что число пакетов начинает насыщать процессор. Эта черта — граница данной статьи, и она переходит к руководству по тюнингу сетевого стека.

6. Подделка источника, ICMP и поверхность маршрутизации

Небольшая, но необходимая группа. Против поддельных адресов источника и поверхности отражения:

# /etc/sysctl.d/90-ddos.conf — anti-spoof and ICMP
net.ipv4.conf.all.rp_filter = 1              # reverse-path filtering (strict)
net.ipv4.conf.default.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1     # close the smurf surface
net.ipv4.icmp_ratelimit = 1000
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.tcp_timestamps = 1                  # required for tw_reuse and PAWS

rp_filter=1 (строгий режим) верен только на сервере с одним аплинком. На машине с асимметричной маршрутизацией, принимающей трафик более чем одним путём, строгий режим сбрасывает легитимные пакеты; там выбирают 2 (нестрогий режим). Неверный режим может вызвать простой вообще без атаки; не ставьте 1, не зная, какой режим вам нужен:

# Effective rp_filter per interface (the max of 'all' and the interface applies)
for i in /proc/sys/net/ipv4/conf/*/rp_filter; do echo "$i = $(cat $i)"; done

# How many packets did rp_filter drop?
nstat -az | grep -i 'IPReversePathFilter'

7. Ограничение скорости по источнику в nftables

Увеличение очередей — пассивная половина. Активная ограничивает долю одного источника, внутри ядра. Динамическое множество nftables делает это на каждый адрес источника:

table inet filter {
  set flood4 { type ipv4_addr; flags dynamic; timeout 60s; }
  set flood6 { type ipv6_addr; flags dynamic; timeout 60s; }

  chain input {
    type filter hook input priority filter; policy accept;

    ct state established,related accept
    ct state invalid drop                 # drop half/broken flows

    # Per-source new-connection rate — IPv4 and IPv6 separately
    tcp dport { 80, 443 } ct state new \
      add @flood4 { ip  saddr limit rate over 30/second } drop
    tcp dport { 80, 443 } ct state new \
      add @flood6 { ip6 saddr limit rate over 30/second } drop
  }
}

Проверка — какие источники упёрлись в лимит, сколько пакетов сбросило правило:

# Sources that hit the limit and entered the set
nft list set inet filter flood4

# Rule counters (if you add 'counter' to the rule)
nft list ruleset | grep -A2 flood

# How much did ct state invalid drop — a counted rule example
nft add rule inet filter input ct state invalid counter drop

Если пропустить множество для IPv6, половина защиты отсутствует. Порог берётся из вашего измерения: для легитимной толпы за NAT 30 может быть слишком мало, для корпоративного API — много. Первую неделю гоняйте правило с counter вместо drop, смотрите, кто попадается, и переходите на drop, лишь убедившись, что легитимный трафик не режется.

Есть ещё synproxy — рукопожатие берёт на себя ядро, а до службы за ним доходят только завершённые соединения — но его взаимодействие с асимметричной маршрутизацией и настройками conntrack тонкое; не выносите его в продакшен без проверки в лаборатории.

Честный предел ограничения по источнику: оно работает против флуда с немногих адресов. Флуд, размазанный по тысячам адресов, где каждый держится ниже порога на адрес, способен заполнить ваш канал в сумме, и это правило его не видит. Форму той же логики в масштабе префикса мы разобрали в руководстве по ковровой бомбардировке.

8. Поведение соединений на уровне приложения

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

net.ipv4.tcp_keepalive_time = 300     # detect a dead connection early
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3

Атаки медленными соединениями (открыть соединение и цедить данные по капле) на уровне ядра полностью не решаются; настоящая защита — собственные таймауты веб-сервера. Это мы разбираем по серверам: руководства по укреплению nginx и укреплению Apache дают эти таймауты директива за директивой.

Полный файл и загрузка за один раз

Разделы вместе:

sudo tee /etc/sysctl.d/90-ddos.conf >/dev/null <<'EOF'
# --- SYN ---
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 8192
net.ipv4.tcp_synack_retries = 2
# --- conntrack ---
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 30
# --- file descriptors ---
fs.file-max = 2097152
fs.nr_open = 1048576
# --- TIME_WAIT / orphan ---
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 1440000
net.ipv4.tcp_max_orphans = 262144
net.ipv4.tcp_tw_reuse = 1
# --- buffers ---
net.core.netdev_max_backlog = 16384
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# --- anti-spoof / ICMP ---
net.ipv4.conf.all.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.tcp_timestamps = 1
EOF

sudo sysctl --system
# verify it applied
sudo sysctl -a 2>/dev/null | grep -E 'somaxconn|syncookies|conntrack_max|file-max|rp_filter'

Ни одно из значений не требует перезагрузки, и все обратимы. Но все они — отправные точки, подтягиваемые вверх или вниз по вашей собственной базовой линии. Руководство, дающее вам точные числа, вводит в заблуждение, потому что верное число зависит от числа ваших сессий, от распределения источников и от вашего железа.

Счётчики для вывода в мониторинг

Укрепление не чувствуют, его читают. Заведите в систему тревог эти шесть сигналов:

# 1. SYN queue drops
nstat -az | grep -E 'ListenDrops|ListenOverflows'
# 2. SYN cookie use (an overflow indicator)
nstat -az | grep -i syncookiessent
# 3. conntrack occupancy ratio (>70% warn)
cat /proc/sys/net/netfilter/nf_conntrack_count
# 4. file descriptor use
cat /proc/sys/fs/file-nr
# 5. receive-path drops
awk '{s+=$2} END{print "softnet drops:", s}' /proc/net/softnet_stat
# 6. socket memory pressure
grep -E 'TCP:' /proc/net/sockstat

Эти шесть строк меньше чем за минуту во время атаки отвечают на вопрос «сужается хост или заполняется канал».

Честный предел этого слоя

Всё до этого места сводится к одной фразе: сервер защитим против атак, исчерпывающих его собственное состояние. За её пределами — два случая, и ни один не решается на хосте.

Первый — насыщение канала. Объём выше вашего канала доступа никогда не доходит до ядра; sysctl не может сбросить пакет, которого не видит. Выше этой черты предмет уже не сервер, а сетевая архитектура.

Граница, за которой уровень на площадке уже не помогает Ваш канал доступа 10 Гбит/с Канал уже забит — только вышестоящий уровень Устройство на площадке подавляет 2 Гбит/с 8 Гбит/с 25 Гбит/с 120 Гбит/с 1 Тбит/с+ Объём атаки (логарифмическая шкала)
Ниже ёмкости канала у слоя хоста есть доля; выше неё ни одна настройка sysctl ничего не значит.

Второй — скорость пакетов. Ещё до заполнения канала число пакетов в секунду способно насытить способность ядра обрабатывать прерывания; симптом — третий столбец softnet_stat и уход процессора в softirq. Настройки того слоя — распределение прерываний, очереди драйвера, RSS/RPS, путь приёма — это отдельный мир, разобранный команда за командой в руководстве по тюнингу сетевого стека.

Выше этих двух порогов укрепление хоста не становится бесполезным; напротив, поднимая пол, именно оно держит сервер на ногах, пока не включится верхний слой, и после того как включится. Но само по себе оно недостаточно, и относиться к нему так, будто достаточно, — значит порождать ложную уверенность.

Порядок применения

  1. Запишите базовую линию (раздел 0). Это стоит недели терпения; это самый дорогой шаг.
  2. Напишите 90-ddos.conf, загрузите через sysctl --system, проверьте через grep.
  3. Выровняйте backlog приложения (listen ... backlog=) с somaxconn.
  4. Добавьте drop-in LimitNOFILE в юниты служб, проверьте через systemctl show.
  5. Настройте размер/время жизни conntrack; выведите бесстатусные службы из таблицы через notrack.
  6. Погоняйте ограничение nftables в режиме counter неделю, затем переключите на drop.
  7. Заведите шесть счётчиков в мониторинг как тревоги.

Ни один из семи шагов не требует перезагрузки, и все обратимы.

Частые вопросы

Собрать все настройки в один файл?
Да, в отдельный файл под /etc/sysctl.d/. Вместо правки собственных файлов дистрибутива создайте файл с высоким номером, например 90-ddos.conf; высокий номер означает, что на конфликтующем параметре побеждает ваш. Загрузка: sysctl --system перечитывает весь каталог, sysctl -p /etc/sysctl.d/90-ddos.conf — только этот файл. Файл нужен для сохранения между перезагрузками; значение, записанное через sysctl -w, теряется при перезагрузке.
Требуют ли эти изменения перезагрузки?
Почти ни одно; параметры sysctl вступают в силу сразу. Исключения — несколько значений, читаемых очень рано, и размер хеш-таблицы conntrack: nf_conntrack_buckets меняется в рантайме через /sys/module/nf_conntrack/parameters/hashsize, а не через sysctl. Drop-in systemd для файловых дескрипторов требует systemctl daemon-reload и перезапуска службы; вживую, как sysctl, он не применяется.
Можно ли полностью отключить conntrack?
Выборочно, на сервере без NAT и без нужды в отслеживании состояния — да. Цель notrack в raw-цепочке nftables держит определённый трафик полностью вне таблицы; порт 53 авторитетного DNS-сервера — классический случай. Тогда этот трафик не может заполнить таблицу conntrack, потому что таблица для него не ведётся. Цена в том, что правила ct state к этому трафику не применяются; вы выбираете это сознательно.
Останавливают ли эти настройки объёмную атаку?
Нет. Трафик, заполняющий ваш канал доступа, никогда не доходит до ядра, а ядро не может управлять пакетом, которого не видит. Каждая настройка здесь защищает от исчерпания состояния: очереди, таблицы, дескрипторы и буферы. Объём выше ёмкости вашего канала — не проблема хоста и на хосте не решается; выше этой черты остаётся только сетевая сторона.
Задавать эти значения внутри контейнера или на хосте?
Большинство параметров сетевого стека привязаны к сетевому пространству имён и должны задаваться в собственном пространстве контейнера; значение хоста внутрь не переходит. В Kubernetes безопасное подмножество разрешено через поле sysctl, остальное требует привилегированного контекста. Общесистемные значения вроде fs.file-max остаются на хосте и охватывают все контейнеры. Не считайте параметр привязанным к пространству имён без проверки для критичной настройки; вывод sysctl -a внутри пространства имён может отличаться от хоста.
Как проверить всё это без атаки?
В лаборатории, на своём же трафике. Наращивайте скорость SYN с помощью hping3 и скорость новых соединений с помощью ab или wrk, и на каждом шаге читайте nstat -az, conntrack -C и ss -s. Ищете вы то, какой счётчик сдвигается при какой скорости; эта скорость и есть ваш реальный порог, а не число из даташита. В продакшене отклонение тех же счётчиков от базовой линии — первый сигнал для вывода в мониторинг.

Опубликовано: август 2026 г.

Руководство обновляется по мере выхода новых моделей и условий лицензирования. Как мы сравниваем производителей