Android VpnService 허용 목록: 허용한 페이지가 여전히 멈추는 이유
허용 목록은 대개 정상입니다. 페이지가 멈추는 원인은 DNS 응답, 라우팅 테이블, 리졸버 소켓 또는 MTU입니다. 확인 순서는 이렇습니다.
도메인을 허용 목록에 넣었고 질의가 앱에 도달하며 매처가 허용을 반환하는데도 페이지는 끝내 로드되지 않습니다. 이 지점에서 대부분은 매처를 다시 작성하고 와일드카드 비교를 덧붙이기 시작합니다. 제 경험상 매처는 보통 정확하고, 실패는 한 계층 아래에 있습니다. 직접 합성한 DNS 응답, 설치한 라우트, 또는 질의를 전달하는 데 사용한 소켓입니다. 목록은 쉬운 부분이고, 허용 목록이 깨지는 곳은 패킷 경로입니다.
매처가 원인인 경우는 드뭅니다
먼저 구분할 것은 차단과 고장입니다. 차단된 이름은 빠르고 눈에 보이게 실패해야 합니다. 브라우저가 이름이 존재하지 않는다는 응답을 받고 멈추기 때문입니다. 고장 난 이름은 멈추고, IPv6로 재시도하고, 다른 전송으로 넘어가며, 사용자는 회전하는 표시만 보게 됩니다. 회전 표시가 보인다면 허용 목록이 클라이언트가 처리할 수 없는 무언가를 응답하고 있을 가능성이 큽니다.
빈 A 레코드가 전형적인 실수입니다. 응답이 하나도 없는 NOERROR는 유효한 응답처럼 보이므로 stub 리졸버가 받아들이고, 브라우저는 다시 시도하고, AAAA를 시도하고, 다른 리졸버를 시도한 뒤 훨씬 나중에 포기합니다. 응답 코드 3, 즉 NXDOMAIN을 질문 섹션을 그대로 돌려주고 트랜잭션 ID를 복사해 보내면 대화가 즉시 끝납니다. 의도적으로 거부하는 모든 이름에 이것을 쓰십시오.
IPv6도 기본값이 아니라 같은 결정을 받아야 합니다. A에 NXDOMAIN을 응답하고 AAAA를 통과시키면 클라이언트가 IPv6로 차단된 이름에 도달할 수 있어 필터가 새어 나갑니다. 일부 구현은 차단된 AAAA 질의에 loopback 주소로 응답해 대체 경로로 넘어가지 않고 즉시 연결이 실패하게 합니다. 하나의 정책을 골라 두 레코드 유형에 적용하고 둘 다 기록하십시오.
모든 것을 잡는 라우트는 모든 것을 소유합니다
DNS만 쓰는 설계는 addRoute("0.0.0.0", 0)를 결코 추가하지 않으므로 TUN 인터페이스에는 질의만 들어가고 나머지는 아래 네트워크를 계속 사용합니다. 이것이 안전한 형태이며, 어떤 허용 목록이 Wi-Fi에서는 동작하는 듯 보이고 셀룰러에서는 실패하는 이유이기도 합니다.
기본 라우트가 존재하는 순간 모든 흐름은 당신의 것입니다. 인터페이스에 도착했지만 전달되지도 응답되지도 않는 TCP 패킷은 클라이언트가 시간 초과할 때까지 큐에 머물고, 아무것도 실패하지 않았으므로 기록할 오류도 없습니다. 누군가 고장 난 허용 목록에 오후 내내 매달렸는데 실제로는 포트 443용 포워더가 없었던 일을 본 적이 있습니다. 버리기로 결정한 모든 패킷의 프로토콜과 목적지를 기록하십시오.
리졸버 소켓은 터널 밖으로 나가야 합니다
전달이 다음 함정의 자리입니다. 통신사 리졸버는 터널이 닿지 않는 사설 주소에 있는 경우가 많아, 전달한 질의가 아무 데도 가지 않고 앱은 읽기 시간 초과를 보고합니다. 승인된 질의는 접근 가능한 것으로 아는 리졸버로 보내고, 그 소켓을 먼저 VpnService.protect()에 넘기십시오. 보호되지 않은 소켓은 질의를 자신의 터널로 되돌려 보내고, 거기서 다시 앱에 도착해 시간 초과가 날 때까지 맴돕니다.
실패는 네트워크 장애와 똑같이 보이며, 그래서 버그 보고서에서 그렇게 오래 살아남습니다. 보낸 질의와 받은 응답에 대한 카운터를 두십시오. 첫 번째 숫자가 오르고 두 번째가 영에 머문다면 목록이 아니라 소켓을 보고 있는 것입니다.
크기와 체크섬은 메시지 없이 실패합니다
조용한 폐기는 크기에서도 옵니다. 큰 응답 집합을 가진 DNS 응답이나 넉넉한 옵션의 TCP 핸드셰이크는 명시적으로 설정한 적 없는 경로 MTU를 넘을 수 있습니다. 단편은 상류에서 사라지고 클라이언트는 더 작게 재시도하거나 재시도를 포기합니다. 인터페이스 MTU를 1280으로 설정하는 것은 대부분의 셀룰러 경로를 견디는 보수적 선택이며, 아무도 측정하지 않은 기본값을 물려받는 것보다 명시하는 편이 낫습니다.
패킷을 직접 만든다면 체크섬이 또 다른 조용한 살해자입니다. UDP 길이나 프로토콜 필드를 빼먹은 의사 헤더로 계산한 UDP 체크섬은 당신 쪽에서는 올바르게 보이지만 수신자에서 흔적 없이 버려지는 패킷을 만듭니다. 의사 헤더를 명세와 필드별로 비교하고, 디버깅하는 동안 알려진 흐름 하나에 대해 계산된 체크섬을 출력하십시오.
VpnService가 대신 해 주지 않는 일
주변을 설계하기 전에 알아 둘 두 가지 한계가 있습니다. 어떤 일반 앱도 Wi-Fi 밖에서 시스템 전체 프록시나 PAC 파일을 설치할 수 없고, 그 부분은 바뀌지 않았습니다. 앱별 경로는 바뀌었습니다. API 29 이전에는 VpnService.Builder가 프록시를 전혀 실을 수 없었고, API 29부터는 setHttpProxy(ProxyInfo.buildPacProxy(uri))가 VPN 네트워크에 대해 평가할 PAC 스크립트를 플랫폼에 넘깁니다. 오래된 API를 대상으로 하거나 PAC 모델이 규칙을 표현할 수 없을 때는 자체 코드에서 규칙을 평가하십시오. UDP도 공짜가 아닙니다. 브라우저는 SOCKS 프록시를 통해 TCP만 보내고, 대부분의 SOCKS5 클라이언트는 UDP ASSOCIATE를 구현하지 않으므로 프록시 기반 설계는 TCP가 멀쩡해 보이는 동안 UDP를 버리거나 흘립니다.
두 번째 한계가 UDP 기반 프로토콜이 일반 네트워크에서는 동작하고 VPN이 연결되는 순간 실패하는 이유입니다. 터널이 UDP를 실어야 한다면 포워더는 소켓 유형만 다른 TCP 경로가 아니라 그에 대한 포워딩 경로를 구현해야 합니다.
이 순서로 확인하십시오
목록을 순서대로 진행하고 처음 실패하는 단계에서 멈추십시오. 앞 단계가 버티지 못하면 뒤의 모든 검사는 잡음일 뿐입니다.
- 설치된 라우트를 나열하고 DNS가 아닌 패킷이 TUN 인터페이스에 아예 들어오는지 확인하십시오.
- 차단된 이름 하나와 승인된 이름 하나에 대해 질의 이름, 결정, 응답 코드를 기록하십시오.
- 리졸버 소켓이 보호되는지, 선택한 리졸버가 터널 안에서 응답하는지 확인하십시오.
- 인터페이스 MTU와 직접 만든 응답 하나의 체크섬을 확인하십시오.
- 같은 페이지를 Wi-Fi와 셀룰러에서 반복하십시오. 통신사 리졸버와 경로 MTU가 다르기 때문입니다.
다섯 가지가 모두 유지되는데도 페이지가 멈춘다면 다음 용의자는 포워더 자체입니다. 캡처한 요청과 캡처한 응답을 비교하고 빠진 바이트 수나 반쯤 닫힌 소켓을 찾으십시오. VpnService.Builder 참조는 라우트, DNS, MTU 계약을 다루며, protect()는 builder 옵션이 아니라 별도의 VpnService 호출입니다. 연결된 인터페이스는 첫 번째 계약이 충족되었다는 뜻일 뿐입니다.