Ответ должен уйти тем же интерфейсом, которым пришёл запрос. Обычная таблица маршрутов этого не обещает: она смотрит на адрес назначения и спокойно выбирает другой выход. В несимметричной сети, у чёрной дыры и там, где из сети есть несколько выходов, такой выбор ломает разговор.
Этот репозиторий хранит привязку в самом соединении. На первом пакете цель nftables DYNSRCROUTE записывает в расширение conntrack выходной интерфейс и, если он есть в кадре, адрес соседа на канальном уровне. Дальше ядро, уже в поиске маршрута, принуждает ответ идти через этот интерфейс. Условие DYNRT только проверяет, что запись для текущего направления уже есть.
Правило нужно один раз, на класс трафика. Отдельной метки fwmark, отдельного ip rule и отдельной таблицы на каждый поток нет.
Проверенная связка: ядро Linux 6.12.111, nftables 1.1.3, libnftnl 1.2.9. Модуль собирается вне дерева ядра. В ядро входят шесть узких патчей, и без загруженного модуля они ничего не меняют в поведении.
Подробный рассказ, со схемами и с историей испытаний, лежит в article/article_habr_02.md. Рядом черновик article/article_habr.md.
Makefile сборка модуля
srcdynroute.c цель DYNSRCROUTE и условие DYNRT
include/dynrt.h раскладка ячейки и договор с ядром
patches/kernel/0001 … 0006 патчи ядра 6.12.111
patches/userspace/1001, 1002 грамматика nftables и libnftnl
tests/ серия S1–S8 и обвязка
article/ статьи и схемы
MANIFEST sha256 перечисленных деревьев
LICENSE GPL-2.0
Патчи ядра, по порядку:
| патч | что делает |
|---|---|
0001-dynrt-abi-header |
заголовок include/net/netfilter/dynrt.h: две ячейки по 16 байт, 32 байта на соединение |
0002-ct-ext-src-dyn-rt |
расширение conntrack NF_CT_EXT_SRC_DYN_RT |
0003-nf-dynrt-helper |
встроенный посредник net/netfilter/nf_dynrt.c: указатель на операции под RCU и пустые обёртки, пока модуль не загружен |
0004-fib-oif-forcing |
принудительный выходной интерфейс в поиске маршрута IPv4 и IPv6, на входном и исходящем пути; если маршрута нет, один запасной поиск без принуждения |
0005-neighbour-l2-override |
подмена адреса канального уровня в neigh_resolve_output и neigh_connected_output |
0006-fib-blackhole-beats-forced-oif |
маршрут blackhole не перебивает уже закреплённый выходной интерфейс |
Патч 0003 включается ключом CONFIG_NF_DYNRT. Патчи 0004–0006 зависят от этого заголовка, не от conntrack и не от nftables.
Пользовательские правки:
| патч | что делает |
|---|---|
1001_nft-dynrt.patch |
слова DYNSRCROUTE и DYNRT в грамматике nftables 1.1.3 |
1002_libnftnl-dynrt.patch |
операции без атрибутов в libnftnl 1.2.9 |
Их ставят в /usr/local, чтобы не затереть дистрибутивный /usr/sbin/nft.
Примеры, схемы и испытания говорят на двух сетях:
| роль | адрес |
|---|---|
первый сегмент, хост enp4s0 |
192.168.200.67 |
первый сегмент, шлюз enp0s3 |
192.168.200.12 |
второй сегмент, мост brvm0 |
192.168.129.1 |
второй сегмент, шлюз enp0s8 |
192.168.129.12 |
Сети 192.168.200.0/24 и 192.168.129.0/24 не пересекаются. Испытания ждут именно эти адреса на названных интерфейсах. Переименование в файлах само по себе ничего не меняет на живых машинах: интерфейсам эти адреса нужно назначить до прогона.
Рядом живут другие сети, их замена не касалась. IPv6 в S2 — fd00:db8:beef::/64. Пересылка в S3 — 10.66.0.0/24. Пара пространств имён в S4 — 10.0.1.0/24 и 10.0.2.0/24. Синтетические сегменты S6 — 172.30.10.0/24, 172.31.10.0/24, 172.33.10.0/24. Наложение VXLAN — 192.168.77.0/24 поверх первого сегмента.
Имена интерфейсов в тестах свои: на шлюзе enp0s3 и enp0s8, на хосте enp4s0 и brvm0. Команды на шлюз уходят по ssh, псевдоним хоста в ~/.ssh/config — deb13. nft на шлюзе ожидается в /usr/local/sbin/nft.
Из корня репозитория клонируют дерево 6.12.111 и накладывают патчи из соседнего каталога patches/kernel:
git clone --depth 1 --branch v6.12.111 \
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git linux-6.12.111
cd linux-6.12.111
for p in 0001-dynrt-abi-header \
0002-ct-ext-src-dyn-rt \
0003-nf-dynrt-helper \
0004-fib-oif-forcing \
0005-neighbour-l2-override \
0006-fib-blackhole-beats-forced-oif; do
git apply ../patches/kernel/$p.patch
done
make olddefconfig
scripts/config --enable NF_DYNRT
make -j"$(nproc)" bindeb-pkgbindeb-pkg собирает пакеты. Для одной лишь сборки модуля достаточно make modules_prepare в этом дереве. Каталог linux-6.12.111/ в репозиторий не входит.
Архивы распаковывают в корне репозитория, чтобы ../patches/userspace смотрел куда нужно.
tar xf libnftnl-1.2.9.tar.bz2
cd libnftnl-1.2.9
git apply ../patches/userspace/1002_libnftnl-dynrt.patch
./configure --prefix=/usr/local && make -j"$(nproc)" && make install
cd ..
tar xf nftables-1.1.3.tar.bz2
cd nftables-1.1.3
git apply ../patches/userspace/1001_nft-dynrt.patch
./configure --prefix=/usr/local && make -j"$(nproc)" && make install
cd ..
ldconfigПроверка, что в пути именно эта сборка: ldd /usr/local/sbin/nft.
KDIR — дерево, в которое уже вошли патчи 0001–0006. Если каталог /root/srcdynroute/linux-6.12.111 существует, Makefile берёт его сам. Иначе нужен явный путь, либо заголовки ядра, на котором идёт сборка.
make KDIR=/путь/к/linux-6.12.111
make KDIR=/путь/к/linux-6.12.111 install
modprobe srcdynrouteМодуль зависит от nf_conntrack и nf_tables. Псевдонимы nft-expr-DYNSRCROUTE и nft-expr-DYNRT позволяют ему загрузиться при первом обращении из nft. Выгрузка (rmmod) имеет смысл ещё и потому, что обработчик выхода очищает таблицу anchor. Пока на модуль ссылается правило, ядро ответит Module in use: сначала удаляют таблицу nftables.
В блоке chain { } каждая строка — отдельное правило. Условия и DYNSRCROUTE пишут одной строкой. Иначе цель сработает на любом пакете, а пустые строки условий не отсекают ничего. Это штатный разбор nftables, не особенность нашей сборки.
table inet dynrt {
chain wr {
type filter hook prerouting priority filter; policy accept;
iifname "enp0s3" ip daddr 192.168.200.12 icmp type echo-request counter DYNSRCROUTE
}
}
Проверка, что правило одно: /usr/local/sbin/nft --debug=netlink list table inet dynrt.
Смысл строки: пакет запроса вошёл в enp0s3 и адресован шлюзу. Ядро запоминает этот вход. Ответ того же соединения уйдёт обратно через enp0s3, даже если маршрут по умолчанию смотрит в enp0s8.
Для сценариев с маршрутом blackhole на время проверки ослабляют rp_filter:
sysctl -w net.ipv4.conf.all.rp_filter=0
sysctl -w net.ipv4.conf.enp0s3.rp_filter=0Серия S1–S8 гоняется с хоста. Обвязка — tests/lib.sh, вход — tests/run_all.sh. Перед прогоном скрипт сам сбрасывает остаточные таблицы, перезагружает модуль и делает conntrack -F. Без этого отрицательная фаза может увидеть старую запись anchor: она живёт 60 секунд после удаления правила. Это свойство механизма, и тесты его учитывают.
cd tests
./run_all.sh # итог пишется в /tmp/dynrt_results.log
./S1_v4_inout.sh # или один сценарий| тест | что проверяет |
|---|---|
| S1 | IPv4, вход и выход: испорченный маршрут 192.168.200.0/24 уводит ответ в enp0s8, правило возвращает его в первый сегмент |
| S2 | то же для IPv6, префикс fd00:db8:beef::/64 |
| S3 | путь пересылки FORWARD и blackhole на ответе |
| S4 | FORWARD и явный SNAT |
| S5 | blackhole на локальном исходящем пути, его обходит патч 0006 |
| S6 | veth, TAP с чужим ответом ARP, VXLAN; dummy пропускается задуманно, bond зависит от CONFIG_NET_BONDING |
| S7 | сто одновременных соединений |
| S8 | ячейка переживает удаление таблицы на живом соединении и не действует на новое |
Заметки к свидетельствам и к тому, как читать счётчики, — в tests/README.md. Журналы исторических прогонов в это дерево не входят.
Короткий ручной жест для первого сегмента:
ip route replace 192.168.200.0/24 dev enp0s8
ping -c 5 192.168.200.12Без правила ответ виден на enp0s8. С правилом выше — на enp0s3. Имеет смысл смотреть оба интерфейса через tcpdump: счётчик nftables и ip route get по отдельности картину не закрывают.
MANIFEST хранит sha256 исходников, патчей, тестов, статей и этого README. В комментариях там же md5 каждого патча. sha256sum предупреждает о комментариях и проверяет только строки с суммой. Код возврата нулевой, когда все эти строки сошлись:
sha256sum -c MANIFESTМодуль, заголовок и патчи ядра — GPL-2.0-only, как в строке SPDX-License-Identifier у srcdynroute.c. Текст лицензии — в LICENSE. Правки nftables и libnftnl живут по лицензии тех проектов, к которым они прикладываются.