Skip to content

About

macOS menu bar app that watches your real egress IP and flips Little Snitch deny-rule groups when traffic leaks past your VPN

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Little Snitch VPN Companion

CI Release

Иконка приложения

Меню-бар-приложение для macOS, которое следит за фактическим egress-IP машины и в момент, когда трафик пошёл мимо VPN, включает запрещающие rule groups в Little Snitch. Туннель восстановился — выключает их автоматически.

English TL;DR: a macOS menu bar app that watches the machine's actual egress IP and, the moment traffic leaks past your VPN, flips deny-rule groups in Little Snitch — then flips them back when the tunnel recovers. Detection is beacon-based (Cloudflare cdn-cgi/trace + a local-country IP beacon), enforcement goes through a tiny root helper that only knows three XPC operations. Docs and UI are in Russian.

Зачем

VPN-клиент упал, переподключается или «тихо» перестал держать маршрут — а трафик продолжает идти, уже напрямую через провайдера. Kill-switch в клиенте есть не всегда, а главное — он доверяет самому клиенту. Это приложение не доверяет никому: оно регулярно спрашивает у внешних маяков, кто на самом деле видит ваш трафик снаружи, и реагирует на факт, а не на статус в чужом UI.

Реакция — штатный механизм Little Snitch: rule groups с запрещающими правилами, которые companion включает при утечке и выключает при восстановлении.

Как это работает

  1. Проба egress. Раз в 60 с (и по каждому событию сетевого пути) приложение опрашивает маяк Cloudflare (cdn-cgi/trace): оттуда приходят фактический egress-IP, флаг warp=on и дата-центр.
  2. Прямой IP для сравнения. Отдельный РУ-маяк (yandex, резерв — 2ip) сообщает, какой IP выдал провайдер напрямую, — это динамическая часть denylist.
  3. Классификация. warp=on или egress из allowlist expectedIPs → Protected. Egress совпал с адресом из forbiddenEgressIPs (ваши же VPN-серверы) → Leak «цепочка вышла напрямую с сервера». Egress совпал с прямым IP провайдера → Leak «полный обход VPN». Маяки недоступны → Offline (это не утечка).
  4. Растяжка (tripwire). Постоянное TCP-соединение с heartbeat 15 с замечает обрыв туннеля за секунды — не дожидаясь следующей пробы.
  5. Реакция. Утечка подтверждается второй пробой (~2.5 с), затем привилегированный helper включает назначенные rule groups (littlesnitch rulegroup -e). Состояние восстановилось — группы выключаются сами. Обо всём — уведомление и запись в журнал.
  6. Эскалация. Если утечка подтверждена, а helper или CLI недоступны, приложение выключает Wi-Fi (networksetup -setairportpower off) — лучше без сети, чем напрямую.
  7. Режимы защиты. Реактивный (по умолчанию): группы включаются только при доказанной утечке. Строгий (fail-closed): открыто только при доказанном VPN — всё неопределённое закрывает. Эшелоны закрытия строгого режима: старт до первого вердикта, пропажа сети, пауза, ⌘Q, крэш приложения (dead-man's switch в helper), загрузка ОС до старта приложения и уход машины в сон — сон не гасит сеть (TCPKeepAlive), поэтому группы включаются до засыпания, а после пробуждения открываются только по свежему вердикту Protected.

Состояния Protected / Leak / Offline / Checking / Paused живут в state machine на чистом Swift без Apple-фреймворков; таблица переходов из ТЗ покрыта тестами построчно. Полное ТЗ: SPEC.md, артефакты разработки: openspec/changes/archive/2026-07-30-build-ls-vpn-companion/.

Что понадобится

Для запуска:

  • macOS 15.7+ (поддерживаемый диапазон — 15.7–26.x)
  • Little Snitch 6 в /Applications/Little Snitch.app
  • rule group в Little Snitch с запрещающими правилами (по умолчанию ожидается имя «VPN down»; любое другое задаётся в настройках)

Для сборки — дополнительно:

  • macOS 26.2+ с Xcode 26.x. Порог сборки выше порога запуска: иконка описана Icon Composer-документом App/AppIcon.icon, а actool компилирует его только на macOS 26 — на Sequoia он падает. Сборка идёт против SDK 26 при deployment target 15.7, это штатная схема, отдельно ничего настраивать не нужно
  • XcodeGen (brew install xcodegen) — проект генерируется из project.yml, сам .xcodeproj в git не хранится

Проверено: сборка и тесты на macOS 26.5.2 (Xcode 26.6, Apple Silicon); тесты — ещё и в CI на образе macOS 15.7 с Xcode 26.x, по нижней границе поддерживаемого диапазона; живая эксплуатация — на 26.x (Apple Silicon) и на 15.7.8 (Intel).

Артефакт универсальный (arm64 + x86_64), поэтому Intel-машине собственный Xcode не нужен: приложение собирается на Apple Silicon и переносится готовым бандлом — см. «Установка из релиза» и «Перенос на другую машину».

Установка из релиза

Собранный бандл лежит во вложениях релизов — Xcode и связка ключей автора для запуска не нужны: сертификат подписи встроен в сам бандл.

Распаковывать сразу в ~/Applications; запуск из ~/Downloads привяжет демона к этому пути в базе BTM (см. «Грабли», п. 1):

ditto -x -k ~/Downloads/LSVPNCompanion-v1.0.zip ~/Applications

Приложение подписано самоподписанным сертификатом и не нотаризовано, поэтому на скачанном архиве стоит флаг карантина и первый запуск отклоняется:

xattr -dr com.apple.quarantine ~/Applications/"Little Snitch VPN Companion.app"

Без командной строки то же самое делается после неудачной попытки запуска — Системные настройки → Конфиденциальность и безопасность → «Всё равно открыть»; обход через Control-click → «Открыть» в Sequoia уже не работает.

Дальше — открыть приложение, пройти онбординг, одобрить объект входа в Системных настройках. Little Snitch, его rule group и доступ для CLI настраиваются отдельно (см. «Что понадобится»).

Сборка и запуск

xcodegen generate && xcodebuild -project LittleSnitchVPNCompanion.xcodeproj -scheme LittleSnitchVPNCompanion -configuration Debug build
open ~/Library/Developer/Xcode/DerivedData/LittleSnitchVPNCompanion-*/Build/Products/Debug/"Little Snitch VPN Companion.app"

Приложение живёт только в строке меню (LSUIElement), в Dock не появляется.

Установка

Для повседневной работы приложение должно лежать в ~/Applications, а не в каталоге сборки: путь внутри DerivedData меняется при смене конфигурации, а System Settings — песочное приложение и не может прочитать оттуда бандл, из-за чего в «Объектах входа» вместо иконки виден серый плейсхолдер.

./Scripts/install.sh

Скрипт собирает Release, копирует в ~/Applications, обновляет реестр LaunchServices и запускает установленную копию. /Applications требует прав администратора; если аккаунт обычный — ~/Applications штатная замена.

После первой установки (и после каждого обновления, меняющего helper) демон нужно перерегистрировать: Настройки → Общие → «Переустановить…», затем одобрить объект входа.

Перенос на другую машину

Xcode на целевой машине не нужен: артефакт универсальный (arm64 + x86_64), и Intel-машина запускает его нативно. Для Intel это единственный путь — Xcode 26 в App Store отдаётся сборкой под Apple Silicon.

Сертификат подписи переносить тоже не нужно: он встроен в подпись, а helper строит XPC-requirement из собственной подписи — «клиент подписан тем же сертификатом, что и я». Совпадение проверяется внутри бандла, связка ключей целевой машины ни при чём.

Собрать Release и упаковать (ditto сохраняет подпись и обе архитектуры):

xcodebuild -project LittleSnitchVPNCompanion.xcodeproj -scheme LittleSnitchVPNCompanion -configuration Release build
ditto -c -k --sequesterRsrc --keepParent ~/Library/Developer/Xcode/DerivedData/LittleSnitchVPNCompanion-*/Build/Products/Release/"Little Snitch VPN Companion.app" ~/Downloads/LSVPNCompanion.zip

На целевой машине распаковывать сразу в ~/Applications — запуск из ~/Downloads привяжет демона к этому пути в базе BTM (см. «Грабли», п. 1):

ditto -x -k ~/Downloads/LSVPNCompanion.zip ~/Applications
xattr -dr com.apple.quarantine ~/Applications/"Little Snitch VPN Companion.app"
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -f -R -trusted ~/Applications/"Little Snitch VPN Companion.app"

Приложение не нотаризовано. Если архив приехал через AirDrop, браузер или почту, на нём стоит флаг карантина — снимает его команда xattr выше. Без неё первый запуск отклоняется, и в Sequoia обход через Control-click → «Открыть» больше не работает: остаётся Системные настройки → Конфиденциальность и безопасность → «Всё равно открыть». Передача через scp, rsync или внешний диск карантина не ставит вовсе.

Дальше — как при обычной установке: открыть, пройти онбординг, одобрить объект входа. Little Snitch, его rule group и доступ для CLI на целевой машине настраиваются отдельно.

Публикация релиза

./Scripts/release.sh 1.1.0

Скрипт собирает Release с MARKETING_VERSION=1.1.0, проверяет получившийся бандл, пакует его в build/LSVPNCompanion-v1.1.0.zip и создаёт на GitHub черновик релиза с этим вложением и инструкцией по установке в заметках. Тег v1.1.0 появится в репозитории только после публикации черновика; --publish публикует сразу.

Собирать релиз можно только на машине с сертификатом из Signing.xcconfig — шага сборки артефакта в CI нет намеренно. Requirement helper построен на «клиент подписан тем же сертификатом, что и я», поэтому приватный ключ этого сертификата — единственный гейт к привилегированному демону, и в секретах Actions ему не место. Ad-hoc сборка из CI на роль релиза тоже не годится: без сертификата проверка вырождается в один identifier, и к демону проходит любой процесс с нужным bundle id.

До сборки скрипт останавливается на грязном рабочем дереве, незапушенном HEAD, занятом теге и отсутствующем сертификате. После — проверяет, что приложение и встроенный helper подписаны именно этим сертификатом (а не свалились на ad-hoc), что оба бинаря универсальные, что launchd-plist демона на месте и что версия доехала до Info.plist.

Настройка под себя

Все адреса в репозитории — заполнители из документационных диапазонов RFC 5737; свои значения вносятся в настройках приложения:

  • forbiddenEgressIPs — IP ваших собственных VPN-серверов. Если egress совпал с одним из них, значит цепочка «схлопнулась» и трафик выходит напрямую с сервера.
  • expectedIPs — allowlist выходных адресов. Детектор считает состояние защищённым при warp=on (Cloudflare WARP как последнее звено) или при egress из этого списка — если ваша цепочка не через WARP, просто внесите сюда свои выходные IP.
  • Маппинг групп — какие rule groups включать при утечке.
  • В самом Little Snitch включите доступ для CLI: Little Snitch → Настройки → Безопасность (см. «Грабли», п. 2).

Тесты

xcodebuild -project LittleSnitchVPNCompanion.xcodeproj -scheme LittleSnitchVPNCompanion -configuration Debug test

130 тестов в четырёх бандлах: DomainTests (state machine и классификатор — таблицы §4.1 и §5 SPEC.md покрыты построчно), ApplicationTests (use cases на фейках портов), HelperTests (толерантный разбор модели LS), InfrastructureTests (журнал, настройки, парсеры маяка и networksetup).

Проверка чистоты доменного слоя (Domain не должен импортировать Apple-фреймворки):

./Scripts/check-domain-purity.sh

Живые проверки детектора

Обе утилиты собираются из доменного слоя и трогают сеть только на чтение (два GET по сотне байт).

swiftc -swift-version 6 Domain/*.swift Scripts/live-check.swift -o /tmp/live-check && /tmp/live-check

Печатает ответ маяка, прямой IP от РУ-маяка, вердикт по фактическому egress и контрольные подмены debug-сценариев §14 (2, 13, 14).

swiftc -swift-version 6 Domain/*.swift Application/Ports.swift Application/AppSettings.swift Infrastructure/TripwireConnection.swift Infrastructure/SystemPathMonitor.swift Scripts/live-tripwire.swift -o /tmp/live-tripwire && /tmp/live-tripwire

Держит растяжку 14 с с heartbeat 3 с и печатает обрывы и события сетевого пути.

Архитектура

App/            Presentation: SwiftUI-экраны, AppModel, composition root
App/DesignSystem/  токены → атомы → молекулы (§8 SPEC.md)
Application/    use cases и порты (протоколы)
Domain/         чистый Swift без Apple-фреймворков: state machine, классификатор, политика
Infrastructure/ проберы, растяжка, XPC-клиент, журнал, настройки, Wi-Fi
Helper/         привилегированный демон (root) + XPC-контракт
Tests/          DomainTests, ApplicationTests, HelperTests, InfrastructureTests
Scripts/        живые проверки детектора, установка, проверка чистоты Domain

Зависимости идут только внутрь: Domain не знает ни о чём, Application — только о Domain и своих портах, Infrastructure и App подставляют реализации в App/CompositionRoot.swift.

Привилегированный helper

Единственный компонент с правами root: только он вызывает littlesnitch CLI (rulegroup -e/-d, export-model). Контракт — три XPC-операции, произвольные команды не проходят по построению. Helper валидирует подключающегося XPC-клиента по code-signing requirement и отклоняет посторонних (в том числе сам Little Snitch).

Установка — шаг 1 онбординга (кнопка «Установить helper…») или тумблер в настройках. После SMAppService.daemon(...).register() macOS требует одобрения в System Settings → Основные → Объекты входа. Пока helper не одобрен, приложение работает в режиме деградации: детектор и журнал живут, группы LS не переключаются, в поповере горит строка с диагнозом.

Подпись

Проект подписывается самоподписанным сертификатом (см. Signing.xcconfig — там же пошаговая инструкция, как создать свой в Keychain Access). Тогда XPC-requirement строится строго: identifier "dev.sunnyday.lsvpncompanion" and certificate leaf = H"<sha1 сертификата>" — helper пускает только приложение, подписанное тем же сертификатом.

Важно: ENABLE_DEBUG_DYLIB = NO обязателен и уже прописан. Hardened Runtime включает library validation, а у самоподписанного сертификата нет Team ID, поэтому отладочная прослойка Xcode (<продукт>.debug.dylib, по умолчанию включена) не проходит проверку и приложение падает при запуске с Library not loaded … different Team IDs. С ad-hoc подписью дефект не проявляется — только с настоящим сертификатом.

Без сертификата (ad-hoc, CODE_SIGN_IDENTITY = -) собрать и запустить тоже можно, но XPC-валидация клиента работает в ослабленном режиме: у ad-hoc подписи нет устойчивой identity, поэтому helper проверяет только identifier и пишет предупреждение в лог.

Грабли, собранные при разработке

Всё ниже проверено на живой машине — сэкономит вам вечер.

1. Пересборка helper требует перерегистрации демона

launchd привязывает демон к его подписи на момент регистрации (launch constraint, LWCR). Пока меняется только код приложения, helper не перекомпилируется и всё работает. Но как только меняются исходники в Helper/, бинарь становится другим и launchd отказывается его запускать — в launchctl print system/dev.sunnyday.lsvpncompanion.helper это видно как last exit code = 78: EX_CONFIG и needs LWCR update, а приложение показывает «helper не ответил за 6 с».

Обычно лечится Настройки → Общие → «Переустановить…» (приложение делает то же само, если демон зарегистрирован, но не отвечает, — не чаще раза за запуск); macOS может попросить заново одобрить объект входа.

После любой регистрации демон поднимается не сразу: SMAppService.status отражает базу Background Task Management, а не launchd, и между register() и первым ответом helper проходит до ~30 с (замер 2026-09-09: новый демон стартовал через 30 с после последнего register(), а каждый цикл unregister → register за это время сбрасывал ожидание с нуля). Приложение ждёт ответа с бюджетом 60 с и растущим интервалом опроса, показывая «ждём launchd… N с»; только по исчерпании бюджета делает один полный цикл снятия и регистрации и ждёт ещё минуту, после чего показывает «зарегистрирован, но не отвечает». Кнопка на время операции неактивна — нажимать её повторно не нужно и нельзя.

Регистрировать сразу после снятия нельзя (замер 2026-09-10): register() через секунду после unregister() падает с SMAppServiceErrorDomain 1 «Operation not permitted», а через четыре секунды «проходит», но регистрация остаётся мёртвой — статус enabled, а job launchd не загружает ни за минуту. Регистрация после паузы поднимает демон мгновенно. Приложение ждёт исчезновения job (launchctl print system/<label> без root: код 0 — job есть, 113 — нет), выдерживает паузу и повторяет register() при временной ошибке.

Если job застрял окончательно (job state = spawn failed), launchd держит устаревший constraint, и ни register(), ни unregister()+register() из приложения его не обновляют. Снять запись может только root:

sudo launchctl bootout system/dev.sunnyday.lsvpncompanion.helper

После этого приложение видит notRegistered и регистрирует демон заново само.

Проверенные тупики, чтобы не повторять: самозавершение helper при смене бинаря (launchd всё равно не запускает заменённый бинарь под старой регистрацией) и перерегистрация из приложения (constraint остаётся прежним).

2. Little Snitch должен разрешить доступ своему CLI

Отдельный тумблер внутри самого Little Snitch. Пока он выключен, littlesnitch даже под root отвечает:

Error: command line tool is not authorized to make changes.
Please enable access in Little Snitch.app > Preferences > Security.

Включается в Little Snitch → Настройки → Безопасность. Приложение распознаёт именно эту ошибку и показывает отдельную подсказку с кнопкой «Открыть Little Snitch…». А без root littlesnitch печатает littlesnitch must be run as root! и выходит с кодом 14 — поэтому CLI и живёт в helper.

3. Формат модели Little Snitch 6

Структура export-model не документирована; снята с живой машины (2026-07-30 и 2026-09-09, Little Snitch 6.5) и закреплена тестами (Tests/HelperTests/LittleSnitchModelTests.swift):

"groups": {
  "aaaaac": {"type": "builtinMacOSServices", "isActive": true},
  "aaaaad": {"type": "builtinICloudServices", "isActive": true},
  "ghoGzc": {"type": "loadedFromFile", "userProvidedName": "Require VPN Services Local",
             "creationDate": "…"},
  "pykpmH": {"type": "loadedFromURL", "factoryName": "Require VPN Services",
             "loadFromURL": "https://example.com/require-vpn.lsrules",
             "lastSuccessfulUpdate": "…", "updateInterval": 86400}
}
  • имя берётся по цепочке полей userProvidedName → customName → factoryName: локальная группа хранит его в userProvidedName, подписка по URL (type: loadedFromURL) — имя из файла .lsrules в factoryName, переименованная пользователем подписка — в customName (допущение: в дампе ключ не встречался, свойство есть в бинаре CLI). У встроенных имени нет вовсе, их опознаёт type (builtinMacOSServices → «macOS Services»);
  • на type локальных групп опираться нельзя: в июльском снимке ключа не было, в сентябрьском появился loadedFromFile. Пропускается только запись без имени в любом из полей — правило «неизвестный тип пропускаем» до сентября 2026 как раз и прятало подписки;
  • isActive присутствует только у включённых групп любого вида: отсутствие ключа означает «выключена»;
  • ключи словаря (aaaaac) — внутренние идентификаторы LS, наружу они не отдаются;
  • rulegroup -e/-d принимает имя подписки как есть (проверено: -d "Require VPN Services" → код 0); флаг -u не нужен — root видит подписки в общей модели;
  • helper отдаёт приложению вид группы полем kind (builtin / local / remote). Поле необязательное: ответ без него читается, а во вкладке «Группы» подписка помечается словом «подписка».

Две ловушки подписок:

  • имя подписки живёт на сервере: если name в .lsrules изменится, группа выпадет из маппинга, и reconcile начнёт писать «группа … не найдена» — отметьте её заново;
  • одноимённые локальная группа и подписка для CLI неотличимы: он принимает только имя. Приложение покажет одну строку и предупредит в журнале; держите имена уникальными.

Если формат сменится с версией LS, парсер переходит к общему поиску по форме объекта, а при неудаче сообщает в журнал фактические ключи и форму — по ним разбор дописывается без гадания.

4. Мелочи эксплуатации helper

  • Версия helper включает отпечаток его бинаря, поэтому приложение замечает устаревший демон. Путь к своему бинарю helper берёт через _NSGetExecutablePath: argv[0] у launchd-демона путём не является, а Bundle.main внутри бандла приложения указывает на бинарь приложения — оба варианта давали версию build unknown.
  • Устаревший демон register() поверх не перезапускает: constraint обновляется, а процесс в памяти продолжает отвечать старой версией. Приложение замечает это по ответу и один раз за запуск перезапускает демон полным циклом снятия и регистрации.
  • SMAppService.unregister() завершается асинхронно: вызвать register() сразу нельзя — снятие регистрации отменит её. HelperLifecycle дожидается фактического .notRegistered перед повторной регистрацией.
  • XPC-вызовы ограничены таймаутом 6 с: зарегистрированный, но не одобренный демон не отвечает и не сообщает об ошибке — без таймаута запрос висел бы молча. К незагруженному сервису вызов, наоборот, падает мгновенно («No such process»), поэтому бюджет ожидания считается по часам, а не по числу попыток.
  • Замена бандла (install.sh, обновление через Finder) сбрасывает запись BTM: статус становится notRegistered при живом старом демоне. Если онбординг пройден и helper не удалял пользователь, приложение регистрирует демон заново само и ждёт ответа, как после любой другой регистрации.
  • Вся логика регистрации — в Application/HelperLifecycle.swift: одно состояние helper на все экраны, не больше одной операции за раз, кнопка «Установить…» / «Переустановить…» / «Удалить…» по состоянию. Тесты — HelperLifecycleTests.
  • «Удалить…» снимает регистрацию без подтверждения в любом режиме, предварительно передав helper неактивный failsafe-конфиг; факт удаления запоминается (helperRemovedByUser в defaults), и авто-восстановление его уважает до следующего «Установить…». В строгом режиме без helper группы не переключаются, а эскалация по Wi-Fi при утечке сохраняется — об этом пишется в журнал.

Скрытые debug-рычаги (приёмка §14)

defaults write dev.sunnyday.lsvpncompanion debugIgnoreWarp -bool true

Игнорировать warp=on — приложение должно за ≤5 с зафиксировать Leak и включить группы, а при снятии флага само вернуться в Protected.

defaults write dev.sunnyday.lsvpncompanion debugFakeEgressIP -string "198.51.100.10"

Подменить egress маяка: адрес из вашего forbiddenEgressIPs даёт диагноз «цепочка вышла напрямую с сервера», текущий прямой РУ-IP — «полный обход VPN».

Снять рычаги:

defaults delete dev.sunnyday.lsvpncompanion debugIgnoreWarp; defaults delete dev.sunnyday.lsvpncompanion debugFakeEgressIP

Изменения подхватываются на следующей пробе — перезапуск не нужен: проберы читают настройки в момент запроса.

Журнал

~/Library/Application Support/LittleSnitchVPNCompanion/journal.jsonl — JSONL, ротация 7 дней. Окно журнала (поповер → «Журнал…») даёт фильтры Все/Переходы/Действия/Ошибки, экспорт в текст и очистку.

Иконка

Нарисована в design/app.pen (лист «Иконка приложения») и визуально перекликается с иконкой Little Snitch: синий градиентный квадрат, белая орбитальная разметка и двухлопастной янтарный ротор. Центральный замок передаёт назначение компаньона: безопасная работа сети и блокировка при отсутствии VPN. Артворк собственный — PNG Objective Development не используются. Все размеры 16–1024 px собраны из одного финального мастера; превью в начале README использует тот же AppIcon-набор.

Статус и ограничения

  • Диапазон macOS — 15.7–26.x, обе границы проверены живой эксплуатацией: macOS 26.x на Apple Silicon и macOS 15.7.8 (Sequoia) на Intel, где приложение работает несколько дней без отличий в поведении. Сборка и тесты на обеих границах прогоняются в CI.
  • Заточено под конкретный сценарий: цепочка с Cloudflare WARP последним звеном и российский провайдер как «прямой» путь. Другие конфигурации работают через expectedIPs, но именно WARP-эвристика (warp=on) — основной сигнал «защищён».
  • РУ-маяки (yandex.ru, 2ip.ru) имеют смысл, когда «прямой» выход — в России; URL обоих маяков меняются в настройках.
  • Little Snitch — не песочница: rule groups фильтруют новые соединения, но уже установленные могут дожить до своего таймаута.

Контрибуция

Правила — в CONTRIBUTING.md. Главное: main принимает только PR с зелёным CI, а любое изменение логики идёт через Spec-Driven Development (OpenSpec) и обновляет спеки в openspec/specs/.

Лицензия

MIT. Little Snitch — продукт Objective Development; этот проект с ними не аффилирован.

About

macOS menu bar app that watches your real egress IP and flips Little Snitch deny-rule groups when traffic leaks past your VPN

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages