Како да исклучите подмрежа од Android VPN тунел
VpnService не може да исклучи рута, па затоа го додавате комплементот: осум генерирани префикси за /8, плус проверките за IPv6, DNS и пребарување што го фаќаат остатокот.
VpnService на Android нема рута за исклучување, и ниту едно читање на API нема да создаде таква. Builder.addRoute() секогаш додава само одредишта во тунелот, така што чувањето LAN сегмент локален значи дека тој сегмент никогаш не влегува во списокот со рути. Она што го додавате наместо тоа е комплементот на сегментот, изразен како обични префикси. Работата е аритметика, и таа пропаѓа на неколку предвидливи места.
Поминете ги проверките подолу по ред. Секоја претпоставува дека претходната важела, затоа запрете на првиот неуспех наместо да крпите симптоми подолу.
Започнете од она што тунелот го тврди
Предавањето на 0.0.0.0/0 на addRoute() му кажува на Android дека секое IPv4 одредиште припаѓа на тунелот. Затоа толку многу клиенти изгледаат неспособни за поделено тунелирање: тие го бараат целиот адресен простор и потоа се обидуваат да одземат од него. Ништо во градителот не одзема. Поконкретна рута за мрежата што сакавте да ја задржите локална го прави спротивното од очекуваното, бидејќи најдолгиот совпаѓачки префикс победува и таа мрежа завршува внатре во тунелот.
Списокот со рути е целата политика. Зад него нема вето по одредиште, и референцата Builder не наведува метод што би го обезбедил тоа.
Генерирајте го комплементот од аритметика на префикси
Комплементот на исклучениот префикс е множеството од неговите браќа и сестри на патот кон коренот на дрвото на адреси. За секоја должина од 1 до исклучената должина земете го блокот со таа должина што го содржи вашиот префикс, превртете го битот веднаш под префиксот и испечатете го резултатот. Звучи апстрактно додека не го извршите.
Исклучувањето на 10.0.0.0/8 дава осум рути: 128.0.0.0/1, 64.0.0.0/2, 32.0.0.0/3, 16.0.0.0/4, 0.0.0.0/5, 12.0.0.0/6, 8.0.0.0/7, 11.0.0.0/8. Ниту една од нив не содржи 10.0.0.0/8, а заедно покриваат сѐ друго.
Осум линии ја генерираат таа листа за кој било префикс:
import ipaddress
def complement(addr, length):
base = int(ipaddress.IPv4Address(addr))
out = []
for k in range(1, length + 1):
block = base & (0xFFFFFFFF << (32 - k))
sibling = block ^ (1 << (32 - k))
out.append(f"{ipaddress.IPv4Address(sibling)}/{k}")
return out
print(complement("10.0.0.0", 8))
Бројот на рути е еднаков на должината на префиксот, и тоа е бројот што треба да се следи. Едно исклучување /21 чини 21 рута и останува читливо. Десет исклучувања може да чинат неколку стотини, што обично значи дека треба да се смени дизајнот на мрежата, а не генераторот.
Проверете го семејството на адреси и обемот на тунелот
IPv4 рутите не прават ништо за IPv6. Ако тунелот бара IPv6 адреса, на IPv6 LAN префиксот му треба сопствен комплемент, генериран со истата аритметика над 128 бита. Честа причина зошто исклучувањето работи на хартија, а локалниот уред останува недостапен е оваа: до домаќинот се стигнало преку IPv6, а исклучен бил само IPv4. Проверете ги двете семејства пред да заклучите дека математиката е погрешна.
Обемот е другата половина од проверката. Android го следи рутирањето по мрежа и по апликација, така што истиот префикс може да оди низ тунелот за една апликација и директно за друга, зависно од тоа како е деклариран VPN-от.
Поставете ги правилата за DNS и по апликација таму каде што припаѓаат
Исклучувањата на рути не го менуваат резолверот. Локалното име на домаќин сѐ уште може да го одговори DNS серверот на тунелот додека неговата адреса се рутира локално, а несовпаѓањето се покажува како име што се разрешува во нешто до што LAN не може да стигне. Поставете ја политиката на резолверот намерно, со домен за пребарување или правило за поделен DNS. Не претпоставувајте дека рутите го решиле тоа за вас.
addDisallowedApplication() држи една апликација надвор од тунелот; не држи одредиште надвор за апликациите што остануваат внатре. protect() му дозволува на сокетот што ќе го отворите да го заобиколи тунелот, и така тунелот избегнува да го фати сопствениот транспорт. Ниту едното не го заменува комплементот.
Проверете со пребарување, не со сопствената листа
Кога тунелот е кренат, прашајте го уредот каде адресата навистина оди наместо повторно да ја читате листата што ја изградивте. На изградба што доставува iproute2, adb shell ip route get 10.1.2.3 го именува интерфејсот што го избрал јадрото. Извршете ја истата команда за јавна адреса и потврдете дека го именува тунелот.
Уште две проверки го затвораат циклусот. Прво разрешете локално име, потоа извршете adb shell ip route get на разрешената адреса и барајте интерфејс што не е тунелот, бидејќи успешниот adb shell ping -c 1 host.lan докажува достапност, а не патеката. Повторете го тестот на семејството на адреси над IPv6 ако тунелот бара едно. Тестирајте на нивото на API што го доставувате, бидејќи начинот на кој VPN мрежите им се изложени на табелата за рутирање се менувал меѓу изданијата. Список со рути што изгледа точно на хартија и пребарување што избира погрешен интерфејс се различни грешки, и само втората стигнува до корисниците.
Ако списокот со исклучувања продолжува да расте, престанете да генерирате рути и пренумерирајте ја оддалечената мрежа или наместо тоа стеснете го тунелот. Генераторот е заобиколување за API-то што недостасува, а не алатка за дизајн.