VpnService protect() of app-uitsluiting: wat lost het routeren op
Bescherm de socket die de tunnel draagt, of sluit de hele app uit: de drie voorwaarden die bepalen welke reikwijdte je Android-VPN nodig heeft.
Je hebt twee manieren om de eigen pakketten van de tunnel buiten de tunnel te houden: de transportsocket beschermen, of de hele app uit de VPN halen. Ze lossen verschillende problemen op, en de bredere kiezen omdat die makkelijker aan te sluiten is, is precies hoe apps zonder het te merken toegang tot het lokale netwerk en gesplitste DNS verliezen. Begin bij wat buiten de tunnel moet blijven en kies dan de kleinste reikwijdte die dat bereikt.
Wat protect() uit de tunnel haalt
VpnService.protect() is een aanroep per socket. Je geeft hem de socket of descriptor die de tunnel draagt, en Android markeert die socket zodat het verkeer het onderliggende netwerk gebruikt in plaats van de VPN-interface. Al het andere dat de app verstuurt, volgt nog steeds de normale VPN-route.
Precies die smalle reikwijdte is het punt. Een beschermde socket verandert niet hoe je HTTP-client namen omzet, welke interface je analyse-aanroepen gebruiken of wat er gebeurt met een tweede verbinding die later wordt geopend.
Drie voorwaarden die de reikwijdte bepalen
De eerste voorwaarde is of alleen het transport van de tunnel naar buiten moet. Als het overige verkeer van de app nog steeds door de VPN moet, is socketbescherming de juiste reikwijdte. De tweede voorwaarde is of je het aanmaken van de socket beheert. Een bibliotheek die diep van binnen zijn eigen socket opent, laat je geen plek om protect() aan te roepen, tenzij die een hook biedt of een kant-en-klare socket accepteert. De derde voorwaarde is of de app het lokale netwerk of een captive portal rechtstreeks moet bereiken, wat een andere eis is dan de tunnel in leven houden.
De app uitsluiten met addDisallowedApplication() is het botte gereedschap. Het haalt elke verbinding die de app maakt uit de VPN, ook degene die je wilde inspecteren, en op sommige apparaten verandert het ook hoe lokale ontdekking zich gedraagt.
Een derde optie is het transport aan een gekozen netwerk binden met bindProcessToNetwork() of een netwerkhandle op socketniveau. Dat is nuttig wanneer het apparaat tegelijk Wi-Fi en mobiel heeft en je wilt dat de tunnel via een specifieke uplink loopt.
Roep het aan terwijl het transport wordt gemaakt
Roep protect() aan in hetzelfde blok dat het transport maakt, vóór connect() of send(). Als de aanroep false teruggeeft, sluit dan de socket en laat de opzet mislukken; doorgaan laat je achter met een transport waarvan de pakketten weer in je eigen interface kunnen belanden.
De vorm is klein genoeg om op één plek te houden:
DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
transport.close();
throw new IOException("transport not protected");
}
transport.connect(serverAddress);Drie details bijten hier. Ten eerste,de boolean is het enige signaal dat je krijgt, dus log de fout met genoeg context om een ingetrokken toestemming van een geweigerde aanroep te onderscheiden. Ten tweede,protect() dekt de socket, niet het descriptornummer: als je de socket sluit en later hetzelfde gehele getal voor een andere socket hergebruikt, is de nieuwe socket niet beschermd. Ten derde, wanneer een native bibliotheek het transport opent, bescherm dan de descriptor voordat de native code verbindt, anders is de route misschien al gekozen.De VpnService-referentie is de plek om te bevestigen welke overloads je doel-API-niveau werkelijk biedt.
Test het geblokkeerde pad, niet alleen het gelukkige
Verifieer met de tunnel omhoog, want dat is de enige toestand waarin de twee paden kunnen verschillen. Stuur één verzoek via het beschermde transport en één vanaf een codepad dat nooit protect() heeft aangeroepen, en vergelijk dan wat de verre kant ziet: de beschermde verbinding moet aankomen met het onderliggende netwerk als bron, terwijl de onbeschermde het eigen adres van de tunnel toont en de byte-tellers van de tunnel verzet. Op het apparaat laat ss of netstat dezelfde splitsing zien voordat er iets de radio verlaat.
Test intrekking apart, want een wijziging van toestemming breekt de interface af zonder je socketcode aan te raken. Trek de VPN-toestemming in of stop de dienst, bouw het transport opnieuw op en bevestig dat protect() nog steeds true teruggeeft en dat het transport opnieuw verbindt zodra de toestemming weer wordt gegeven.
Als het beschermde verzoek toch de tunnel volgt, gebeurt de beschermingsaanroep waarschijnlijk nadat de socket al is gerouteerd. Verplaats hem naar voren en test opnieuw.
Houd de beslissing opgeschreven naast de transportopzet: welke sockets beschermd zijn, welk appverkeer is uitgesloten en wat de app doet als bescherming faalt. Juist die regel weerhoudt een latere refactor ervan de uitsluiting naar de hele app te verbreden omdat één test faalde. Hij geeft de volgende persoon ook een manier om de bedoeling te controleren zonder het manifest terug te ontwerpen.