Позначення трафіку з однієї cgroup — проста частина. Три окремі рішення мають збігтися, перш ніж один застосунок вийде через фізичний інтерфейс, а все інше залишиться в тунелі: який сокет належить винятку, який пошук маршруту запускає позначка і яку адресу джерела несе пакет, коли виходить. Більшість невдалих спроб роблять перше правильно, друге наполовину і ніколи не дивляться на третє.

Три рішення, а не одне

Класифікація відповідає, хто звільнений. Маршрутизація відповідає, куди натомість іде позначений пакет. Вибір джерела відповідає, що побачить далекий бік і куди має повернутися відповідь.

Ці речі відмовляють незалежно. Пакет може бути класифікований, переспрямований на фізичний інтерфейс і все одно нести приватну адресу, яку жоден вищий маршрутизатор не може повернути, що дає тайм-аут, який виглядає точно як блокування фаєрвола. Ніщо на кроці класифікації про це не попереджає. Ніщо й на кроці маршрутизації.

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

Якщо ти не можеш назвати, який із трьох етапів зламаний, ти не зневаджуєш, а переставляєш правила й сподіваєшся.

Зістав cgroup, яка існуватиме й завтра

nftables може перевірити належність сокета до cgroup v2, і це справді корисно, бо воно йде за процесом, а не за призначенням. Пастка — ідентифікатор. Застосунок, запущений із меню, часто живе в тимчасовому scope, ім'я якого змінюється під час наступного запуску.

nft list ruleset
systemctl status session.slice 2>/dev/null
cat /proc/self/cgroup

Копіювання імені scope з однієї сесії та жорстке його вписування дає правило, яке працює сьогодні й тихо перестає збігатися завтра. Натомість помісти застосунок в усвідомлений, стабільний контекст виконання, наприклад окремий scope або unit, яким ти керуєш, і зіставляй із ним. Тоді правило переживає перезавантаження й оновлення пакетів. Воно також стає чимось, про що колезі можна міркувати без історії твоєї сесії.

Будь також точним щодо механізму, який використовуєш. Класифікація net_cls у cgroup v1 і зіставлення сокетів у cgroup v2 — різні речі, і схоже назване поле в документації не робить їх взаємозамінними.

Другий пошук запускає ланцюг route

Для локально створеного трафіку тип ланцюга вирішує, чи щось станеться після встановлення позначки. Ланцюг output типу filter може позначити пакет, і ядро продовжить використовувати вже обраний маршрут. Ланцюг типу route — це механізм, що просить нового пошуку.

table inet bypass {
  chain mark {
    type route hook output priority mangle; policy accept;
    socket cgroupv2 level 2 "app.slice" meta mark set 0x1 comment "app bypass"
  }
}
ip rule add fwmark 0x1 lookup 200
ip route add default via 203.0.113.1 dev wlan0 table 200

Далі позначці потрібне правило політики, яке надішле її до таблиці, і ця таблиця має бути повною: маршрут за замовчуванням, локальні призначення, які застосунку ще потрібні, і вищий шлюз, що справді відповідає.

Вибирай значення позначки усвідомлено. WireGuard використовує власний fwmark для зовнішнього транспортного трафіку, і випадкове потрапляння на те саме число може завадити власному винятку маршрутизації тунелю так, що діагностувати буде боляче.

Адреса джерела не йде за маршрутом

Сокет, який уже вибрав адресу джерела для шляху тунелю, зберігає цю адресу після переспрямування. Ніщо у зміні маршруту її не переписує, тому пакет виходить через фізичний інтерфейс із приватним джерелом, яке вища мережа не може повернути.

Видимий симптом — застосунок, що ніби надсилає дані й ніколи не отримує відповіді, та журнал фаєрвола, який нічого не показує, бо пакет так і не повернувся.

Вузьке виправлення — правило трансляції джерела, обмежене позначкою цього застосунку та його реальним вихідним інтерфейсом, вбудоване в наявний NAT, а не замість нього:

table inet bypass {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    meta mark 0x1 oifname "wlan0" masquerade
  }
}

Транслюй виняток і нічого більше. Загальне правило masquerade виправляє симптом і руйнує здатність сказати, який трафік пішов яким шляхом. Зіставляй позначку, реальний вихідний інтерфейс і, якщо можеш, адресу самого застосунку. Широкі правила легко писати й майже неможливо потім перевіряти.

Що ще вислизає від винятку

Розв'язання імен може надходити від окремого процесу resolver в іншій cgroup, тож класифікація сокета браузера не класифікує його запитів. Якщо запити resolver залишаються в тунелі, а з'єднання виходить за його межі, відповіді split-horizon можуть не збігатися зі шляхом. Ця розбіжність дає збої, схожі на помилки DNS, якими вони не є.

Дочірні процеси успадковують cgroup у більшості конфігурацій, але помічник, який переносить себе у власний scope, — ні. Перевіряй, а не припускай, особливо для всього, що перезапускає себе.

Насамкінець виріши, що має статися, коли тунель опущений. Виняток, який тихо стає повним обходом, — це інша позиція безпеки, ніж той, що закривається при відмові, і лише один із них — те, що ти мав на увазі.

Схожі статті