Skip to content

Security and Privacy.ru

Iman edited this page Sep 6, 2026 · 2 revisions

Безопасность и конфиденциальность

Вики Caspian

Руководство перенесено из README. Даты измерений сохранены; перенос документации не означает нового запуска тестов. English | فارسی | Русский | 中文

Что он гарантирует

Каждый заголовок здесь подкреплён сгенерированным выводом файрвола в internal/netcfg/testdata/, поименованным тестом или измерением, записанным в репозитории. docs/BEHAVIOUR.md, это читаемый список обещаний. Каждый заголовок в нём, это имя сценария в test/bdd/, и у каждого сценария есть соответствующий внедряемый дефект. Поэтому «этот тест способен обнаружить то, что он заявляет» само по себе является результатом теста.

Пересылаемый клиентский трафик отказывает в закрытую, и блоку не нужен туннель

Политика цепочки forward, это drop. Первое правило в ней, это блок против утечки, и он называет только хотспот и аплинк:

iifname "wlan0" oifname "eth0" drop comment "fail-closed: client traffic never leaves by the uplink"

Каждое правило, разрешающее клиентский трафик, называет устройство туннеля, поэтому, когда туннель исчезает, эти правила перестают срабатывать и политика отбрасывает всё. Сам блок не может перестать работать при исчезновении туннеля, потому что он его не упоминает. Каждый интерфейс сопоставляется по имени и никогда по индексу, поэтому набор правил загружается и без туннеля, а это ровно тот момент, когда он нужен. Цепочка postrouting пуста намеренно.

Сценарий: "with the tunnel gone, nothing lets client traffic out by the uplink". Вспомогательный тест анализатора: TestWithoutInterfaceRemovesOnlyTheRulesNamingIt.

Аварийное отключение покрывает и собственный трафик коробки

Цепочка output имеет policy drop с поименованным списком разрешений. Разрешения выведены перечислением того, что на целевой машине действительно работает, а не выборочным наблюдением за трафиком, и каждое разрешение в сгенерированном наборе правил несёт то показание, которое его оправдывает: сокеты DHCP-клиента NetworkManager, systemd-timesyncd, DNS, loopback, устройство туннеля, обнаружение соседей IPv6 и сам прокси-сервер, разрешённый по адресу, а не по порту, чтобы транспорт по UDP на 443 не оказался молча сломан. Одно разрешение добавлено из рассуждения, а не из измерения, и прямо об этом говорит: коробка, отвечающая по DHCP как сервер на хотспоте, что conntrack покрыть не может, потому что ответ DHCP и запрос к нему не имеют общего кортежа.

Провокации и, что важнее, отрицательные контроли записаны в PROVENANCE.md, раздел «The three provocations, run with the policy loaded». Тесты: TestRestrictedEgress_PermitList, TestRestrictedEgress_AcceptsEstablishedBeforeItDropsAnything, TestRestrictedEgress_ServerIsPermittedByAddressNotPort.

Цена этого указана в собственном заголовке набора правил, а не обнаруживается на ходу: apt update из шелла на коробке не работает, пока устройство включено.

Есть аварийная отсечка, которая не уносит с собой хотспот

Выключение устройства опускает хотспот, а это отключает телефон, с которого нажимали кнопку. Поэтому существует отдельный орган управления, который обрывает пересылаемый клиентский трафик, оставляя хотспот, DHCP, DNS и панель поднятыми. Смотрите internal/privsvc/cut.go и действие панели cut в internal/panel/priv.go.

Отсечка, это состояние времени выполнения, и оно никогда не пишется на диск, поэтому выдёргивание питания её отменяет. Тесты: TestCuttingClientTrafficLeavesTheWayBack, TestACutIsNeverWrittenDown, TestACutDoesNotSurviveARestart, TestForwardCut_StopsClientsAndKeepsThePanelReachable.

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

Он считывает интерфейс обратно из ядра, а не доверяет процессу

Запущенный процесс не является доказательством того, что он сработал. Это был настоящий сбой. Заголовок internal/privsvc/readback.go фиксирует, что 2026-08-30 служба записала себя как работающую с хотспотом на wlan0, в то время как wlan0 всё ещё был клиентом в домашней сети. hostapd был живым процессом, чей управляющий сокет не отвечал. Телефон в комнате показывал одиннадцать сетей, и нашей среди них не было. А dnsmasq отвечал чужому устройству в чужой локальной сети сообщением DHCPNAK.

Ничему не позволено привязаться к интерфейсу хотспота, пока netcfg.AssertHotspotInterfaceReleased не докажет, что тот свободен, и ничто не сообщает о себе как о работающем, пока AssertHotspotIsAccessPoint не считает обратно точку доступа, вещающую ожидаемое имя. Тесты: TestNothingBindsToTheHotspotInterfaceUntilItIsProvedFree, TestTheServiceDoesNotReportRunningUntilTheAccessPointReadsBackAsOne, TestAnAccessPointBroadcastingAnotherNameIsNotOurs, TestTheReleaseIsReadBackBeforeAnythingBindsAndTheAccessPointAfter.

Вы получаете свой Wi-Fi обратно

Каждое сетевое изменение записывается в /var/lib/caspian/netcfg.journal вместе с обратным действием до того, как оно сделано, и выключение проигрывает их в обратном порядке. Запись лежит на диске, а не в памяти, поэтому убитый процесс или отключение питания её не теряют. Коробка, умершая посреди изменения, проигрывает запись прежде, чем посмотрит на машину или применит что-то новое.

Захват интерфейса Wi-Fi журналируется шаг за шагом. Прямая последовательность и обратные к ней действия были выполнены на целевой машине и записаны в PROVENANCE.md, раздел «The release sequence has been run on the target». Четыре команды ушли, и обратные действия вернули коробку в её собственную сеть с её собственным адресом восемь секунд спустя.

У одного изменения намеренно нет обратного действия, и TestPlan_InvariantsHoldOnEveryModelledMachine утверждает, что оно единственное: поднятие интерфейса хотспота. Опускать радио на выходе хуже, чем оставить его поднятым, потому что собственный Wi-Fi машины, и панель, которую читает пользователь, могут быть на нём.

Сценарии: "turning the switch off returns every change the box made", "a teardown replayed from the journal of a killed process undoes the same changes", "a box killed halfway through cleans up before it does anything else". Тесты: TestJournal_RecordsInverseBeforeTheChange, TestTeardown_ReplaysInExactReverseOrder, TestRecover_UndoesAJournalLeftByAKilledProcess, TestTheTakeoverReleasesTheInterfaceItSaysItWillRelease.

Если обратное действие не удаётся, обратное действие для файрвола удерживается, а не выполняется, поэтому коробка, которая не смогла отменить свои маршруты, сохраняет блокировку. Тест: TestTheFirewallIsNotRemovedWhenAnEarlierInverseFailed.

Вставленный конфиг не попадает ни на экран, ни в лог, ни в читаемый файл

Всё, что порождает этот поток, проверяется на наличие вставленных учётных данных:

  • каждая ошибка и каждое сообщение, показанное пользователю
  • строки лога устройства
  • описание конфига в панели
  • сохранённые настройки в том виде, в каком они рендерятся для диагностики
  • сгенерированный файрвол
  • сгенерированная конфигурация DHCP и DNS
  • собственный лог движка
  • журнал на диске
  • запрос, идущий от непривилегированной панели к привилегированной службе

Их нет ни в одном из перечисленного. При этом проверяется, что они есть в тех двух местах, где они быть обязаны, чтобы тест не мог пройти из-за того, что конфиг просто пропал.

Сценарии: "the pasted credential never reaches a screen, a log or a readable file", "the hotspot password reaches the access point and nothing else". Тесты: TestPastedConfigNeverAppearsInAResponseOrALog, TestFailedConfigPathsDoNotEchoTheInput, TestStartRequestRedactsItself, TestNoCredentialReachesTheAdvancedView, TestTheServerAddressNeverAppearsInADiagnosticLine.

Панель ничего не просит у интернета

Каждая таблица стилей, каждый скрипт и каждая иконка, которые загружает браузер, вкомпилированы в бинарник через go:embed. Смотрите internal/panel/assets.go. Веб-шрифтов нет вообще: набор шрифтов в таблице стилей состоит только из системных начертаний, с поддерживающими персидский на первом месте.

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

Механизмов два, а не один. TestNoAssetReferencesAnExternalURL и TestNoRenderedPageReferencesAnExternalURL сканируют ресурсы и каждую отрисованную страницу на наличие абсолютного URL. setSecurityHeaders отправляет default-src 'none' со всеми перечисленными источниками, выставленными в 'self', поэтому браузер откажет тому, что проскочило мимо тестов. Никакого исходящего HTTP-клиента нигде в internal/panel за пределами его собственных тестов не существует.

Сгенерированная конфигурация также нигде не называет резолвер Google и не использует ни одного правила geoip: или geosite:, потому что любое из них вернуло бы загрузку из сети в продукт, вся история установки которого состоит из одного проверенного бинарника. Тесты: TestNoGoogleAnywhereInGeneratedConfigs, TestGoogleResolverIsRejectedAtTheSource. Сценарий: "the box needs no download and asks no Google server anything".

Привилегии разделены

caspian serve --privileged работает от root и владеет маршрутами, файрволом, точкой доступа и движком. Он принимает короткий список поименованных действий через unix-сокет и никогда команду, собранную из пользовательского ввода. caspian serve --panel работает под непривилегированной учётной записью caspian и владеет веб-интерфейсом и больше ничем. Про словарь и формат кадра смотрите раздел «Архитектура» выше.

Пароль панели хешируется через argon2id. Смотрите internal/state/password.go. Это локальный пароль на коробке. Никакой учётной записи где-либо ещё не существует.

Часы проверяются до любого рукопожатия

У Pi нет часов с батарейкой, а от настенного времени зависят два разных механизма. REALITY вписывает его в рукопожатие, и то, какие конфиги xray-core принимает, зависит от даты. Поэтому коробка, у которой часы поднялись неверно, не просто не подключается. Она принимает конфиг, который тот же самый бинарник отвергает, как только часы выправлены.

Проверка выполняется до валидации и до любых попыток. Смотрите internal/privsvc/clock.go, вызываемый из Service.Start как шаг 1 в applyLocked. Он выдаёт отдельную ошибку, чтобы панель не обвиняла конфиг пользователя. Тест: TestClockFailureIsNotBlamedOnTheConfig.

Три вида отказа конфига различаются между собой

«Не смог прочитать эту ссылку», «прочитал, и в таком виде её использовать нельзя» и «со ссылкой всё было в порядке, а сервер не ответил» требуют от пользователя трёх разных действий, и третий случай самый частый. Именно обвинение конфига в первую очередь заставляет человека выбросить конфиг, который никогда не был сломан. Ничто на машине не трогается до того, как вставленный текст прочитан. Сценарии: "text that is not a link at all is refused before anything is touched", "a link the engine will not accept is told apart from one that would not parse", "a link whose server never answers is not blamed on the link".

Чего он не гарантирует

Вот этот список стоит читать внимательно.

DNS over HTTPS на порту 443 переносится, а не блокируется, и здесь его никто не видит

Клиентский DNS на порту 53 перенаправляется на эту коробку по обоим протоколам, а не просто разрешается. Поэтому устройство с зашитым резолвером получает ответ здесь, а не выпускается наружу к тому резолверу, который ему назвали. DNS over TLS на 853 отклоняется TCP-сбросом, поэтому устройство откатывается на перенаправленный порт. DNS over QUIC на 853 отбрасывается.

DNS over HTTPS на порту 443 не отличим от любого другого HTTPS и переносится через туннель, как и всё остальное. Клиент, который им пользуется, находится внутри туннеля и ничего не утекает. Он при этом невидим. Ни что-либо в этом проекте, ни что-либо в аппаратном стенде наблюдать его не может. Это ограничение дизайна. Оно указано в самом сгенерированном наборе правил, в docs/BEHAVIOUR.md и в печатаемом выводе проверки на утечку DNS, а не только здесь.

IPv6 заблокирован, и путь для IPv6 не доделан

Туннеля для IPv6 нет. Устройство с работающим путём по IPv6 предпочтёт его IPv4 и обошло бы туннель полностью, поэтому политика по умолчанию, это блокировать. Держат это четыре вещи. IPv6Block является значением по умолчанию в netcfg.DefaultOptions. Коробка не пересылает IPv6. Файрвол отбрасывает пересылаемый IPv6 на хотспоте в обе стороны. И анонсы маршрутизатора в сторону хотспота отбрасываются, поэтому устройство не может выдать себе адрес. Сценарий: "clients are never offered the IPv6 the tunnel cannot carry".

IPv6Forward существует как опция, и её собственный комментарий говорит не включать её. Не было показано, что входящий TUN движка переносит IPv6 на целевой машине. Она также намеренно не добавляет ни одного разрешающего правила в цепочку forward: разрешения для IPv4 называют подсеть хотспота в обе стороны, во всём плане нет ни одного v6-префикса, который можно было бы назвать, а правило, совпадающее только по двум именам интерфейсов, приняло бы любой адрес источника, который написал клиент. TestRuleset_NoUnconstrainedIPv6AcceptInForward держит эту линию.

«Заблокирован» относится к маршрутизации, а не к DNS, и разница имеет значение. AAAA-запрос от подключённого устройства не подавляется и не получает пустой ответ. Он идёт в движок, проходит через туннель и возвращается с настоящими AAAA-записями, потому что документ движка запрашивает UseIP, а dnsmasq не задаёт filter-AAAA. Поэтому устройство узнаёт IPv6-адреса, до которых у него нет никакого пути, и откатывается на IPv4.

Это безвредно, пока ничто не может выдать клиенту v6-адрес, и это первое, что перестанет быть безвредным, если что-нибудь когда-нибудь сможет, потому что клиент с работающим путём по v6 предпочтёт AAAA-ответ и ушёл бы маршрутом, который эта коробка не переносит. Это записано здесь, а не оставлено сюрпризом, и TestAAAAQueriesAreAnsweredAndNotSuppressed закрепляет обе половины, так что изменить это должно быть решением.

Аппаратный стенд вообще не способен оценивать IPv6, поэтому никакой его результат по IPv6 ничего не значит. test/hardware/README.md фиксирует в разделе «What this vantage cannot grade: IPv6», что у телефона есть только link-local адрес, что ip -6 route show default пуст и на телефоне, и на Pi, и что соединение с IPv6-литералом отвечает «Network is unreachable». В той локальной сети IPv6 нет вообще, поэтому проверка на утечку IPv6, запущенная там, прошла бы и без каких-либо действий устройства. Каждый аппаратный результат, который есть у этого проекта, является результатом по IPv4. Тот, кто запускает это в сети с работающим IPv6, обязан считать это новым вопросом, а не закрытым, и должен рассчитывать, что тест придётся написать, а не включить.

Собственный трафик коробки находится вне обещания fail-closed, и это сделано намеренно

Обещание касается пересылаемого клиентского трафика. Собственное соединение коробки с вашим сервером обязано дойти до аплинка напрямую, иначе никакого туннеля не будет, и docs/2026-08-29-design.md, раздел 7, по этой причине выносит собственный трафик коробки за пределы гарантии.

Аварийное отключение в цепочке output сужает это, но не закрывает. Сгенерированный набор правил указывает остаток в собственном заголовке. DNS, это дыра: что угодно на коробке всё ещё может достучаться до сети по порту 53, а имя сервера разрешается в открытом виде в локальной сети до того, как появится хоть какой-то туннель. Ни то, ни другое не является утечкой клиентского трафика, и ни то, ни другое не ухудшается аварийным отключением.

Политика цепочки input, это accept, тоже намеренно и тоже указано в наборе правил. В более ранней версии стояло drop, и PROVENANCE.md фиксирует, что случилось, когда это измерили на целевой машине. Каждое новое входящее соединение отклонялось и SSH переставал отвечать, при этом уже открытая сессия продолжала работать, что на машине без монитора неотличимо от падения. Единственное место, где цепочка input что-то ограничивает, это сторона хотспота, где подключённое устройство получает доступ к DHCP, DNS, панели и ICMP echo и ни к чему больше на коробке.

Изоляция клиентов друг от друга, это правило, а не измерение

В наборе правил есть iifname "wlan0" oifname "wlan0" drop. То, что правило присутствует, проверяется. То, что оно работает, не проверяется.

Ничто в этом репозитории не фиксирует адрес выхода

test/tunnel двигает настоящие байты через настоящий сервер xray-core, и всё в нём происходит на loopback, поэтому он не фиксирует адрес выхода и не может. У test/bdd нет ни сети, ни радио, ни root, ни устройства туннеля. Он запускает настоящий движок внутри процесса через настоящий загрузчик конфигов, поэтому «движок принял этот конфиг и запустился» означает именно это, но входящий туннель при этом выключен.

Поэтому ничто в этом репозитории не удовлетворяет собственному стандарту проекта для того, чтобы называть что-либо работающим. docs/BEHAVIOUR.md заканчивается разделом «What this suite does not prove», перечисляющим то, что ещё не сделано. Читайте его как часть набора тестов.

Ничто не перепроверяет файрвол после его загрузки

Смотрите дефект D1 ниже. Если что-то очистит таблицу, пока устройство работает, коробка продолжит пересылать трафик, панель продолжит показывать подключение, и никто ничего не заметит.

Ничто не следит за аплинком

Смена интернета, из-за выдернутого кабеля или иначе продлённой аренды адреса, это изменение, на которое ничто громко не падает. Закреплённый маршрут до сервера всё ещё существует и всё ещё указывает на адрес, который больше не является выходом наружу. Коробка этого не замечает, и туннель остаётся остановленным, пока кто-нибудь снова не нажмёт переключатель.

Ценой этому является доступность, а не приватность. Клиентский трафик остаётся заблокированным всё это время, потому что политика forward, это drop, а каждое разрешение в ней называет туннель. netcfg.WatchUplink и Plan.RederiveForUplink существуют и работают, и ни один поставляемый участок кода их не вызывает. TestNothingInTheApplianceWatchesTheUplink, это то, что не даёт противоположной фразе снова просочиться в документы, где она стояла до 2026-08-30.

Режим B ни разу не запускался на настоящем железе

Каждая фикстура режима B написана вручную. PROVENANCE.md фиксирует, что у целевой машины одно радио и ни одного USB-адаптера, поэтому схема, ради которой продукт советует людям купить адаптер, доказана против байтов, которые никто не измерял.

Architecture | Panel-and-Configuration | Troubleshooting

Clone this wiki locally