휴대폰에서 chroot 아래에 리눅스 배포판을 돌리고 그 안에 VPN 클라이언트를 설치하면 셸이 터널이 완성되었다고 알린다. 그런데 브라우저로 넘어가면 아무 일도 없었던 것처럼 트래픽이 모바일 데이터로 나간다. 아니면 반대로, 클라이언트가 Android가 실제로 쓰는 네임스페이스에 기본 경로를 넣었기 때문에 휴대폰의 모든 앱이 연결을 잃는다. 이 두 증상은 같은 빠진 조각에서 나오며 어느 쪽도 VPN 클라이언트의 버그가 아니다.

셸은 연결됐다 하고, 앱은 동의하지 않는다

첫 번째 결과가 흔하다. chroot에서 연 사용자 공간 터널은 핸드셰이크를 마치고 통계를 출력하며 같은 셸이 만드는 트래픽을 실어 나를 수 있지만, Android 앱은 휴대폰의 기본 네트워크를 계속 쓴다. 아무것도 가로채지 않는데, VPN이 있다고 Android에 알린 것이 없기 때문이다.

두 번째 결과는 위험하다. chroot가 Android의 네트워크 네임스페이스를 공유하면 기본값이 그렇다. 이때 충분한 권한을 가진 프로세스가 휴대폰 전체에 영향을 주는 경로와 TUN 인터페이스를 추가할 수 있다. 그러면 의도하지 않은 앱까지 모든 앱에서 터널이 동작하고, 휴대폰의 연결은 수명 주기 관리도 연결 표시도 상시 연결 정책도 없는 데몬에 의존하게 된다.

둘 중 어느 쪽인지는 명령 하나로 판별된다. chroot 안의 ip route를 바깥 Android 셸에서 같은 명령과 비교하라. 출력이 같으면 chroot가 호스트 네임스페이스에 있다는 뜻이고, 그 네트워크 변경은 휴대폰 전체에 적용된다. 출력이 다르면 런처가 전용 네임스페이스를 만든 것이며, 그 터널은 구조상 Android에 보이지 않는다.

chroot가 바꾼 것과 바꾸지 않은 것

ObjectEffect of a chroot
Pathname resolutionChanged: lookups start at the new root
Kernel and driversUnchanged: the phone's kernel, its TUN driver, its limits
Network namespaceUnchanged unless the launcher creates one
Android VPN registrationUnchanged: no app consented, no VPN is selected

chroot 시스템 호출이 하는 일은 하나다. 경로 해석에 쓰는 디렉터리를 바꾼다. man 페이지는 이것이 보안 경계가 아니라고 분명히 말하며 인터페이스, 경로, VPN 상태에 대해서는 아무 말도 하지 않는다. 사람들이 리눅스 컨테이너에 기대하는 것, 곧 전용 네트워크 스택, 필터된 파일 시스템, 자원 한도는 런처가 썼는지 알 수 없는 다른 메커니즘에서 온다.

그래서 chroot가 VPN을 격리한다는 주장은 대개 틀린 질문에 답한다. 물어야 할 것은 그 프로세스가 어느 네임스페이스에서 돌고 어느 SELinux 도메인에 갇혀 있는가이며, 둘 다 어떻게 시작되었는지의 성질이지 chroot라는 단어의 성질이 아니다.

TUN에는 세 가지가 필요하고, 장치 노드는 그중 하나일 뿐이다

전통적인 사용자 공간 터널에는 열 수 있는 TUN 장치, 그 장치에 대한 커널 지원, 인터페이스와 경로를 설정할 권한이 필요하다. 이것들은 별개의 요구이며 /dev 아래에 경로 이름을 만드는 것만으로는 어느 것도 충족되지 않는다.

ls -l /dev/net/tun
ip tuntap add dev tun0 mode tun

Android의 커널은 보통 TUN 지원을 포함한다. 플랫폼 자체의 VPN 서비스가 쓰기 때문이다. 하지만 그것이 당신의 프로세스가 써도 된다는 뜻은 아니다. 인터페이스 설정에는 네트워크 관리 권한이 필요하고, 장치 노드에 닿을 수 있는지는 SELinux가 결정한다. 거부는 원인을 알 수 없는 권한 오류처럼 보이며 답은 감사 로그에 있다.

dmesg | grep avc
logcat -d | grep "avc: denied"

root로도 해결되지 않는다. SELinux는 uid 0 프로세스도 다른 프로세스와 똑같이 제약하므로, root 셸에서 시작한 chroot도 보이는 장치에 대해 거부당할 수 있다. 오래된 설치 프로그램을 돌리려고 강제를 끄는 것은 터널보다 훨씬 멀리 영향을 미치는 휴대폰 전체의 결정이며 낡은 바이너리의 수정이 아니다.

Android 앱이 당신의 터널을 무시하는 이유

Android가 지원하는 모델은 플랫폼의 VPN 서비스에 등록하고 사용자에게 한 번 동의를 구한 뒤 자기 터널의 파일 디스크립터를 받는 VPN 앱이다. 그다음 플랫폼이 트래픽을 그 디스크립터로 보내고 사용자나 관리자가 고른 앱 범위를 적용한다. 그래서 같은 메커니즘이 앱별, 프로필별로, 그리고 상시 연결과 VPN 없이 차단 설정과 결합될 수 있다. Android VPN 개발 가이드가 그 핸드셰이크를 설명하며, 핵심은 이것이 등록이지 라우팅 요령이 아니라는 점이다.

chroot 안의 데몬은 결코 등록하지 않는다. 동의 대화상자도, 상태 표시줄의 열쇠 아이콘도, 앱별 포함과 제외 목록도, 상시 연결 정책 참여도 없다. 그래서 앱은 기본 네트워크를 계속 쓰고 터널은 셸 자신이 보내는 것만 실어 나른다.

반대 방향도 도움이 되지 않는다. 호스트 네임스페이스로 경로를 밀어 넣으면 플랫폼이 로컬에 두려던 트래픽까지 모두 잡고, 앱별 제어도 사용자가 보고 취소할 방법도 없다. chroot 데몬이 한 앱만을 위해 트래픽을 보내는 중간 설정은 없다.

바이너리 호환성은 별개의 벽

네트워크 쪽이 흥미로워도 실행 파일에는 자기 요구가 있다. CPU 아키텍처, 동적 로더, C 라이브러리, 그리고 기대하는 커널 기능이다. Android는 glibc가 아니라 bionic을 쓰고, 데스크톱 배포판용으로 빌드된 오래된 바이너리는 기기에 더는 없는 라이브러리를 요구할 수 있다.

라이브러리 파일 이름을 더 새로운 버전에 연결해도 ABI 호환성은 생기지 않고, 십 년 된 패키지에서 꺼낸 암호 라이브러리는 유지 관리되는 구성 요소가 아니다. 계획이 그것에 의존한다면 그 계획은 기기에서 네트워크 권한으로 관리되지 않는 코드를 돌리는 것이다.

Android 맥락에서 증명하고 뒤처리를 하라

정말 중요한 것을 중요한 쪽에서 시험하라. 특정 앱을 열고, 특정 사설 목적지에 도달하고, 어느 리졸버가 답했는지 확인한 뒤 터널을 끊고 문서화된 동작을 확인하라. chroot 안 터미널의 결과는 그 터미널의 실행 맥락에 대한 증거일 뿐이다.

실험 중 호스트 경로를 바꿨다면 되돌리고 무엇을 바꿨는지 기록하라. 그다음 휴대폰에 상시 연결 VPN이 설정되어 있는지 확인하라. 규칙을 남긴 오래된 실험은 몇 달 뒤 플랫폼 결함처럼 보일 수 있다.

실제 원격 접속이 필요하면 서비스가 지원하는 클라이언트나 승인된 게이트웨이 설계를 쓰라. 물을 가치가 있는 질문은 root 설치 프로그램을 돌릴 수 있느냐가 아니라, 지원되는 구현이 기기에 주인 없는 특권 변경을 남기지 않고 필요한 앱을 의도한 경계 너머로 옮기느냐이다.

관련 글