Почему ловушка VPN-межсетевого экрана добавляет одно и то же правило дважды
Повторное подключение может снова запустить ловушку, поэтому проектируйте под желаемое состояние: собственные цепочки, проверка перед вставкой, одно сериализованное изменение и узкий демонтаж.
Ловушка работает в первый раз, а после трёх повторных подключений в цепочке остаются три копии одного и того же правила. Ничего не упало громко; скрипт просто при каждом запуске делал то, что ему говорили. Ловушка VPN — это операция настройки, которая может выполняться больше одного раза, поэтому ей нужны владелец, желаемое состояние и сериализованное изменение. Идите в этом порядке, и дубликаты прекратятся.
Дайте правилам владельца, прежде чем писать хотя бы одно
Решите, какой цепочке или таблице принадлежит эта интеграция, прежде чем писать хотя бы одно правило. Отдельная цепочка, которую ловушка создаёт и очищает, — самая ясная форма владения, потому что очистка затем может удалить эту цепочку, не затрагивая чужую политику. Если вам приходится добавлять правила в общую цепочку, помечайте их комментарием, чтобы более поздний поиск находил именно ваши. Владение делает демонтаж безопасным, а демонтаж — это та часть, которая тихо отказывает.
iptables -N VPN_HOOK 2>/dev/null || true
iptables -C INPUT -j VPN_HOOK 2>/dev/null || iptables -I INPUT -j VPN_HOOK
iptables -S VPN_HOOK
Комментарий здесь не украшение. Это разница между удалением своего правила и удалением правила, которое случайно выглядит похожим, и он сохраняется в сохранённом наборе правил. Напишите комментарий один раз в переменную и используйте ту же строку в каждой команде, которая добавляет, проверяет или удаляет.
Проверяйте желаемое состояние, а не добавляйте снова
Запишите, что должно существовать, пока туннель поднят: какие интерфейсы, какой источник и назначение, какой протокол и какой порт. Затем выразите каждое правило как проверку, за которой следует вставка, чтобы второй запуск нашёл правило и ничего не сделал. Команда проверки завершается нулём, когда правило существует, и это именно тот сигнал, который нужен скрипту. Справочник по iptables документирует используемые здесь формы проверки, добавления и вставки.
iptables -C VPN_HOOK -i tun0 -p tcp --dport 443 -j ACCEPT ||
iptables -A VPN_HOOK -i tun0 -p tcp --dport 443 -j ACCEPT
Будьте осторожны с позицией. Проверка правила и вставка его в начало может сдвинуть разрешение перед намеренным запретом, который раньше шёл первым, поэтому второй запуск меняет поведение, хотя ничего не добавляет. Добавляйте в конец внутри своей собственной цепочки и управляйте порядком через место перехода в эту цепочку. Если порядок внутри цепочки важен, пересоберите цепочку из известного списка, а не латайте её правило за правилом.
Остановитесь, если нужное правило нельзя выразить как стабильную проверку. Обычно это значит, что желаемое состояние всё ещё расплывчато.
Сериализуйте изменение, а не надейтесь, что оно удержится
Два вызова ловушки могут перекрываться, и оба могут увидеть отсутствующее правило до того, как один из них его добавит. Параметр ожидания xtables заставляет команду ждать блокировку, но не делает последовательность «проверить, затем добавить» атомарной в рамках двух команд. Сериализуйте всю операцию собственной блокировкой или используйте менеджер, применяющий объявленную партию как одну транзакцию. nftables умеет последнее, и это одна из причин предпочитать его на хостах, где он доступен.
flock /run/vpn-hook.lock /usr/local/sbin/vpn-firewall-apply
nft -f /etc/nftables/vpn-hook.nft
Какой бы механизм вы ни выбрали, держите критическую секцию короткой и делайте её повторно входимой: ловушка, которая срывается на полпути, должна быть безопасной для повторного запуска. Записывайте результат, чтобы повторный запуск был виден в журнале, а не выводился из количества правил. Если две разные службы надзора могут вызывать ловушку, сначала исправьте это; никакая блокировка не исправляет двух владельцев.
Демонтируйте только то, что принадлежит вам
Демонтаж должен отличать соединение-замену от того, которое он отправил на пенсию, и он должен пережить событие, которое никогда не получает аккуратной очистки. Удаляйте по тем же идентификаторам, которые использовали для добавления, и никогда не очищайте общую цепочку, чтобы упростить уборку. После жёсткой остановки запустите проход сверки при следующем старте, а не предполагайте, что нижняя ловушка сработала.
iptables -D VPN_HOOK -i tun0 -p tcp --dport 443 -j ACCEPT
iptables -F VPN_HOOK
ip -brief link show type tun
Время жизни интерфейса тоже часть проекта. Повторное подключение может создать устройство с другим именем, поэтому правило, жёстко задающее имя второго туннеля, устаревает, хотя всё ещё выглядит действительным. Предпочитайте идентичность, которую раскрывает само соединение, и перечитывайте её при каждом запуске. Проверьте три события по отдельности: повторное подключение, соединение-замену и выключение. Разрешённый трафик должен пережить все три без дубликатов, а посторонние службы должны остаться нетронутыми.
Проход заканчивается, когда у ловушки есть владелец, проверка для каждого добавляемого правила, один сериализованный путь и демонтаж, удаляющий только её собственное состояние. Ловушка, которую безопасно запускать дважды, ценнее ловушки, которая умна один раз.