VPN на Mac подключён, но ip-up так и не запустился: настройте наблюдение launchd
Туннели Cisco IPSec и IKEv2 никогда не запускают pppd, поэтому /etc/ppp/ip-up молчит. Следите за каталогом конфигурации и заставьте скрипт доказать туннель.
Вы кладёте команду route в /etc/ppp/ip-up, переподключаете офисный туннель, а файл так и не выполняется. Со скриптом всё в порядке: это соединение вообще не идёт через PPP. Встроенные клиенты Cisco IPSec и IKEv2 никогда не запускают pppd, поэтому каталог хуков не читается, хотя PPTP и L2TP поверх IPSec по-прежнему проходят через него. Определите свой случай, затем запустите не-PPP вариант из задачи наблюдения launchd.
Проверьте интерфейс, прежде чем править любой хук
Две команды решают, какой механизм может работать. Перечислите настроенные службы, затем перечислите интерфейсы, которые существуют, пока туннель поднят. Имя интерфейса — доказательство; надпись в Системных настройках — нет.
scutil --nc list
ifconfig -l | tr ' ' '\n' | grep -E '^(utun|ppp)[0-9]+$'
Служба, создающая ppp0, выполняет хуки PPP и запустит ваш файл. Служба Cisco IPSec или IKEv2 вместо этого создаёт устройство utun, туннельный интерфейс без слоя PPP, поэтому ничто внутри /etc/ppp до него не дотянется. Когда появляются оба вида, проверьте, к какой службе вы подключились. Люди теряют полдня на неправильном хуке, читая имя клиента.
На пути PPP файл должен принадлежать root и быть исполняемым, потому что pppd проверяет и то и другое, прежде чем что-либо запускать. install задаёт владельца и режим за один шаг.
sudo install -o root -g wheel -m 0755 /dev/null /etc/ppp/ip-up
sudo ls -l /etc/ppp/ip-up
Файл, созданный через Finder или графический редактор, часто оказывается во владении вашей учётной записи или без бита исполнения, а затем отказывает так же тихо, как отсутствовавший хук. Положите рядом ip-down сейчас. root и wheel в столбце владельца плюс rwxr-xr-x в столбце режима — это то, что нужно.
Пусть launchd запускает скрипт, когда появляется туннель
Для клиентов, которые никогда не касаются PPP, триггер должен приходить из другого места, и хранилище системной конфигурации — честный источник: macOS переписывает его при каждом переходе туннеля. Задача launchd, следящая за этим каталогом, вызывается, когда туннель появляется или исчезает.
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>WatchPaths</key>
<array><string>/Library/Preferences/SystemConfiguration</string></array>
<key>Label</key><string>net.example.tunnel-watch</string>
<key>ProgramArguments</key>
<array><string>/bin/sh</string><string>/usr/local/sbin/tunnel-hook</string></array>
<key>RunAtLoad</key><true/>
<key>ThrottleInterval</key><integer>20</integer>
</dict>
</plist>
Установите его как launch daemon, а не как агент для каждого пользователя, потому что починка маршрутов принадлежит машине. bootstrap заменяет старую форму load -w в текущих выпусках. RunAtLoad делает первый запуск наблюдаемым, а ThrottleInterval не даёт шквалу изменений подключения Wi-Fi запускать скрипт снова и снова.
sudo install -o root -g wheel -m 0644 tunnel-watch.plist \
/Library/LaunchDaemons/net.example.tunnel-watch.plist
sudo launchctl bootstrap system \
/Library/LaunchDaemons/net.example.tunnel-watch.plist
Ловушка в том, что наблюдаемый каталог меняется и от событий, не связанных с вашим VPN: подключение Wi-Fi, продление DHCP, вставленный кабель. Сторож ниже превращает шумный триггер в пригодный хук, а строка журнала объясняет маршрут в странный час.
Владение распространяется на plist так же, как на скрипт. Демон, выполняющий файл в каталоге, куда может писать ваша собственная учётная запись, отдаёт ей root.
Защитите скрипт, чтобы второй запуск ничего не менял
Скрипт сначала решает, существует ли туннель, и тихо выходит, когда нет. Всё после этого должно быть безопасно повторять, потому что триггер срабатывает гораздо чаще одного подключения. Определяйте интерфейс во время выполнения, а не вшивайте индекс, который изменится при следующем подключении.
#!/bin/sh
set -u
TUN=$(ifconfig -l | tr ' ' '\n' | grep -m1 '^utun[0-9]' || true)
[ -n "$TUN" ] || exit 0
netstat -rn -f inet | grep -q '^10\.42' && exit 0
route -n add -net 10.42.0.0/16 -interface "$TUN"
route add выходит с ненулевым статусом, если маршрут уже есть, поэтому скрипт, считающий любой ненулевой статус ошибкой, каждый раз, когда поступает правильно, сообщает о сломанном хуке. Тест с netstat выше превращает этот случай в явный no-op.
Наличие интерфейса — более слабое доказательство, чем кажется, потому что другие VPN-клиенты и некоторые среды контейнеров тоже создают устройства utun. Спросите саму службу через scutil --nc status "Office VPN" и требуйте подключённого состояния, прежде чем трогать маршрутизацию. Держите ответ в строке журнала, потому что через неделю эта запись станет единственным объяснением, которое у вас будет.
Верните шлюз, не сломав DNS
Когда туннель забирает маршрут по умолчанию, обычный просмотр уходит через офис, и локальный путь в интернет перестаёт работать. Сначала захватите маршрутизатор физического интерфейса, затем направьте маршрут по умолчанию обратно на него. DHCP лучше разбора таблицы, в которой уже есть собственный маршрут по умолчанию туннеля.
PHYS=en0
GW=$(ipconfig getoption "$PHYS" router)
[ -n "$GW" ] && sudo route -n change default "$GW"
Резолверы — отдельный слой с отдельным инструментом, и починка маршрута их не чинит. Посмотрите через scutil --dns, что действует, затем исправьте то, что это создаёт: порядок служб и записи DNS на физической службе. Очистка кеша диагностирует зависший запрос после смены сети; это не то, что стоит планировать.
Туннель, переживший смену Wi-Fi, но с застрявшим DNS, — третья неисправность: sudo killall -HUP mDNSResponder заставляет резолвер перечитать конфигурацию. Используйте это один раз, вручную.
Докажите, что задача срабатывает сама
Принудительно выполните один запуск через kickstart -k, прежде чем доверять пути наблюдения, затем прочитайте статус выхода, записанный задачей. Чистый выход доказывает, что скрипт работает; он не доказывает, что работает триггер. Затем отключитесь и подключитесь снова с загруженной задачей и следите за запуском, который вы не просили.
sudo launchctl kickstart -k system/net.example.tunnel-watch
sudo launchctl print system/net.example.tunnel-watch | grep -E 'last exit code|state ='
Проверяйте scutil --nc status до и после каждой попытки. Скрипт, отработавший при выключенном туннеле, ничего не доказывает о пути подключения.
Ветвь демонтажа заслуживает такого же обращения, как ветвь подключения: удалите маршрут вручную, дайте задаче отработать и подтвердите, что он возвращается. Хук, который добавляет маршрут и никогда его не удаляет, оставляет машину, которая через неделю ведёт себя неправильно, и ничто не связывает два события. Когда оба поведения держатся, sudo launchctl bootout system/net.example.tunnel-watch чисто снимает задачу. Оставленный загруженным демон наблюдения — это путь, которым разовая починка через месяцы становится необъяснённым маршрутом.