Я напоролся на глухой дедлок внутри NetworkExtention, которого НЕТ НИ В ОДНОЙ ДОКУМЕНТАЦИИ APPLE.
Раньше я уже писал пост про впн, который пишу, фейловер и пинг и остальное, а тут расскажу про баг, из-за которого всё это чуть не встало на паузу на дольше, чем хотелось бы признавать и это оказалось неприятно. Симптом я получил максимально простой и максимально бесячий: жмёшь «Подключить» — и приложение виснет на «Подключение…» навсегда. Крашей нет, ошибок нет, нормальных логов нет, есть только "Подключение..."
Когда расширение поднимает туннель через движок sing-box (он у меня в проекте живёт как отдельная библиотека, не процесс) — и в какой-то момент этому движку нужно попросить саму систему настроить сетевые параметры туннеля. Запрос уходит через официальный API Apple, тот самый, что по документации должен дёрнуть колбэк по готовности. Тот самый, ради которого тебе необходимо энроллить Developer Account.
Но вот загвоздка - колбэк не приходит. Никогда. Ни через секунду, ни через десять, ни через таймаут в отдельном потоке. Никогда. И это не «где-то потерялась ошибка» — это ровно то, что называется deadlock: система ждёт ответ на очереди, которая сама же должна была этот ответ доставить, а не может, потому что занята ожиданием себя же.
Нашёл это не логами (они здесь бесполезны — зависший поток просто ничего не пишет), а живым снятием стека прямо в момент зависания, на работающем процессе. Только так стало видно: колбэк должен прилетать на ту же самую системную очередь, на которой прямо сейчас всё стоит намертво в ожидании этого колбэка. Замкнутый круг, который никак не разорвать, перебирая очереди руками — а я перебрал их все, какие только можно было придумать.
Разрыв круга — не подбор правильной очереди (её не существует), а полный отказ от той версии API, которая требует колбэк вообще. У Apple есть вторая, современная версия того же самого запроса — без колбэков, через async/await, которая по-другому интегрируется с системой и не создаёт этой петли по построению. Пересадка на неё убрала проблему полностью, за один заход, кстати.
Отдельно то, что удивило: это не какой-то экзотический edge-case. Это стандартный путь настройки туннеля, который документация описывает как рабочий. Просто конкретно в моем контексте старый API Apple не может выполнить собственный контракт. По сути, баг внутри апи эпла!
Как именно на эту async-версию пересаживались — и почему это заняло дольше, чем один вечер — расскажу отдельно, наверное. А может и нет. Главное, что заработало.