Защо iOS VPN приложението ти показва „прекъснато“, докато тунелът е активен
iOS приложението ти управлява тунела, но не го притежава, а състоянието на мениджъра е кеширано. Първо зареди предпочитанията, после прочети статуса и броячите на интерфейса.
Приложението ти казва „прекъснато“, лентата с менюта казва „свързано“, а трафикът очевидно тече. Тази разлика не е състояние на състезание, което можеш да поправиш с повторен опит: приложението и тунелът са отделни процеси, а обектът, който кодът ти чете, е кеширан изглед на конфигурация, която системата съхранява. Personal VPN интерфейсът на Apple е API за управление, а не път за данни, и повечето от странностите му произтичат от този един факт. Ето какво всъщност се случва при всяко извикване.
Обектът на мениджъра е кеширан изглед, а не тунелът
NETunnelProviderManager се попълва от хранилище за предпочитания, което системата притежава, така че свойствата му не означават нищо, докато loadFromPreferences не завърши. Прочетеш ли connection.status преди това, получаваш стойност по подразбиране, поради което първото обновяване на статуса след повторно стартиране съобщава „прекъснато“, докато тунелът работи. Първо зареди, после прочети статуса и конфигурацията на протокола от същия манипулатор при завършване. Наблюдавай по-късните промени чрез NEVPNStatusDidChange вместо периодично запитване и отбележи, че нотификацията се задейства и за връзки, стартирани от On Demand, където няма извикване на приложението, което да върне каквото и да е. Същото важи и за настройките на протокола: те се четат от заредената конфигурация, а не от това, което приложението е подало.
Самият тунел живее в процес на доставчик на Network Extension, така че приложението може да загуби собственото си състояние, без да загуби връзката. Всичко, което интерфейсът показва, трябва да се възстанови от мениджъра, а не да се помни от последната сесия.
Ред на стартиране: запази, зареди, после стартирай
Връзката, базирана на доставчик, изисква три стъпки в този ред. Запиши конфигурацията на доставчика и извикай saveToPreferences, зареди мениджъра, така че запазената конфигурация да е тази, която държиш, после стартирай чрез NETunnelProviderSession.startVPNTunnel(). Ако пропуснеш запазването, стартът няма какво да пусне; ако заредиш преди запазването, мениджърът все още отразява старата конфигурация.
Булевата стойност, върната от startVPNTunnel, казва само, че заявката е приета. Отказите на ниво доставчик идват по-късно като промяна на статуса към „прекъснато“, която наблюдаваш чрез нотификацията NEVPNStatusDidChange, а манипулаторът на нотификацията трябва да прочете отново connection.status след loadFromPreferences, защото кешираната стойност може все още да е старата. Отказите на ниво заявка се връщат веднага от хвърлящата форма на извикването или чрез изходния параметър за грешка на startVPNTunnelAndReturnError:. Код, който приема върнатата стойност за резултат, съобщава за връзка, която така и не е установена. Другият тих отказ е идентификатор на пакет на доставчик, който не съвпада с инсталираното разширение: заявката се отхвърля и нищо в съобщението не сочи към идентификатора. Ако идентификаторът е грешен, заявката обикновено се проваля веднага, преди да се появи ред в лога на доставчика.
Презареждането на предпочитанията след старт е това, което прави екраните със статус коректни. Мениджърът не ти изпраща нова конфигурация, когато системата я промени.
Доверителният материал идва чрез профил, никога от кода на приложението
Приложението няма поддържан начин да добави коренов сертификат към хранилището за доверие на iOS, така че конфигурацията за Personal VPN може да реферира само материал, на който устройството вече се доверява: верига, установена от инсталиран профил, от управление на устройства или от CA, на който системата се доверява по други причини. Приложение, което създава собствен CA при първото стартиране и очаква системата да го признае, ще се провали при валидацията вътре в доставчика, където интерфейсът на приложението не може да види причината.
Приемай тази граница като дизайнерско ограничение, каквото е, и доставяй котвата за доверие заедно с профила за записване. Дръж автентикацията на VPN отделно от решението за доверие, защото те се провалят на различни места и се диагностицират от различни хора.
Броячите идват от интерфейса, не от VPN API
Мениджърът показва статус и конфигурация и нищо, което брои байтове за тунел. Ако ти трябват числа за използване, извикай getifaddrs() и прочети if_data за съответния интерфейс: en0 за Wi-Fi, интерфейсите pdp_ip за клетъчна мрежа и устройство utun за самия тунел. Броячите на utun описват вътрешния трафик и включват протоколната служебна информация.
Тези числа са оценки, а не отчетност по приложения и се нулират, когато интерфейсът се пресъздаде. Етикетирай ги така в интерфейса, иначе някой ще ги използва, за да таксува клиент. Ако ти трябват числа по приложения, ти трябва напълно различен механизъм, а не по-добро четене на тези полета.
Почистването е в stopVPNTunnel, а не при прекратяване
applicationWillTerminate ти дава няколко секунди, преди системата да убие процеса, което не е надеждно място за разрушаване на връзка. Изпрати спирането към stopVPNTunnel() или го обработи в обратното извикване за спиране на доставчика, и запази идентификатора на мениджъра или конфигурацията, който си стартирал, за да се насочи спирането към тази връзка, а не към всяка конфигурация на устройството.
Съхрани този идентификатор заедно с профила, към който принадлежи, тъй като обектът на мениджъра не записва коя конфигурация е избрал потребителят. И помни, че връзка, вдигната от On Demand, няма извикване на приложение зад себе си, така че екранът със статус все още трябва да зареди предпочитания, преди да нарисува каквото и да е.