VPN на Android работает по IPv6, но браузер по-прежнему выбирает IPv4
Пройдите четыре этапа, на которых исчезает частный адрес IPv6: резолвер, объявление VpnService, проверка кандидатов браузером и доставка маршрута.
VPN поднят, частный веб-интерфейс отвечает по IPv6 с ноутбука, а телефон на Android по-прежнему открывает ту же страницу по IPv4 или вообще отказывается её загружать. Запись AAAA существует. Туннель сообщает адрес IPv6. В клиенте ничего не выглядит неправильно. Исход определяют четыре отдельных решения: какой резолвер ответил, что VpnService объявил Android, какое семейство адресов браузер был готов попробовать и может ли пакет пройти путь туда и обратно. Разбирайте их в этом порядке, а не добавляйте маршрут по умолчанию в надежде, что симптом сдвинется.
Шаг 1: докажите, какой резолвер ответил на запрос AAAA
У Android нет одного DNS-пути. VPN может протолкнуть DNS-сервер для своей сети, система может использовать Private DNS для имён узлов, которые сочтёт подходящими, а Chrome может запускать собственный зашифрованный резолвер из chrome://settings/security. На телефоне, где туннель проталкивает 10.61.0.1, но Private DNS указывает на публичного провайдера, имя внутри вашей частной зоны разрешает не та машина, которая владеет зоной. Браузер получает ответ от резолвера без полномочий на это имя и считает его отсутствующим, а текст ошибки никогда не упоминает DNS.
Спросите резолвер туннеля напрямую, прежде чем менять что-либо ещё. Первые две команды показывают, какие DNS-серверы Android привязал к активной сети и в каком режиме Private DNS — автоматическом или строгом; третья выполняется с любого клиента внутри туннеля, а не с телефона.
adb shell dumpsys connectivity | grep -A3 "DnsAddresses"
adb shell settings get global private_dns_mode
nslookup -type=AAAA nas.lan 10.61.0.1
Шаг 2: прочитайте, что VpnService действительно объявил
Сторона Android явная, а не выведенная. VpnService.Builder добавляет адреса через addAddress, маршруты через addRoute, DNS-серверы через addDnsServer и может ограничить весь туннель одним семейством адресов через allowFamily. Строитель, который вызвал allowFamily(AF_INET), а позже добавил маршрут IPv6, сообщил системе две противоречащие вещи, и система сохраняет именно ограничение семейства. Маршрут, покрывающий fd00:61::/64, вместе с адресом интерфейса в другом префиксе не работает по сходной причине: адрес существует, но он не on-link для этого маршрута.
Читайте объявление, а не доверяйте переключателю в приложении. dumpsys netd печатает адреса и маршруты, привязанные к интерфейсу tun, а LinkProperties по каждой сети внутри dumpsys connectivity показывает список DNS рядом с семействами, которые сеть заявляет.
adb shell ip -6 addr show tun0
adb shell ip -6 route show table all | grep tun0
adb shell dumpsys connectivity | grep -E "LinkAddresses|Routes"
Если у интерфейса tun есть адрес IPv6, но ни один маршрут не соответствует префиксу службы, доставка IPv6 уже сломана до того, как в дело вступит браузер. Это другая ошибка, чем у браузера, который отказывается пробовать, и она требует другого исправления.
Шаг 3: браузер решает, пытаться ли вообще использовать IPv6
Chromium использует не каждую полученную запись AAAA. Его резолвер узлов несёт проверки доступности IPv6 и фильтрацию кандидатов, которые могут отбросить IPv6 или понизить его приоритет, когда сеть признана неспособной его нести, а исходный код резолвера показывает эту проверку вместе с окружающими условиями. Логика зависит от версии и флагов, и именно поэтому два телефона в одном туннеле могут вести себя по-разному. Как только кандидаты появились, Happy Eyeballs устраивает гонку семейств для каждого соединения, поэтому медленно отвечающий путь IPv6 всё ещё может проиграть быстро отвечающему IPv4.
Наблюдайте за решением, а не выводите его. Сеанс, записанный через chrome://net-export и воспроизведённый в просмотрщике netlog, показывает каждое разрешение и выбранное семейство; chrome://net-internals/#dns показывает результаты из кеша. Разрешение, возвращающее только записи A для имени, у которого есть AAAA на вашем собственном резолвере, прямо указывает на этот этап.
Шаг 4: убедитесь, что пакет может пройти путь туда и обратно
Выбор адреса — это не доставка. Android держит таблицы маршрутизации и правила политики по каждой сети, которые выбирают таблицу по fwmark, поэтому маршрут, видимый в основной таблице, не обязательно тот, который использует сокет. Чтение правил и просьба к ядру разрешить назначение для адреса источника разделяют эти две вещи.
adb shell ip -6 rule show
adb shell ip -6 route get fd00:61::20 from fd00:61::10
Именно вывод route get отвечает на вопрос. Если он называет tun0, а пакеты всё равно гибнут, переходите к MTU туннеля и пересылке IPv6 на шлюзе, а не назад к телефону.
Почему добавление ::/0 обычно делает хуже
Добавление ::/0 меняет притязание туннеля с одного префикса на каждое назначение IPv6. На телефоне это может удовлетворить проверку доступности, потому что тестовый адрес вне вашего префикса теперь выглядит маршрутизируемым. Но это не создаёт пропускную способность IPv6 вверх по потоку на шлюзе, а у многих домашних шлюзов вообще нет работающего восходящего канала IPv6. Результат — чёрная дыра для публичного IPv6: имена, которые раньше откатывались на IPv4, теперь зависают, а частная страница может казаться лучше только потому, что проверка измеряет другое.
Есть и цена для политики. С ::/0 в туннеле весь трафик IPv6 выходит через VPN, включая трафик, который владелец сети никогда не соглашался нести. Относитесь к этому как к решению о развёртывании с названным ответственным, а не как к шагу отладки в 1 час ночи.
Порядок работы, который это завершает
Подтвердите, что ответ AAAA приходит от резолвера, владеющего зоной, прочитайте состояние VpnService и netd для интерфейса tun, снимите трассу net-export, чтобы увидеть, отбросил ли Chromium кандидата, и затем проверьте путь туда и обратно с помощью ip -6 route get. Исправьте тот уровень, который действительно отказал, и не трогайте политику разделения. Если частным именам нужны другие ответы, чем публичным, для этого и существует split tunneling.
Запишите сборку телефона, версию браузера и резолвер, который вы опрашивали. Эти три факта превращают "IPv6 is broken" в то, что следующий человек сможет воспроизвести.