Перейти к содержимому

Root-режим изнутри

Справочник к Root-режиму: каждый файл, правило, цепочка и таблица, которые пишут приложение и его загрузочный скрипт. Обоснования и замеры свёрнуты.

/data/adb/service.d/tailscaled.sh, его ставит Автозапуск Magisk/KernelSU службы (ЭКСПЕРИМЕНТАЛЬНО), запускает демона при загрузке так же, как это сделало бы приложение, — и ставит только T1 (почему).

Запуск на стадии late_start, от root (uid 0), силами Magisk, KernelSU или APatch
Читает /data/adb/tailsocks/control_proxy.env — его пишет приложение через su при каждом запуске Root-режима
Состояние профиль, записанный там как TAILSOCKS_STATE_DIR
Права сокет 0666, состояние 0700, лог 0644 (ротируется после 2 МБ)
Версия вторая строка, # tailsocks-boot-script: <версия> <сборка>. Другая версия или отсутствие строки (до 5.0.0) — пометка «устарел» у Переустановить скрипт
Env-файл, запасные варианты, хеш версии

Env-файл принадлежит root. Приложение переписывает его при каждом запуске Root-режима — и при собственном запуске, и при присоединении к демону, поднятому этим скриптом, — так что демон, запущенный при загрузке, тот же, что запустило бы приложение:

Группа Содержимое
Прокси канала управления ALL_PROXY, HTTP_PROXY, HTTPS_PROXY, заранее разрешённые TS_STATIC_HOSTS
Переменные демона TS_DNS_FALLBACK, TS_VPN_BYPASS, TS_SOCKS5_USER/TS_SOCKS5_PASS, TS_TAILDROP_DIR, переключатель Hostinfo, TS_USE_CACHED_NETMAP=false, если кэш карты сети выключен
Командная строка каталог состояния, адреса SOCKS5/HTTP-слушателей, режим туннеля (tailscale0 или userspace-networking)

Скрипт принимает только строки вида export NAME='value', строку без кавычек отбрасывает. Он никогда не исполняет ничего из каталога данных приложения, куда может писать само приложение или восстановленный бэкап. В файле есть строка-комментарий с пакетом и сборкой, которые его записали; версии формата нет.

Запасные каталоги состояния — на установке, где Root-режим ни разу не запускался из приложения: files/states/default, затем первый найденный tailscaled.state, затем states/root. При нескольких профилях однозначен только записанный.

Права. Сокет /data/data/io.github.bropines.tailscaled/files/tailscaled.sock получает 0666 благодаря патчу safesocket в ядре TailSocks, и скрипт ставит то же самое — те же права, что ставит приложение, запуская демона само.

Маршрутизация. Как только появляется tailscale0, скрипт ставит T1 для IPv4 и IPv6 — маскированный fwmark 0x1000000/0x1000000 → таблица 53, правила по адресу назначения для диапазонов tailnet, цепочку TAILSOCKS_MARK и пару правил FORWARD для tailscale0 — и удаляет остатки 3.5.x (старые версии).

Версия — первые 12 шестнадцатеричных цифр SHA-256 от скрипта в том виде, в каком его пишет эта сборка (шаблон, пакет, путь к демону и env-файл), так что она меняется с каждой правкой шаблона и с каждым переездом каталога установки. Настройки читают её через su.

Установить консольную утилиту Tailscale CLI (ЭКСПЕРИМЕНТАЛЬНО) добавляет команду tailscale для Termux, su- и adb-консоли и скриптов. Она запускает собственный libtailscale.so приложения и дописывает к каждой команде --socket=/data/data/io.github.bropines.tailscaled/files/tailscaled.sock.

Путь Что это
/data/adb/service.d/tailscale исполняемый запасной вариант
/data/adb/modules/tailscaled/system/bin/tailscale модуль Magisk, накрывающий /system/bin
/product/bin/tailscale оверлей раздела product на живой системе
/system/bin/tailscale прямая ссылка, если раздел доступен для записи
Один бинарник; собственные файлы отладочной сборки
  • Один бинарник. CLI встроен в бинарник демона (ts_include_cli). Обёртка выставляет TS_BE_CLI=1, без которого тот же файл поднял бы второго демона.
  • Отладочная сборка (io.github.bropines.tailscaled.dev) ставит свои root-файлы под именами с -dev — tailscale-dev в тех же четырёх местах (модуль tailscaled-dev), service.d/tailscaled-dev.sh, /data/adb/tailsocks-dev/ — и никогда не читает, не перезаписывает и не удаляет файлы релизного приложения. Общим у них остаётся набор правил ядра (таблица 53, цепочки, tailscale0, сохранённое значение rp_filter): держать tailscale0 в каждый момент может только один root-демон.

BootReceiver слушает ACTION_BOOT_COMPLETED, QUICKBOOT_POWERON и ACTION_MY_PACKAGE_REPLACED. При замене APK он заново ставит скрипт service.d и CLI-обёртку из ассетов приложения, так что путь к libtailscale.so в /data/app/… следует за новой версией.

При включённом Нативный Linux TUN (tailscale0) демон работает на настоящем интерфейсе ядра tailscale0, и слот VPN в Android остаётся свободным. Когда демон в состоянии Running, приложение ставит policy routing трёх уровней:

Уровень Из чего состоит На что претендует
T1 — доступность tailnet таблица 53 с защитой от чужого CGNAT, два правила priority 100, TAILSOCKS_MARK, пара правил FORWARD для tailscale0, публикация в /etc/hosts, локальные SOCKS5/HTTP-прокси пакеты на адреса tailnet
T2 — захват маршрута по умолчанию catch-all с priority 200 в таблицу демона 52, throw-маршруты для Исключенные подсети IP, ослабление rp_filter, исключения приложений с priority 190 всё, пока выбран выходной узел
T3 — DNS всего устройства TAILSOCKS_DNS и два её хука в nat OUTPUT каждый пакет на порт 53

T1 уживается с чем угодно. T2 и T3 забирают всё устройство, поэтому уступаются другому VPN-клиенту, который держит слот (три состояния). Root-режим — только клиент: в нём это устройство не может быть выходным узлом или subnet-роутером.

Как применяются правила

Всё живёт в именованных цепочках, так что правила идемпотентны, при каждом применении заменяются как единое целое и снимаются одним махом при остановке службы. Применение проверяет собственный результат (таблица заполнена, DNS-цепочка существует, когда должна), а не доверяет коду возврата шелла, и после трёх неудачных попыток сдаётся и сообщает об этом в лог.

Пакеты, которые устройство шлёт на адрес tailnet, уходят в tailscale0 через таблицу 53 — что бы ещё ни было установлено.

Элемент Что это
таблица 53 100.64.0.0/10 и fd7a:115c:a1e0::/48 через tailscale0
priority 100 ip rule … fwmark 0x1000000/0x1000000 table 53 — помеченные пакеты
priority 100 ip rule … to 100.64.0.0/10 iif lo table 53 — по адресу назначения
TAILSOCKS_MARK mangle, вызывается из OUTPUT для диапазонов tailnet: --set-xmark 0x1000000/0x1000000
защита от чужого CGNAT throw в таблице 53 для каждого префикса внутри 100.64.0.0/10, который держит другой интерфейс (оператор связи, tun другого VPN)
Зачем правило по адресу назначения, зачем iif lo, зачем маска
  • Правило по адресу назначения нужно для собственных сокетов демона. Метка ставится в mangle OUTPUT после того, как ядро уже выбрало адрес источника из таблицы Wi-Fi, так что без этого правила запросы демона к split-DNS-резолверу на пире уходили в tailscale0 с Wi-Fi-адресом источника и не получали ответа.
  • iif lo ограничивает правило пакетами, которые устройство порождает само. Пакет, просто проходящий через телефон (от клиента точки доступа, через subnet-роутер), остаётся на попечении правил Android, а не забирается нами и не получает ответ с нашего адреса источника. Правилу по метке такая оговорка не нужна: метка ставится только в mangle OUTPUT, куда транзитные пакеты не попадают.
  • Маска. Трогается лишь один старший бит. Android хранит в fwmark собственное решение о маршрутизации — netId в младших 16 битах, выше флаги explicit/protect/permission, — и затирание его, как делала «голая» метка старых версий, ломает маршрутизацию, когда сетью по умолчанию владеет другой VPN.
Защита от чужого CGNAT: когда срабатывает и как её увидеть

100.64.0.0/10 — это ещё и настоящее пространство операторского NAT (CGNAT). Оператор может выдать WAN-адрес оттуда, а другой VPN-клиент — поселить там свой tun. Правило priority 100 стоит выше любых исключений приложений, так что без этой защиты ничто на устройстве не могло бы вернуть такой трафик из туннеля. Upstream Tailscale закрывает тот же случай через HasCGNATInterface() в netmon; здесь тот же обход интерфейсов, но силами приложения. В логе ROOT она называется CGNAT guard.

  • Когда срабатывает. Интерфейс поднят, его имя не начинается с tailscale, и на нём есть IPv4-адрес внутри 100.64.0.0/10 с префиксом от /11 до /32. Только IPv4.
  • Что делает. throw <префикс> в таблице 53, по одному на префикс. Совпадение по самому длинному префиксу выбирает throw, а не /10, и поиск проваливается к правилам ниже наших: к catch-all priority 200, где он стоит, — так что пир tailnet внутри этого префикса по-прежнему достижим через свой /32 в таблице 52, — а иначе к правилам netd, которые маршрутизируют его так же, как до включения Root-режима.
  • Что отклоняется. Префикс /10 и короче накрыл бы сам tailnet. Такой не выбрасывается; лог ROOT называет интерфейс и говорит, что он остаётся захваченным.
  • Когда ставится. При каждом применении, в любом состоянии: защита — часть T1. Пересекающиеся префиксы входят в то, что приложение сравнивает перед применением, поэтому новый адрес (скажем, оператор выдал его, когда вы вышли из дома) или пропавший переустанавливает правила без перезапуска. Новые throw ставятся раньше, чем вычищаются устаревшие, так что в промежутке /10 этот трафик не накрывает. Если список интерфейсов прочитать не удалось, защита пропускается, и лог ROOT это говорит (CGNAT guard skipped).
  • Снимается при следующем применении, когда устарела, и вместе со всеми остальными throw в таблице 53 при остановке Root-режима. Загрузочный скрипт её не ставит: она приходит с первым применением правил приложением.
  • Где видно: в логе ROOT строкой CGNAT guard: <префикс> on <интерфейс> sits inside 100.64.0.0/10 without being the tailnet; …, в разделе таблицы 53 в Проверить маршрутизацию и командой su -c ip route show table 53.
Почему T1 не конфликтует с другим VPN

ip rule просматривается по возрастанию приоритета, так что priority 100 читается раньше любого правила, которое netd пишет для VPN-приложения. Это безопасно, потому что эти правила совпадают только с адресами tailnet, а вменяемый туннельный клиент диапазоны tailnet себе не забирает. Замер с настоящим клиентом (Exclave): он покрывает пространство IPv4 кусками по /8, но ставит 100.0.0.0/10 и 100.128.0.0/9 и намеренно не 100.64.0.0/10, так что tailnet остаётся доступен изнутри его туннеля без какой-либо настройки с обеих сторон.

Выходные узлы и маршруты подсетей живут в собственной таблице демона 52. Приложение добавляет одно правило, и непомеченные локальные пакеты сначала смотрят в таблицу 52:

200: from all fwmark 0x0/0x2020000 iif lo lookup 52

Вместе с ним каждый префикс из Исключенные подсети IP получает throw в таблице 52 и остаётся в реальной сети, пока включён выходной узел.

Исключенные подсети IP в Root-режиме
  • Где задаются. Настройки → Режим туннеля → VPN (TUN) → Исключенные подсети IP. Эта карточка видна только при выбранном режиме VPN; Root-режим берёт сохранённое там значение — по умолчанию 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12. Link-local, 169.254.0.0/16 и fe80::/10, добавляется всегда.
  • Что пишется. ip route replace throw <префикс> table 52 для каждого обычного CIDR из списка: IPv4 всегда, IPv6 — только если IPv6 на устройстве включён. Throw завершает поиск в таблице 52 и отдаёт его правилам netd, так что трафик к этому префиксу уходит по Wi-Fi или сотовой сети, а не через выходной узел. Разделять записи можно запятыми, точками с запятой, пробелами или переводами строки; запись, которая не является обычным CIDR, отбрасывается и называется в логе ROOT (exit-node exclusions ignored, not plain CIDRs: …).
  • Ограничения «Android 13+» здесь нет. Пометка под настройкой относится к режиму VPN, где маршруты исключаются через VpnService; Root-режим пишет маршруты ядра.
  • Когда ставятся. Только вместе с catch-all priority 200 — на всё устройство или суженным при частичной уступке. Throw переживают собственную перенастройку демона, поэтому пишутся один раз за применение, а не при каждом изменении карты сети.
  • Снимаются при уступке T2 — ровно префиксы из списка и никогда не throw, которые демон написал для себя (его throw для Доступ к локальной сети живут в той же таблице). При остановке Root-режима удаляются все IPv4-throw в таблице 52, кроме демонского 127.0.0.0/8, и все IPv6-throw.
  • Где видно: командой su -c ip route show table 52 и в Проверить маршрутизацию: её проверка маршрута до первого исключённого префикса отвечает tailscale0, если локальная сеть идёт через выходной узел.
Что делает catch-all безопасным
Защита Что делает
SO_MARK 0x2000000 Каждый сокет, который tailscaled открывает сам (WireGuard/disco UDP, DERP, control plane, STUN, DNS fallback), несёт бит 25 (патч 16 — вне собственных бит fwmark Android 0–20 и отдельно от бита 24 приложения). Такие пакеты минуют priority 200 и уходят в физическую сеть по правилам netd.
бит маски 0x20000 Бит protectedFromVpn Android — тот же приём, что netd использует в собственных VPN-правилах: сокеты, которые Android держит вне VPN (проверки доступности сети, MMS/IMS и собственные защищённые сокеты другого VPN-клиента), тоже минуют выходной узел.
iif lo только исходящие поиски; клиенты точки доступа остаются на правилах netd
ослабленный rp_filter Ответы на помеченные сокеты демона приходят по Wi-Fi или сотовой сети и проходят reverse-path-проверку с меткой 0, которая через таблицу 52 указывает на tailscale0, — строгий rp_filter (1) их бы отбросил. Если хоть один интерфейс в строгом режиме, all переводится в loose (2); прежнее значение сохраняется в /data/adb/tailsocks/rp_filter.orig и восстанавливается при очистке — или сразу, как только T2 отдан: без catch-all ослабление ни к чему.
перечитывание После установки правило перечитывается: бинарник ip, потерявший маску, оставил бы правило «совпадает со всем» — оно удаляется, и об этом сообщается.
Как доказательство читается из лога демона
  • Строка — одна непрерывная строка; перед ней стоят префикс Go с датой и временем YYYY/MM/DD HH:MM:SS и префикс компонента, например magicsock:.
  • «Текущий запуск». Демон дописывает в один и тот же файл от запуска к запуску и в этой сборке не печатает стартового баннера (ts_omit_logtail). Запуск начинается с более поздней из двух строк: TailSocks: daemon start, которую оба способа запуска — загрузочный скрипт и приложение — пишут прямо перед стартом демона, и собственной строки демона wgengine.NewUserspaceEngine(tun "tailscale0") ..., которую любое ядро печатает при каждом старте прямо перед созданием движка (а вместе с ним и пробы). Лог просматривается с конца до этого места, сколько бы демон ни проработал, и строка пробы засчитывается, только если ниже неё найдено начало запуска, — так что ни строка прежнего запуска, ни демон, перезапущенный вручную, не могут позаимствовать чужой вердикт.
  • Молчащий лог — это не «нет». Лог ротируется, и у демона, работающего не первый день, стартовые строки давно уехали, — на живом устройстве из-за этого выходной узел отключался по причине, никак не связанной с маршрутизацией. Если начала запуска нет или под ним нет строки пробы, TailSocks спрашивает сам работающий процесс и засчитывает его, если /proc/<pid>/exe указывает внутрь nativeLibraryDir приложения: бинарник, который мы собираем, помечает свои сокеты безусловно.
  • Явно отрицательный вердикт, записанный самим демоном, так не перебивается. Если нет ни одного из двух свидетелей, на priority 200 ничего не ставится; маршрутизация tailnet через таблицу 53 от этого не зависит.
  • Строки с пометкой TailSocks: пишут приложение и загрузочный скрипт — за строки демона они не считаются. Загрузочный скрипт эту проверку не читает вовсе: правило priority 200 он не ставит.
Почему никакая метка не убережёт приложения чужого VPN от выходного узла

netd ставит правила VPN-приложения на priority 13000 и 16000–17000. Когда в таблице 52 стоит маршрут по умолчанию, первый же поиск совпадает, и за выходным узлом идёт каждое приложение — включая участников чужого туннеля, — а до тех правил дело не доходит. Поэтому T2 и отдаётся, когда слотом владеет кто-то другой.

Бит protect покрывает туннельные сокеты другого клиента, но не приложения, которые он ведёт: пакеты обычного приложения бита protect не несут, а netId в их метке — 0. Расширение маски не помогает, и это измерено: ip route get 1.1.1.1 mark 0x84 uid <приложение-участник> по-прежнему отвечает dev tailscale0 table 52. Никакая метка не переспорит правило, которое читается раньше, поэтому уровень приходится отдавать или сужать по uid’ам, как при частичной уступке.

Что лежит в таблице 52 и что делает роутер демона на Android

В таблице 52 всё, что знает только демон: /32 пиров, принятые маршруты подсетей, 100.100.100.100/32, default dev tailscale0, пока выбран выходной узел, и throw-маршруты для префиксов локальной сети при включённом Доступ к локальной сети. Приложение добавляет туда свои throw для Исключенные подсети IP, а в остальном оставляет таблицу демону.

Роутер демона делает только то, что может только он: поднимает интерфейс, ставит адреса, пишет маршруты в таблицу 52. Включение и выключение выходного узла целиком обрабатывает его router.Set(), как на десктопном Linux: в приложении нет состояния выходного узла и нечего переделывать при переходе Wi-Fi ↔ сотовая сеть. Он не ставит ip-правила 52xx — таблицы main/default на Android пусты, так что десктопные правила либо зацикливают, либо роняют трафик — и вообще не трогает netfilter: метки Tailscale 0x40000/0x80000 совпадают с битами разрешений netd, а на части ядер нет типа nft-цепочки nat; отсюда и брался прежний health-warning router config failed.

TAILSOCKS_DNS (nat, вызывается из OUTPUT для порта 53 UDP/TCP, на позиции 1) перенаправляет DNS устройства на MagicDNS, 100.100.100.100. Нужны включённые Использовать MagicDNS (accept-dns) и Системный DNS через MagicDNS (Настройки → DNS); при полной уступке редирект снимается.

# Покидает цепочку до редиректа
1 netId чужого туннеля, при частичной уступке
2 исключённые приложения, по uid — это меньше, чем кажется
3 100.64.0.0/10: split-DNS-резолверы на пирах
4 upstream-резолверы демона, TS_DNS_FALLBACK: Резервные DNS или встроенные значения, если настройка пуста
Зачем эти условия и выходы
  • accept-dns. Перенаправить всё устройство на резолвер, который не отвечает, значит сломать DNS целиком.
  • Диапазон tailnet — чтобы split-DNS-резолверы на пирах были доступны напрямую.
  • Upstream-резолверы. Без этого выхода запросы самого резолвера перенаправляются обратно в него же, и ни одно внешнее имя не разрешается.
  • Запросы чужого туннеля уходят до DNAT. Хук стоит на позиции 1 в nat OUTPUT, то есть адрес назначения переписывается после того, как ядро выбрало адрес источника. Пока устройством владел другой туннель, запросы его участников приходили к нам с адресом источника того туннеля — это видно как поток netstack inject ts-service: src=100.100.100.100 dst=<адрес чужого туннеля> с частотой, достаточной, чтобы сработал ограничитель логов самого демона. RETURN, который их не пускает, сверяется с netId в fwmark, а не с uid.

Исключенные приложения (Настройки → Режим туннеля, тот же список, что в режиме VPN) получают по два правила на uid — и только пока стоит catch-all, дырой в котором они являются:

ip rule add uidrange 10234-10234 goto <наименьший приоритет netd> priority 190
iptables -t nat -A TAILSOCKS_DNS -m owner --uid-owner 10234 -j RETURN

Правило политики перепрыгивает через наш catch-all на правила netd: приложение уходит в туннель другого VPN или в физическую сеть. Адреса tailnet по-прежнему идут через tailscale0.

Что даёт исключение в зависимости от приложения
Исключённое приложение Его трафик Его DNS-запросы
обычное мимо туннеля по-прежнему MagicDNS, поэтому имена MagicDNS разрешаются
разрешает имена само — асинхронный резолвер Chrome, клиенты DoH/DoT, curl и всё остальное в Termux, приложения со своей библиотекой резолвера мимо туннеля системный резолвер; имена MagicDNS — только через публикацию в /etc/hosts, домены split DNS — никак

Системный DNS через MagicDNS — единственный переключатель, который покрывает трафик системного резолвера.

Тонкости: цель перехода, полная уступка, отцепленные правила, отсутствие xt_owner
  • Список при каждом применении превращается в uid’ы. Это тот же список, что режим VPN отдаёт в addDisallowedApplication: один список на оба режима. --uid-owner сравнивает uid процесса, который отправляет пакет.
  • Цель перехода читается с устройства: наименьший приоритет от 10000 и выше, на котором на этом телефоне действительно есть правило.
  • Почему переход, а не привязка. Привязка к физической таблице выводила приложение и из другого VPN — поэтому Chrome гас с DNS-ошибкой «bad config», а его туннельный клиент не видел ни одного запроса.
  • Правила priority 100 стоят выше всего этого, так что системе возвращается только маршрут по умолчанию приложения.
  • При частичной уступке исключения тоже ставятся: что catch-all захватил, то исключение может вернуть. При полной уступке — нет: захватывать нечего, а такое правило перепрыгнуло бы и через правила чужого клиента и вынуло бы приложение из туннеля, в котором оно должно быть.
  • Отцепленные правила. goto на приоритет, где нет правила, ядро принимает, печатает как [detached] и потом молча пропускает при поиске. Поэтому каждое правило перечитывается: отцепленное удаляется и заменяется правилом, привязывающим uid к физической таблице, а экран Логи говорит, что приложение не пойдёт и через другой VPN. Где невозможно даже это, приложение не исключается — оно остаётся в туннеле, а не повисает наполовину.
  • Пакет, которого уже нет на устройстве, пропускается и называется в логе ROOT. При пустом списке ничего не ставится.
  • Нет xt_owner. -m owner — необязательный модуль ядра. Если его нет, правила RETURN для DNS отклоняются, и лог ROOT это говорит; исключение из маршрутизации — правило политики, а не правило файрвола — не страдает. Приложение тогда оказывается вне туннеля, но разрешает имена внутри него, и та же строка лога это называет.

Android отдаёт слот VPN одному приложению, но наши правила priority 100 и 200 читаются раньше VPN-правил netd (13000 и 16000–17000 на телефоне автора). Поэтому T2 и T3 уступаются клиенту, который держит слот, — целиком или до тех приложений, которые он оставил снаружи.

Чего стоило забрать слишком много (замеры) и где сырые данные

Все числа в этом разделе сняты с устройства. Сырой снимок — собственные правила netd, uid’ы, netId’ы, интерфейсы и DNS-серверы вместе с командами, которыми они получены, — лежит в docs/research/foreign-vpn-measurements.md (на английском); принятые по нему решения и оставшиеся в дорожной карте пункты — в docs/research/root-exit-node-design.json.

Android никак не разбирает, что рутованное устройство делает ниже слота VPN: что бы мы ни забрали, мы забираем это первыми, включая трафик чужого туннеля. С настоящим клиентом — Exclave, com.github.dyhkwong.sagernet — и выбранным с нашей стороны выходным узлом:

  • Chrome, участник чужого туннеля, получал маршрут dev tailscale0 table 52. Его пакеты уходили в наш выходной узел, его собственный клиент их не видел, и браузер погас.
  • Каждый пакет на порт 53 переписывался в MagicDNS, включая запросы, которые системный резолвер делал для приложений того туннеля, — и приходили они с адресом источника чужого туннеля (T3).
Ситуация Что установлено Что работает
Один — другого VPN нет T1 + T2 + T3 всё
Полная уступка — другой VPN забирает все приложения T1 адреса tailnet, имена MagicDNS через /etc/hosts, локальные прокси. Не работают: системный DNS, выходной узел и маршруты подсетей мимо прокси
Частичная уступка — он пропускает часть приложений T1 + T2 для этих uid’ов + T3 без его сети выходной узел и маршруты подсетей для пропущенных приложений; MagicDNS везде, кроме его туннеля
Всё равно занять устройство включено T1 + T2 + T3 у нас всё; его приложения теряют трафик и DNS
Приложение показывает Полная уступка Частичная уступка
Главный экран Только tailnet Делим с другим VPN
Выходной узел не используется Выходной узел: частично
Здоровый priority 200 пуст по правилу на каждый пропущенный диапазон uid’ов

Ни одна уступка — не сбой, и обе пересматриваются, как только меняется другой VPN или влияющая на выбор настройка.

Состояния подробнее
  • Один — значит, работает всё: выходные узлы, принятые маршруты подсетей, системный MagicDNS, исключения приложений.
  • Полная уступка. Узел остаётся подключённым, и всё, что направлено в локальные SOCKS5/HTTP-прокси, уходит через выходной узел; выходного узла и принятых маршрутов подсетей нет только у приложений, которые не направлены в эти прокси. Чужой клиент сохраняет свои приложения и свой резолвер в целости. На главном экране вместо обычной строки Root написано Только tailnet, выбранный выходной узел показан как не используется, а не притворяется работающим, а Режим туннеля и DNS в настройках поясняют, что именно выключено и до каких пор.
  • Частичная уступка. Приложения, которые тот клиент отдал физической сети, идут через наш выходной узел и наши маршруты подсетей, а MagicDNS отвечает всему устройству — кроме запросов собственного туннеля того клиента. Его участники не тронуты: их трафик остаётся в его туннеле, а имена им разрешает тот, на кого он их направил. Ничего не выключено, поэтому ничего такого и не написано: строка выходного узла остаётся активной, а настройки говорят, для каких приложений уровни встали.
  • Переопределение — не состояние, а выход из обоих: приложения чужого клиента теряют и трафик, и разрешение имён на всё время работы TailSocks (переопределение).
  • Оба состояния записываются во вкладку ROOT экрана Логи (какие уровни встали и почему), а Проверить маршрутизацию показывает тот же вердикт.
  • Что запускает пересмотр: чужой клиент запустился или остановился, изменился состав его участников, поменялась влияющая на выбор настройка — так что ни уступка, ни сужение, ни возврат устройства себе не ждут перезапуска.

VPN-клиент, который туннелирует только часть приложений, остальные отдаёт физической сети. TailSocks берёт их себе; тот клиент ничего не теряет.

Шаг Как
Участники правила netd с uidrange, чья таблица названа по интерфейсу чужого туннеля
Переносятся разрывы между ними: установленные приложения минус VPN-клиенты и минус TailSocks
Маршрутизация по правилу priority 200 на каждый переносимый диапазон вместо общего на всё устройство
DNS по RETURN на каждый чужой netId в начале TAILSOCKS_DNS; остальное устройство остаётся на MagicDNS
Переносить нечего полная уступка
Правила в том виде, в каком их показывает root-консоль

Правила членства netd — диапазоны это участники, так что разрывы здесь — uid 10354 (сам клиент) и 10375 (TailSocks):

13000: from all fwmark 0x0/0x20000 iif lo uidrange 0-10353 lookup tun0
13000: from all fwmark 0x0/0x20000 iif lo uidrange 10355-10374 lookup tun0
13000: from all fwmark 0x0/0x20000 iif lo uidrange 10376-99999 lookup tun0

Что TailSocks ставит для переносимого uid’а — селектор общего правила, ограниченный uid’ами, которые чужому туннелю не нужны, — и для netId 0x84 того туннеля:

200: from all fwmark 0x0/0x2020000 iif lo uidrange 10380-10380 lookup 52
iptables -t nat -A TAILSOCKS_DNS -m mark --mark 0x84/0xffff -j RETURN

В ip rule нет «кроме этих uid’ов», поэтому правил столько же, сколько диапазонов, — ровно столько же ставит и сам чужой клиент.

Что снимается при уступке

То, что не ставится, ещё и активно снимается: catch-all прошлого применения (общий или суженный), throw-маршруты Исключенные подсети IP, которые мы писали в таблицу 52, изменённый rp_filter, правила исключений приложений и оба DNS-хука. Суженные правила так же целиком переписываются, когда чужой туннель меняет состав пропускаемых приложений. Пропущенная установка не убрала бы ничего, а эти правила вполне могли остаться от предыдущего применения в этом же запуске.

Почему считается только таблица самого туннеля

Локальная таблица физической сети (wlan0_local) содержит uidrange на все uid’ы устройства. Если её учесть, разрывов не остаётся вовсе — частичная уступка молча превращается обратно в полную.

uid'ы, которые не переносятся никогда: VPN-клиенты и песочницы SDK
  • Собственный uid VPN-клиента всегда оказывается в разрыве: владелец VpnService всегда вне своего туннеля, иначе он зациклил бы через себя трафик до релея. Перенеся его, мы отправили бы соединение того клиента с его же релеем через наш выходной узел, а список разрывов никогда не был бы пустым, и до полной уступки дело не дошло бы вовсе. Android знает эти приложения и отвечает без root и без разрешений — PackageManager.queryIntentServices(Intent("android.net.VpnService")), — поэтому TailSocks спрашивает его, а не гадает.
  • uid’ы, за которыми нет приложения. Android резервирует рядом с каждым приложением uid песочницы SDK (appId + 10000), так что в списке netd и там есть дырка. Замер: uid 20354 лежал в разрыве, и за ним не стояло ни одного пакета — правило для него не могло совпасть ни с чем. Поэтому переносимое множество строится из установленных приложений, а не вычитанием того, что выглядит подозрительно.
  • Большие разрывы. Разрыв до 64 uid’ов — обычный случай, когда мимо туннеля пропущено несколько приложений, — разбирается по одному приложению. Больший (клиент в режиме «туннелировать только эти приложения» оставляет снаружи почти всё устройство) сохраняется целиком, из него лишь вырезаются известные uid’ы VPN-клиентов: сотни правил по одному uid’у обходятся дороже, чем несколько правил, которые иногда ни с чем не совпадают.
Почему DNS делится по netId, а не по uid (замер)

Совпадение по uid не может оставить чужому клиенту его собственное разрешение имён: запросы приложения отправляет системный резолвер от своего uid’а (исключения приложений). Зато сохраняется идентификатор сети netd — он проставляется в младшие 16 бит fwmark на пакете резолвера. Замер за 18 секунд просмотра страниц внутри чужого туннеля, счётчики в mangle OUTPUT по udp --dport 53:

метка (маска 0x1ffff) пакетов что это
0x10084 6 резолвер работает от имени приложения внутри чужого туннеля
0x73 18 разрешение имён по физической сети

netId’ы читаются той же пробой ip rule, что и диапазоны uid’ов (fwmark 0x10084/0x1ffff → netId 0x84), и сравниваются между тиками, так что чужой туннель, перезапустившийся с новым netId, замечается, и исключение переписывается. Расширять маску catch-all вместо этого бесполезно: никакая метка не переспорит правило, которое читается раньше.

Чтобы уступить, достаточно любого из двух признаков: безопасное направление — «устройство принадлежит кому-то другому».

Признак Что читает
Android ConnectivityManager сообщает о сети с транспортом VPN — только о туннеле, участником которого является TailSocks
Проба со стороны root туннельный интерфейс, не наш (tun*, ppp*, wg*, ipsec*), и правила по uid’ам в полосе приоритетов netd, указывающие на таблицу, которая не наша и не физическая; заодно даёт диапазоны участников и netId’ы
Почему два признака и почему форма сверяется на каждом тике
  • Ответ Android слеп к тому клиенту, который важен. Клиент, исключивший TailSocks из своего туннеля, — правильная настройка, — для него невидим, а это ровно тот клиент, который мы и сломали бы.
  • Нужны обе половины пробы. Эта пара — то, как netd выражает «эти приложения — участники VPN», и больше никто на устройстве такого не пишет; остановленный клиент оставляет за собой интерфейс, а вручную маршрутизируемый WireGuard не занимает слот VPN.
  • Одно чтение — два ответа. Проба отвечает и на «есть ли здесь кто-то», и на «что он оставил» — всё, из чего собирается частичная уступка. Диапазон, который не разобрался, отбрасывается, а не додумывается: неверный отдал бы нам собственные приложения того туннеля.
  • На каждом тике обновления. Состав участников меняется и без падения туннеля, а callback возможностей Android об этом сообщить не может (для не-участника состав скрыт, а клиент, исключивший нас, — как раз тот, который важен), поэтому приложение запоминает форму и сверяет её на каждом тике.
  • Лог ROOT получает вердикт, входные данные решения — метки, чужой туннель, переопределение, диапазоны, разрывы, netId’ы — и причину при каждом применении.

Всё равно занять устройство (Настройки → Режим туннеля) ставит все три уровня на всё устройство независимо ни от чего — без сужения и без исключения чужого DNS. По умолчанию выключено и запоминается; диалог подтверждения называет цену. Выход через выходной узел и MagicDNS для приложения, которому можно указать резолвер, переопределения не требуют — есть локальные прокси.

Почему его нет в бэкапе профиля

Это согласие насчёт этого телефона и его второго клиента, а не настройка, которую стоит восстанавливать где-то ещё, поэтому бэкап профиля намеренно его не переносит. Лог ROOT повторяет цену при каждом применении с этим флагом. Включайте, когда чужой туннель не жалко прервать.

Загрузочный скрипт ставит T1 и на этом останавливается; остальное — уступку, её область и исключения — приложение решает на первом тике в состоянии Running.

Автозапуск при загрузке После перезагрузки
выключен только доступность tailnet — ни выходного узла, ни принятых маршрутов подсетей для остальных приложений, ни системного MagicDNS, — пока вы не откроете TailSocks
включён то же, на несколько секунд между переходом демона в Running и первым применением правил приложением
Почему загрузочный скрипт останавливается на T1
  • На стадии late_start нет PackageManager. А всё, что делает T2 и T3 безопасными, — throw-маршруты локальной сети, исключения приложений, исключения DNS по uid’ам — требует разрешения имён пакетов в uid’ы.
  • Другого VPN-клиента ещё нет. Скрипт выполняется задолго до старта службы любого другого клиента, так что проверка соседства здесь почти всегда отвечала бы «устройство свободно».
  • Захват без своих исключений хуже, чем отсутствие захвата. Именно так перезагрузка с выбранным выходным узлом уносила в туннель локальную сеть и приложения из списка исключений (старые версии).

Ничего из этого не обязательно, но от этого зависит, насколько хорошо эти двое уживаются:

  1. Исключить TailSocks из его туннеля (поприложенный прокси в режиме обхода или его список исключённых приложений).
  2. Держать свои адреса вне 100.64.0.0/10 и fd7a:115c:a1e0::/48: адрес релея или сервера, DNS-сервер, подсеть его tun-интерфейса.
  3. Чтобы tailnet был внутри его туннеля, направить его исходящий трафик в наш SOCKS5-прокси (по умолчанию 127.0.0.1:48115) или HTTP-прокси, а DNS — на 127.0.0.1:1053.
Почему так и что показал замер с Exclave

У настоящего клиента, Exclave, первые два пункта оказались верными без всякой ручной настройки.

  • Пункт 1. Собственные сокеты демона несут бит protect от Android и в любом случае маршрутизируются физической сетью, так что туннель поднимется и без этого. Но всё остальное — control plane через HTTP-прокси, обновления, Admin API — иначе поедет по второму туннелю без всякой причины, и сбой в том клиенте будет выглядеть как сбой TailSocks. Замер: диапазоны uid’ов Exclave уже пропускают и его собственный uid, и наш.
  • Пункт 2. Наши правила priority 100 отправляют эти диапазоны в tailscale0 для всех приложений на устройстве. 100.64.0.0/10 — настоящее пространство CGNAT, и некоторые клиенты выбирают адреса именно оттуда. Замер: Exclave маршрутизирует IPv4 кусками по /8, но ставит 100.0.0.0/10 и 100.128.0.0/9 и намеренно не 100.64.0.0/10, а его собственный туннель живёт на 172.19.0.0/30. Защита от чужого CGNAT покрывает адрес интерфейса внутри диапазона — например, подсеть собственного tun другого клиента; адрес вне всех таких префиксов — скажем, удалённый релей — по-прежнему уходит в tailnet.
  • Пункт 3. SOCKS5-прокси (Настройки → Локальные прокси, с паролем с той же страницы, если вы его задали) и HTTP-прокси обслуживает сам демон, поэтому всё, что идёт через них, следует за tailnet и выбранным выходным узлом независимо от того, какие уровни стоят на устройстве. Замер с выбранным выходным узлом: соединения через SOCKS5-прокси выходили через выходной узел, и пир tailnet по нему отвечал. Поэтому «Root-режим или прокси-клиент» — ложный выбор: клиент, выбирающий upstream по приложению, может отправлять выбранные вами приложения в TailSocks — с выходом через выходной узел, — пока устройство остаётся за ним.

Проверить маршрутизацию (Настройки → Диагностика и разработчику → Служебная информация о демоне (ЭКСПЕРИМЕНТАЛЬНО), при включённом нативном TUN) выводит текущее состояние всего описанного выше — с неё и начинайте.

Если видно Это значит
tailscale0 для 8.8.8.8 под исключённым uid приложение на деле не исключено
tailscale0 для первого исключённого локального префикса локальная сеть идёт через выходной узел
правило, которое никогда не срабатывает goto, который ядро не смогло разрешить
ещё один --set-xmark в mangle второе приложение делит с нами fwmark
правило DNS-цепочки с нулём пакетов правило есть, но трафик до него не доходит
маркер запуска, строка SO_MARK и вердикт почему priority 200 поставлен или нет
вердикт соседства кто владеет устройством и какие диапазоны uid’ов и netId переносятся

Логи → ROOT: каждая root-команда, её ошибки и сводка каждого применения. TAILSCALE: собственный лог демона.

Всё, что печатает «Проверить маршрутизацию», и вкладки логов
  • полные списки ip rule / ip -6 rule: наши правила priority 100, catch-all priority 200, когда он стоит, исключения priority 190 и любые оставшиеся правила 52xx;
  • другие туннельные интерфейсы на устройстве;
  • таблицу 53 (префикс tailnet и throw на каждый чужой CGNAT-префикс), таблицу 1099, таблицу 52 для IPv4 и IPv6 (с throw для Исключенные подсети IP) и все маршруты через tailscale0;
  • число маршрутов по умолчанию в таблице 53 — ожидается 0; единица значит, что устройство захвачено;
  • решение маршрутизации для 8.8.8.8 с меткой демона и без неё — по каждому исключённому uid’у и для первого исключённого локального префикса;
  • любое правило, которое стоит, но никогда не срабатывает;
  • каждый --set-xmark в mangle, наш и чужой;
  • rp_filter и сохранённое исходное значение;
  • последний маркер запуска и строку SO_MARK из лога демона вместе с выводом, который приложение из них сделало;
  • вердикт соседства — владеет ли устройством другой туннель, по чему он опознан и, если он пропускает часть приложений мимо себя, какие диапазоны uid’ов и какой netId переносятся;
  • цепочки, со счётчиками пакетов на DNS-цепочке (это отличает «правила нет» от «правило есть и ни разу не сработало»);
  • адрес tailscale0 и версию ip.

Вкладка ROOT записывает применение и снятие маршрутизации, остановку демона и установку скриптов; ошибки попадают туда, а не теряются молча, а сводка каждого применения говорит, какие уровни встали и почему какие-то нет. TAILSCALE показывает файл logs/tailscaled.log — это та же вкладка, которую демон заполняет в режиме прокси. Кнопка «Очистить» спрашивает, что чистить: лог приложения, лог демона или всё.

Стандартная политика не даёт приложению connectto на unix_stream_socket root-демона, и на устройстве в режиме enforcing подключение падает с EACCES — что выглядит в точности как мёртвый демон. RootUtils.allowSocketConnect добавляет правило по требованию; при загрузке ничего не патчится.

Когда приложение подключается к сокету (или ждёт его), и подключение отклонено с ошибкой доступа
Применяет allow <домен приложения> <домен демона> unix_stream_socket connectto через magiskpolicy, ksud sepolicy patch или supolicy — только до перезагрузки
Лог успех или неудача, во вкладке ROOT
Откуда берутся домены и что приложить к отчёту

Демон запускается из root-консоли, поэтому работает в домене root-решения (magisk, su — каким бы ни пользовался установленный менеджер), а приложение подключается к tailscaled.sock из собственного домена приложения. Оба домена читаются, а не предполагаются: приложения — из /proc/self/attr/current, демона — из /proc/<pid>/attr/current процесса, найденного по --socket=. Если инструмента политики нет, Root-режим не достучится до демона ровно так же, как без правила.

tools/root-debug.sh печатает getenforce, контексты консоли и демона и последние отказы AVC — то, что нужно в отчёте об этой проблеме.

Завершать Root-демон при остановке (Настройки → Режим туннеля) решает, что делает «Остановить». Находится только демон этого приложения — по аргументу --socket=, — так что второй профиль, tailscaled из Termux или другое приложение с той же библиотекой никогда не пострадают.

Переключатель «Остановить»
включён (по умолчанию) SIGTERM, затем SIGKILL root-процессу libtailscale.so
выключен приложение только отсоединяется; следующий запуск подключается к работающему демону

Ручная остановка окончательна в любом случае. При запуске демона, который ещё поднимается, ждут, а не убивают.

Как запуск подхватывает найденного демона
При запуске Что происходит
демон отвечает на сокете подхватывается сразу
ответа нет, но root находит процесс с этим --socket= запуск ждёт, пробуя каждые 400 мс: подхватывает демона, когда тот ответит, и заменяет его, только если тот умер или не ответил за 15 с
такого процесса нет запуск не ждёт вовсе

Аргумент --socket=, по которому опознаётся демон, указывает на приватный каталог данных приложения. При выключенном переключателе изменение настроек, пока вы хотите соединение, тоже подключается к работающему демону вместо запуска нового. После ручной остановки ничто не оживит службу, пока вы не запустите её снова.

Файл Что это
/data/data/io.github.bropines.tailscaled/logs/tailscaled.log лог демона
/data/data/io.github.bropines.tailscaled/files/tailscaled.sock сокет демона
/data/adb/service.d/tailscaled.sh загрузочный скрипт
/data/adb/tailsocks/control_proxy.env с чем загрузочный скрипт запускает демона
/data/adb/tailsocks/rp_filter.orig значение rp_filter, которое нужно вернуть
/data/adb/modules/tailscaled/module.prop модуль Magisk для CLI-обёртки
Окно терминала
su -c tail -f /data/data/io.github.bropines.tailscaled/logs/tailscaled.log # лог демона в реальном времени
su -c ps -ef | grep tailscaled # работает ли он от root?
su -c ls -la /data/data/io.github.bropines.tailscaled/files/tailscaled.sock # права на сокет
# Наши правила: 100 — tailnet, 190 — исключённые приложения, 200 — catch-all выходного узла; 52xx быть не должно
su -c ip rule show
su -c ip route show table 53 # /10 tailnet плюс throw на каждый чужой CGNAT-префикс
su -c iptables -t mangle -S TAILSOCKS_MARK
su -c iptables -t nat -S TAILSOCKS_DNS
Команды для выходного узла и доказательства SO_MARK

Priority 200 — одно правило на всё устройство, когда слот VPN свободен, по правилу на переносимый диапазон uid’ов при частичной уступке и ни одного при полной.

Окно терминала
su -c ip route show table 52 # пиры, маршруты подсетей, 'default dev tailscale0' при выходном узле, throw исключённых подсетей
su -c ip route get 8.8.8.8 # tailscale0 с выходным узлом на нашем устройстве, иначе Wi-Fi/сотовая
su -c ip rule show | grep '^200:' # пусто — правильно, пока устройство держит другой VPN
su -c ip route get 8.8.8.8 mark 0x2000000 # собственный путь демона: всегда физическая сеть
su -c grep -n 'TailSocks: daemon start' /data/data/io.github.bropines.tailscaled/logs/tailscaled.log | tail -n 1 # где начинается текущий запуск
su -c grep 'netns: SO_MARK' /data/data/io.github.bropines.tailscaled/logs/tailscaled.log # ожидается после этой строки: '... set on tailscaled sockets (root bypass)'

Два скрипта в репозитории собирают всё это для отчёта об ошибке. Приложите то, что они записали, к issue о Root-режиме на GitHub.

  • tools/root-debug.sh — на устройстве, от root: снимок только для чтения.
  • tools/root-debug-session.sh <adb-serial> [package] — с компьютера через adb: полный цикл запуска и остановки со снимком на каждой фазе в ./root-debug-<timestamp>/.
Окно терминала
adb push tools/root-debug.sh /data/local/tmp/
adb shell su -c sh /data/local/tmp/root-debug.sh # необязательный аргумент: имя пакета
tools/root-debug-session.sh 192.168.1.83:44895
Что собирает каждый скрипт
  • root-debug.sh печатает версию приложения, относящиеся к делу глобальные настройки, процесс демона и сокет, интерфейс tailscale0 и другие туннели, правила политики, таблицы 53 и 1099, решения маршрутизации с меткой и без, обе цепочки со счётчиками пакетов (это отличает «правила нет» от «правило есть, но трафик до него не доходит»), остатки старых правил, bind-mount /system/etc/hosts, пробу разрешения DNS и последние 40 строк лога демона. Ничего не изменяет.
  • root-debug-session.sh пишет 00-before.txt, 01-t{5,15,30}s.txt, пока демон поднимается, 02-after.txt, 03-after-stop.txt (именно он показывает, остались ли правила после остановки), а также logcat.txt и внутренний лог приложения applog.txt. Служба запускается и останавливается через am start-foreground-service … -a START_ACTION / -a STOP_ACTION.

Что прежние сборки делали иначе — для устройства, на котором такая стояла. Текущие сборки сами убирают старые правила и таблицы.

Что может остаться и что изменилось

Что может остаться от старой установки:

  • Таблица 1099 вместо 53 — во всех сборках до 4.0.0. netd называет таблицы своих сетей номером интерфейса плюс 1000, так что на телефоне, у которого индекс интерфейса доходил до 99, эта таблица принадлежит живой сети, — а прежняя очистка сбрасывала её целиком. Больше в неё ничего не пишется; каждое применение и каждая остановка удаляют то, что оставила старая версия, — по содержимому и никогда через flush. Если на устройстве 4.0.0 ещё не запускалась, смотрите в 1099.
  • «Голая» метка 1099 (--set-mark 1099) и прямые записи в OUTPUT без цепочек — от 3.5.x. «Голая» метка затирала весь fwmark Android и ломала маршрутизацию, когда сетью по умолчанию владел другой VPN. И то и другое удаляется автоматически при первом запуске после обновления, а также загрузочным скриптом.
  • Правила 52xx — 5210 lookup main, 5230 lookup default, 5250 unreachable, 5270 lookup 52 — десктопные правила, оставшиеся от ядра до 4.0. Удаляются по содержимому при каждом применении и при очистке.
  • Mangle-цепочка TAILSOCKS_BYPASS — от прежней версии исключений приложений, которая метила исключённые uid’ы bypass-битом демона. Метка в mangle OUTPUT ставится уже после того, как ядро выбрало маршрут и адрес источника, так что сделать работу правил политики она не могла. Отцепляется и удаляется при каждом применении.
  • Исключения с переходом на 16000 — до 4.0.0 это значение было зашито, а оно не везде такое; теперь цель читается с устройства.
  • libtailscale_cli.so — отдельный CLI до 4.9. Обёртка переустанавливается при обновлении, так что старая надолго не остаётся указывать на исчезнувший файл.
  • Загрузочный скрипт без строки версии, как все копии до 5.0.0: помечается устаревшим рядом с Переустановить скрипт.

Что изменилось в поведении:

  • До 4.0.0 загрузочный скрипт сам ставил уровни для всего устройства. Окно после перезагрузки было меньше, но то, что он ставил, не имело ни исключений для локальной сети, ни исключений приложений — перезагрузка с выбранным выходным узлом уносила туда роутер, NAS и adb по Wi-Fi, а исключённые приложения исключёнными не были, — и вообще не видело другого VPN-клиента. T2 и T3 уступаются другому клиенту начиная с 4.0.0.
  • До 4.1.0 загрузочный скрипт оставлял сокету, состоянию и логу права 777/777/666 до следующего запуска демона из приложения, а демон, поднятый при загрузке, был вовсе без прокси, так что Taildrive в «Файлах» и клиенты из локальной сети не работали до ручного перезапуска.
  • Строка env-файла без кавычек отбрасывается — из-за этого Игнорировать чужие VPN раньше сбрасывалось после каждой перезагрузки.
  • До 3.6 и приложение, и загрузочный скрипт при каждом запуске и каждой загрузке выполняли фиксированное magiskpolicy --live "allow untrusted_app magisk unix_stream_socket connectto". Это открывало доступ любому недоверенному приложению на устройстве и называло домены, неверные на большинстве из них (untrusted_app — не тот домен, в котором работает приложение с современным targetSdk; magisk — не домен KernelSU или APatch), так что часто ничего не давало, кроме расширения политики.
  • До 4.10 запуск один раз пробовал сокет: демон, который его ещё не открыл (старый файл сокета отвергает подключения, пока никто не слушает), убивался как устаревший, рядом с демоном без файла сокета запускался второй, а ответивший NoState убивался как ненастроенный.