앱은 연결 끊김이라고 하고, 메뉴 막대는 연결됨이라고 하며, 트래픽은 분명히 흐르고 있습니다. 이 차이는 재시도로 고칠 수 있는 경쟁 조건이 아닙니다. 앱과 터널은 별개의 프로세스이고, 코드가 읽는 객체는 시스템이 보관하는 구성의 캐시된 뷰일 뿐입니다. Apple의 Personal VPN 표면은 제어 API이지 데이터 경로가 아니며, 이상한 동작의 대부분은 이 한 가지 사실에서 나옵니다. 각 호출에서 실제로 무슨 일이 일어나는지 살펴보겠습니다.

관리자 객체는 캐시된 뷰이며 터널이 아니다

NETunnelProviderManager은 시스템이 소유한 환경설정 저장소에서 채워지므로, loadFromPreferences이 끝나기 전에는 그 속성이 아무 의미가 없습니다. 그 전에 connection.status을 읽으면 기본값을 얻게 되고, 그래서 재실행 후 첫 상태 새로 고침은 터널이 켜져 있어도 연결 끊김을 보고합니다. 먼저 불러오고, 같은 완료 핸들러에서 상태와 프로토콜 구성을 읽으세요. 이후 변화는 폴링 대신 NEVPNStatusDidChange으로 관찰하고, 알림이 On Demand가 시작한 연결에도 발생한다는 점을 기억하세요. 그 경우 아무것도 반환할 앱 호출이 없습니다. 프로토콜 설정도 마찬가지로, 앱이 넘긴 값이 아니라 불러온 구성에서 읽힙니다.

터널 자체는 Network Extension 공급자 프로세스에 살기 때문에, 앱은 연결을 잃지 않고 자기 상태를 잃을 수 있습니다. UI가 보여 주는 모든 것은 관리자에서 다시 만들어야 하며, 지난 세션에서 기억해서는 안 됩니다.

시작 순서: 저장, 불러오기, 그다음 시작

공급자 기반 연결에는 순서대로 세 단계가 필요합니다. 공급자 구성을 쓰고 saveToPreferences을 호출하고, 저장된 구성이 손에 있는 것이 되도록 관리자를 불러온 뒤 NETunnelProviderSession.startVPNTunnel()으로 시작하세요. 저장을 건너뛰면 시작할 것이 없고, 저장 전에 불러오면 관리자는 여전히 이전 구성을 반영합니다.

startVPNTunnel이 반환하는 불리언은 요청이 접수되었다는 것만 말합니다. 공급자 수준의 실패는 나중에 연결 끊김으로의 상태 변화로 도착하며, NEVPNStatusDidChange 알림으로 관찰합니다. 알림 핸들러는 loadFromPreferences 후에 connection.status을 다시 읽어야 합니다. 캐시된 값이 아직 예전 것일 수 있기 때문입니다. 요청 수준의 실패는 예외를 던지는 형태의 호출에서 즉시 돌아오거나 startVPNTunnelAndReturnError:의 오류 출력 매개변수로 돌아옵니다. 반환값을 결과로 취급하는 코드는 결코 성립하지 않은 연결을 보고합니다. 또 다른 조용한 실패는 설치된 확장과 일치하지 않는 공급자 번들 식별자입니다. 요청은 거부되고, 메시지 어디에도 식별자를 가리키는 내용이 없습니다. 식별자가 틀리면 공급자 로그 한 줄이 나오기 전에 요청이 보통 즉시 실패합니다.

시작 후 환경설정을 다시 불러오는 것이 상태 화면을 올바르게 만듭니다. 시스템이 구성을 바꿔도 관리자가 새 구성을 밀어 넣지는 않습니다.

신뢰 자료는 프로파일로 도착하며, 앱 코드에서 오지 않는다

앱에는 루트 인증서를 iOS 신뢰 저장소에 추가하는 지원되는 방법이 없습니다. 그래서 Personal VPN 구성은 기기가 이미 신뢰하는 자료만 참조할 수 있습니다. 설치된 프로파일, 기기 관리, 또는 시스템이 다른 이유로 신뢰하는 CA가 세운 체인입니다. 첫 실행 때 자체 CA를 만들고 시스템이 그것을 받아들일 것이라 기대하는 앱은 공급자 내부의 검증에서 실패하며, 그곳에서 앱 UI는 이유를 볼 수 없습니다.

그 경계를 있는 그대로의 설계 제약으로 다루고, 신뢰 앵커를 등록 프로파일과 함께 배포하세요. VPN 인증은 신뢰 결정과 분리해 두세요. 실패하는 위치가 다르고, 디버깅하는 사람도 다릅니다.

카운터는 인터페이스에서 오며, VPN API에서 오지 않는다

관리자는 상태와 구성을 노출할 뿐, 터널별 바이트를 세는 것은 없습니다. 사용량 수치가 필요하면 getifaddrs()을 호출하고 해당 인터페이스의 if_data을 읽으세요. Wi-Fi는 en0, 셀룰러는 pdp_ip 인터페이스, 터널 자체는 utun 장치입니다. utun 카운터는 내부 트래픽을 나타내며 프로토콜 오버헤드를 포함합니다.

이 수치는 추정치이며 앱별 회계가 아닙니다. 인터페이스가 다시 만들어지면 초기화됩니다. UI에서도 그렇게 표시하세요. 그렇지 않으면 누군가 고객에게 청구하는 데 씁니다. 앱별 수치가 필요하다면 전혀 다른 메커니즘이 필요합니다. 이 필드를 더 잘 읽는 것으로는 안 됩니다.

정리는 stopVPNTunnel에 속하며, 종료 처리에 속하지 않는다

applicationWillTerminate은 시스템이 프로세스를 죽일 수 있기까지 몇 초를 줍니다. 연결을 정리하기에 믿을 만한 곳이 아닙니다. 중지를 stopVPNTunnel()로 보내거나 공급자의 중지 콜백에서 처리하고, 시작할 때 쓴 관리자 또는 구성 식별자를 유지하세요. 그래야 중지가 기기의 모든 구성이 아니라 그 연결을 대상으로 합니다.

그 식별자는 속한 프로파일 옆에 저장하세요. 관리자 객체는 사용자가 어떤 구성을 선택했는지 기록하지 않습니다. 그리고 On Demand가 올린 연결에는 앱 호출이 전혀 없다는 점을 기억하세요. 그래서 상태 화면은 무엇을 그리기 전에 여전히 환경설정을 불러와야 합니다.

관련 글