Два канала в квартиру и желание разложить трафик по ним осмысленно. Маршрутизация по назначению тут не поможет — нужна маршрутизация по источнику.
Вводная: mini-PC с OpenWrt, два аплинка — основной кабельный и резервный LTE-свисток. Обычная таблица маршрутизации умеет отвечать на вопрос «куда идёт пакет» и понятия не имеет о вопросе «от кого он». А мне нужно ровно второе: рабочий ноут и стенд — в один канал, телевизор и гостевой wifi — в другой.
Это и называется policy routing: решение принимается не по destination, а по любому признаку, какой сумеем навесить.
Механика состоит из трёх частей, и путаница обычно возникает потому, что их смешивают:
main, а ещё своя на каждый аплинк.Заводим таблицы:
# /etc/iproute2/rt_tables
100 wan_main
200 wan_lte
Наполняем — в каждой ровно один маршрут по умолчанию, в свой шлюз:
ip route add default via 192.168.1.1 dev eth1 table wan_main
ip route add default via 192.168.8.1 dev wwan0 table wan_lte
И связываем метку с таблицей:
ip rule add fwmark 0x100 lookup wan_main priority 100
ip rule add fwmark 0x200 lookup wan_lte priority 200
Теперь собственно политика. В OpenWrt файрвол — nftables (в свежих версиях), метки ставим в mangle prerouting:
table inet mangle {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
ip saddr 192.168.10.0/24 meta mark set 0x200 # гостевая сеть -> LTE
ip saddr 192.168.1.50 meta mark set 0x200 # телевизор -> LTE
ip saddr 192.168.1.0/24 meta mark set 0x100 # остальные -> кабель
}
}
Порядок важен: последнее совпавшее правило перезапишет метку, поэтому частные случаи идут выше общих.
Вот здесь ломаются все с первого раза, и я сломался тоже.
Метка живёт на пакете, а пакет — один. Запрос ушёл через LTE, ответ приходит на интерфейс LTE — и роутер, маршрутизируя его назад в локалку, метки уже не имеет. Для транзитного трафика это чаще всего неважно, но как только у роутера появляются свои соединения или проброшенные порты — начинается асимметрия: пришло по LTE, ушло по кабелю, ответ отброшен как нежданный.
Лечится связкой connmark: метку запоминаем на соединении, а не на пакете.
table inet mangle {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
# у известного соединения — восстановить метку и больше не думать
ct mark != 0x0 meta mark set ct mark accept
ip saddr 192.168.10.0/24 meta mark set 0x200
ip saddr 192.168.1.0/24 meta mark set 0x100
# запомнить решение на соединении
meta mark != 0x0 ct mark set meta mark
}
}
Смысл: решение принимается один раз, на первом пакете соединения, и дальше просто восстанавливается из conntrack. Заодно дешевле — не гоняем правила на каждый пакет.
Второе следствие той же проблемы: rp_filter. Ядро по умолчанию отбрасывает пакеты, пришедшие «не с того» интерфейса, с точки зрения таблицы main. При двух аплинках это ровно наш случай. Либо net.ipv4.conf.all.rp_filter=2 (loose), либо готовьтесь долго смотреть в tcpdump и не понимать, куда деваются пакеты, которые точно приходят.
Отдельный абзац, оплаченный поездкой через весь город к роутеру, до которого больше не достучаться.
Ситуация: вы сидите по ssh, применяете правило, которое заворачивает вашу же подсеть в другой канал. Ваша ssh-сессия — часть этой подсети. Ответы роутера уходят в другой аплинк, сессия умирает вместе с вашей возможностью откатить правило.
Три меры, каждая из которых спасала:
Первая — исключить трафик, адресованный самому роутеру, до всякой маркировки:
iifname "br-lan" ip daddr 192.168.1.1 accept
Вторая — не трогать established: правило ct mark != 0x0 ... accept сверху как раз это и делает — существующие соединения продолжают ходить как ходили.
Третья, главная — не полагаться на первые две:
sleep 300 && /etc/init.d/firewall restart &
Запускаем перед тем, как применять что-то страшное. Если через пять минут вы всё ещё живы и довольны — убиваете таймер. Если нет — конфиг откатится сам. В OpenWrt для этого есть штатный механизм: luci-reload с подтверждением, но sleep && restart честнее и работает всегда.
Проверка — с нужной машины смотрим, каким IP мы наружу:
# с ноута
$ curl -s ifconfig.me
81.x.x.x # кабель
# с телевизора (через ssh на роутер, -I br-lan.10)
$ curl -s --interface 192.168.10.5 ifconfig.me
100.x.x.x # LTE, как и хотели
Работает уже полгода. Гостевой wifi и телевизор не мешают рабочему каналу, при падении кабеля можно одной строчкой перекинуть всех на LTE, а стенд ходит куда ему нужно и никого не трогает.
Всё это, конечно, умеет mwan3 — штатный пакет OpenWrt, с failover, health-check и веб-мордой. Я его сначала и поставил. Снёс через неделю: он делает то же самое, но пятью слоями абстракции поверх тех же ip rule, и когда что-то не работает — а не работает всегда — отлаживать приходится всё равно на уровне правил. Проще написать двадцать строк и понимать каждую.