Одно приложение через WireGuard: привязка — не маршрутизация
Привязка приложения к wg0 задаёт только его адрес источника. Ядру по-прежнему нужны таблица с маршрутами туннеля и правило, которое отправляет этот источник туда.
Настройка выглядит как переключатель для отдельного приложения, поэтому люди привязывают клиент к интерфейсу WireGuard и ждут, что туннель будет использовать только этот клиент. На деле привязка выбирает адрес источника. Затем ядро ищет маршрут к назначению, находит обычный маршрут по умолчанию и отправляет пакет через физический канал с прикреплённым адресом туннеля. Замысел выполняет только маршрут, выбранный для этого адреса источника.
У механизма две половины: таблица маршрутизации, которой должен пользоваться только трафик туннеля, и правило, которое отправляет соответствующий источник туда.
Привязка задаёт адрес источника и ничего больше
Linux выбирает исходящий интерфейс, ища назначение в таблице маршрутизации, после того как правила политики получили возможность перенаправить этот поиск. Правила могут выбирать по адресу источника, метке файрвола, входящему интерфейсу, UID или даже порту; каждое — это другой способ сказать этот трафик, а не весь. Конфигурация VPN-клиента выбирает таблицу, в которой хранятся маршруты туннеля; правило выбирает, какому трафику разрешено к ней обращаться. Одна привязка не даёт ни того, ни другого.
Именно поэтому сбой выглядит так странно. Приложение определённо использует адрес туннеля, туннель определённо работает, а трафик определённо уходит через неправильный интерфейс. Каждое утверждение верно, и вместе они описывают систему, в которой ничто не связало адрес с маршрутом.
Дайте туннелю собственную таблицу
wg-quick устанавливает маршруты туда, куда указывает профиль. Добавление Table = 40 в раздел [Interface] переносит их в таблицу 40 вместо основной. Поднимите интерфейс и убедитесь, что они доехали: ip -4 route ls table 40 должна показать маршруты туннеля, а ip -4 route get 10.66.127.142 показывает, как ядро сейчас разрешает один из них. Когда маршруты в своей таблице, основная таблица продолжает работать для всего остального.
Сделайте то же для IPv6, вместо того чтобы сопоставлять широкий префикс. ip -6 rule add from fd00::2/128 lookup 40 закрепляет один адрес; fc00::/8 может столкнуться с другими интерфейсами на хосте и дать правило, которое совпадает с большим, чем задумано.
Добавьте правило источника и следите за счётчиками
Затем соедините половины: ip -4 rule add from 10.66.127.142/32 lookup 40, с тем же адресом, прикреплённым к интерфейсу WireGuard. Поместите оба правила в PostUp, чтобы переподключение их восстановило. Проверьте настоящим запросом, а не настройкой: ip route get 1.1.1.1 from 10.66.127.142 должно назвать устройство туннеля, а curl --interface wg0 ifconfig.me должно сообщить адрес туннеля, а не домашний.
Если всё ещё не работает, прочитайте ip -s rule. Правило, которое существует, но никогда не совпадало, показывает нулевые счётчики, и это отличает неверное правило от отсутствия правила. Одна эта команда избавляет от множества догадок о том, проигнорировало ли приложение свою настройку интерфейса.
Обратный случай: держите SSH вне туннеля
Противоположная проблема столь же часта: wg-quick владеет маршрутом по умолчанию, и SSH-сессия к серверу обрывается посреди сессии. Его правила стоят на низком номере приоритета, поэтому исключение должно оцениваться раньше них. ip rule add dport 22 ipproto tcp table main priority 123 делает это в свежем iproute2; там, где селектор недоступен, пометьте поток в nftables и сопоставляйте метку.
Проверьте и путь ответа. SSH-сессия, которая устанавливается и затем замолкает, обычно имеет асимметричную маршрутизацию: одно направление уходит через туннель, другое — через физический интерфейс. ip route get для адреса сервера подскажет, куда пойдёт ответ.
Запишите номер таблицы, правило источника и исключение в сам профиль. Все три относятся к одному проекту и будут редактироваться вместе.