Netfence работает как демон на хостах виртуальных машин и контейнеров: автоматически внедряет программы eBPF-фильтров в контрольные группы (cgroups) и сетевые интерфейсы, а встроенный DNS-сервер разрешает разрешённые домены и наполняет список допустимых IP-адресов.
Как Envoy xDS, только для eBPF-фильтров.
Демоны Netfence могут управляться исключительно через локальный API на Unix-сокете или подключаться к центральной управляющей плоскости (control plane), которую вы реализуете через gRPC, для синхронизации списков разрешений и запретов с вашим бэкендом.
Управляющая плоскость отправляет сетевые правила вида ALLOW *.pypi.org или ALLOW 10.0.0.0/16 на подключённые интерфейсы и cgroups. Когда виртуальная машина или контейнер выполняет DNS-запрос, Netfence разрешает его, добавляет IP-адреса в eBPF-фильтр и блокирует трафик к неизвестным адресам ещё на хосте — накладные расходы на «прогретом» пути (warm-path) в текущих бенчмарках практически неотличимы от обычного вызова connect на сокете.
Возможности
-
Подключение eBPF-фильтров к сетевым интерфейсам (TC) или cgroups
-
Режимы политик: отключён, список разрешений, список запретов, полная блокировка
-
Поддержка IPv4 и IPv6 CIDR с опциональными TTL
-
DNS-сервер UDP/TCP для каждого вложения (attachment) с белым/чёрным списком доменов и упорядоченными вышестоящими серверами
-
Правила для доменов поддерживают поддомены с сопоставлением по специфичности (побеждает более конкретное правило)
-
Автоматическое добавление разрешённых доменов в IP-фильтр по результатам резолвинга
-
Метаданные для демонов и вложений — для привязки к идентификатору виртуальной машины, тенанту и т.д.
-
Поддержка проксирования DNS-запросов на управляющую плоскость для принятия DNS-решений на уровне отдельного вложения
Замечание по безопасности: исключения по умолчанию
В режиме списка разрешений IPv4 link-local (169.254.0.0/16) больше не разрешён автоматически по умолчанию — сервис метаданных облака (169.254.169.254) заблокирован, если явно не добавлен в список разрешений. Это сделано намеренно: сервис метаданных является мишенью для кражи учётных данных, и изолированные рабочие нагрузки не должны иметь к нему неявный доступ. Localhost (127.0.0.0/8, ::1) и IPv6 neighbor discovery (fe80::/10, ff02::/16) по-прежнему разрешены по умолчанию, чтобы обеспечить базовую связность и работу NDP. Чтобы разрешить сервис метаданных для конкретной нагрузки, добавьте 169.254.169.254/32 в список разрешений (переопределение на уровне отдельного вложения через управляющую плоскость запланировано в дальнейшем).
IPv4 broadcast (255.255.255.255) и multicast (224.0.0.0/4) исключений не имеют и подпадают под действие политики: в режиме списка разрешений через TC трафик вида DHCP-renewal broadcasts блокируется, если не разрешён явно. Проверки исключений выполняются до применения списка запретов, поэтому заблокировать диапазон из исключения можно только отключив само исключение. Поскольку IPv4 link-local теперь отключён по умолчанию, режим списка запретов тоже способен блокировать сервис метаданных.
Отличия от других решений
Несколько ключевых преимуществ, которые большинство других решений не предоставляют:
-
Немедленный разрыв существующих соединений при изменении правил на запрет IP-адреса (только для вложений на интерфейс)
-
Поддержка всех сетевых протоколов и прямого подключения к IP. Например, отличный httpjail не позволяет подключаться напрямую к IP-адресам или устанавливать прямые TCP/UDP-соединения — например, для баз данных
-
Динамическое разрешение DNS с предварительной фильтрацией (исключается эксфильтрация вида
secretdata.someattacker.com)
Насколько мне известно, ни одно другое решение не объединяет все эти возможности одновременно.
Известное ограничение: вложения через cgroup фильтруют на уровне сокетов (хуки connect/sendmsg), поэтому процесс с CAP_NET_RAW может создавать сырые пакеты (raw packets), обходящие их. Для рабочих нагрузок, которые могут иметь CAP_NET_RAW, используйте вложение через TC (интерфейс), фильтрующее на уровне устройства.
При этом подходе накладные расходы несколько выше, чем у таких решений, как httpjail.
Снимок производительности
Приведённые числа получены в привилегированном окружении Docker Linux на linux/arm64 с помощью make bench-docker. Значения — медианы пяти замеров.
Прогретый путь сокета (warm socket path)
Бенчмарк прогретого сокета использует подключённые UDP-сокеты, чтобы изолировать стоимость хука eBPF cgroup/connect4 от задержки TCP-рукопожатия. На этом пути DNS уже разрешил домен, IP находится в пределах TTL, а IP/CIDR уже присутствует в eBPF-таблице (map).
| Путь | Медианная задержка |
|---|---|
Обычный connect на сокете без eBPF |
~2.647 мкс |
Прогретый список разрешений, совпадение по защищённому LPM |
~2.691 мкс |
Прогретый список разрешений, совпадение по точному DNS-хосту |
~2.741 мкс |
Промах в списке разрешений, локальная блокировка |
~1.652 мкс |
Измеренный разброс между обычным путём, защищённым LPM и точным DNS-хостом укладывается в шум замеров.
Пути «промах в ядре — запрос к родительскому процессу» на данный момент не существует. Промах в cgroup-списке разрешений обрабатывается локально eBPF и сразу же блокируется.
Путь DNS-запроса
Эти числа измеряют путь DNS-сервера, а не прогретый путь connect.
| Путь | Медианная задержка |
|---|---|
Холодный проксируемый запрос, функция политики in-process |
~31.336 мкс |
Прогретый проксируемый запрос |
~27.964 мкс |
Холодный запрос через список разрешений с локальным вышестоящим сервером |
~53.510 мкс |
Прогретый запрос через список разрешений с локальным вышестоящим сервером |
~53.432 мкс |
«Холодные» строки синхронизируются через реальный барьер мутации вложения и сбрасывают граф владения бенчмарка и снимок фиктивной exact-таблицы между запросами. Таймер работает непрерывно для сохранения локальности UDP-планировщика, а ns/op вычитает отдельно сообщаемое wall-time fixture-reset-ns/op (включая хвост предыдущего обработчика после получения пакета клиентом) и тем самым измеряет текущий клиентский Exchange. raw-total-ns/op отражает оба значения вместе. Сброс сохраняет настроенные домены политики и базовое хранилище; бенчмарк проверяет одно физическое добавление в exact-таблицу на запрос. «Прогретые» строки инициализируют владение один раз и проверяют одно физическое добавление на всю серию.
Приведённые ниже внутренние микробенчмарки владения — это диагностика масштабируемости, а не приёмочные строки для сквозного пути DNS-запросов. Кешированный хелпер оставлен только для тестов и бенчмарков; он оборачивает по одной записи за раз и повторяет валидацию домена. Как он, так и обычный трафик резолвера проходят через барьер мутации вложения, тогда как обычный трафик принимает каждый полный ответ одной транзакцией.
| Внутренняя диагностика масштабируемости | Текущая медиана | Память / аллокации |
|---|---|---|
Холодное добавление нового ключа, пустой граф владения |
~370.3 нс |
232 Б, 5 allocs/op |
Холодное добавление нового ключа, 4095 несвязанных записей |
~451.2 нс |
232 Б, 5 allocs/op |
Давление на физическую ёмкость и LRU-замена |
~3.820 мс |
~4.23 МБ (4 226 243 Б), 4336 allocs/op |
Предварительная проверка при исчерпанном физическом бюджете |
~611.9 нс |
344 Б, 9 allocs/op |
Максимальное давление рёбер, ответ на 64 адреса |
~6.849 мс |
~7.66 МБ (7 658 774 Б), 2233 allocs/op |
Максимальный рабочий guard графа, допустимый полный план |
~2.763 мс |
~4.26 МБ (4 264 386 Б), 3074 allocs/op |
Максимальный рабочий guard графа, отклонение при исчерпанной предварительной проекции |
~10.935 мкс |
8.76 КБ (8760 Б), 14 allocs/op |
Операция churn-бюджета вблизи числового потолка |
~18.98 нс |
0 Б, 0 allocs/op |
Согласованный снимок статистики владения |
~2.094 нс |
0 Б, 0 allocs/op |
Сканирование истёкших записей без действий, 4095 записей |
~74.849 мкс/сканирование |
0 Б, 0 allocs/op |
Архитектура
+------------------+ +-------------------------+
| Ваша управляющая|<------->| Демон (на хост) |
| плоскость (gRPC)| поток | |
+------------------+ | +-------------------+ |
| | DNS-сервер | |
| | (на вложение) | |
| +-------------------+ |
+-------------------------+
|
+------+------+
| |
TC-фильтр Cgroup-фильтр
(veth, eth) (контейнеры)
Каждое вложение получает уникальный DNS-адрес (порт), выделяемый демоном. Контейнеры и виртуальные машины должны быть настроены на использование назначенного им DNS-адреса; фильтрация обычного DNS-трафика рабочих нагрузок не перенаправляет его прозрачно.
Топология и поведение DNS-резолвера
dns.listen_addr должен указывать на конкретный IPv4- или IPv6-адрес, доступный каждой подключённой рабочей нагрузке. Wildcard-адреса отклоняются, поскольку их нельзя объявить как конечные точки резолвера. Настроенное имя хоста разрешается один раз при запуске демона, а полученный конкретный IP используется для привязки, объявления, хранения и начальной загрузки фильтра. Значение по умолчанию 127.0.0.1 подходит только тогда, когда рабочая нагрузка разделяет сетевое пространство имён (network namespace) с демоном; контейнеру или виртуальной машине в другом namespace обычно требуется доступный адрес хоста или моста.
dns:
listen_addr: 10.0.0.1
port_min: 11000
port_max: 11500
# Глобальный запасной вариант демона, когда DnsConfig.upstream_servers пуст.
upstream: 1.1.1.1:53
# Жёсткие потолки демона для ограниченного DNS exact-владения каждого вложения.
# Ноль/не задано использует эти значения по умолчанию (max_ips_per_family
# вместо этого выводится из filter.max_dns_rule_entries).
max_ips_per_family: 4096
max_ips_per_response: 64
max_ips_per_policy_domain: 1024
max_tracked_domains: 1024
max_ownership_edges: 8192
# Скользящий бюджет физических допущений/LRU-мутаций и допустимая
# трудоёмкость медленного планирования. Окно глобальное для демона и
# неизменяемо до перезапуска; DnsConfig.max_churn_units может только
# снизить потолок демона.
max_churn_units: 8192
churn_window: 1m
Attach возвращает конкретный dns_address; настройте именно этот адрес как резолвер рабочей нагрузки. Netfence устанавливает защищённую, не истекающую запись /32 или /128 для IP слушателя, чтобы режим списка разрешений мог загрузиться без правила DNS-IP от управляющей плоскости. Текущие фильтры применяют IP-префиксы, а не порты назначения, поэтому эта защищённая запись разрешает все порты на IP слушателя (не только его DNS-порт) — это особенно важно для моделей угроз при вложении в cgroup. Используйте выделенный IP слушателя, если такая широкая доступность неприемлема.
Назначенная конечная точка обслуживает UDP и TCP одновременно. UDP-ответы обрезаются до 512-байтового лимита устаревшего клиента или до объявленного им размера EDNS и несут флаг TC при необходимости — это позволяет рабочей нагрузке повторить запрос к той же конечной точке по TCP. При резолвинге на вышестоящем сервере усечённый UDP-ответ сначала повторяется по TCP к тому же вышестоящему серверу. Сбой транспорта, SERVFAIL или REFUSED переключают запрос на следующий настроенный вышестоящий сервер по порядку.
DnsConfig.upstream_servers переопределяет глобальный dns.upstream демона для одного вложения. Записи используют синтаксис host:port (IPv6-литералы в скобках), канонизируются и дедуплицируются в порядке первого появления, максимум восемь уникальных серверов. Пустой список выбирает глобальный запасной вариант.
В режимах фильтрации Netfence удаляет параметры ipv4hint и ipv6hint из ответов HTTPS/SVCB, включая соответствующие ссылки mandatory, поскольку подсказанные адреса независимо не прошли допуск фильтра. В отключённом режиме ответы вышестоящего сервера передаются без изменений.
Счётчики DNS-запросов взаимно исключают друг друга: dns_queries_allowed считает успешно отвеченные запросы, разрешённые политикой (включая NXDOMAIN), dns_queries_blocked — ответы REFUSED по политике, dns_queries_errors — пути ошибок резолвера, прокси, допуска фильтра, записи ответа и прочих. Каждый запрос увеличивает ровно один счётчик.
Каждый содержащий адреса ответ в фильтрующем режиме DNS принимается в exact IPv4/IPv6 HASH-уровень вложения одной транзакцией до возврата любого A/AAAA-адреса из секций ответа, авторитетной части или дополнительных секций. Если полный ответ не может быть представлен, резолвер возвращает SERVFAIL без адреса и сохраняет ранее принятое рабочее множество. Решения PROXY, возвращающие адреса, должны устанавливать add_to_filter; иначе они тоже закрываются с SERVFAIL. Отключённый DNS-режим является явным исключением сквозной передачи.
Exact-записи несут TTL-рёбра от нормализованного запроса к совпавшему владельцу политики. Удаление или запрет домена оперативно удаляет его последние DNS-only exact-адреса, тогда как общий адрес сохраняется, пока у него есть другой живой владелец запроса, а перекрывающийся CIDR управляющей плоскости продолжает независимо существовать в защищённом LPM-уровне. DNS DENYLIST default-allow и explicit-allow ответы тоже отслеживаются — даже пока пакетный DENYLIST игнорирует exact-разрешения — чтобы при последующем переключении пакетного режима в ALLOWLIST можно было использовать уже возвращённые кешированные адреса без повторного запроса.
Всё обычное/живое пользовательское состояние владения ограничено пятью настройками выше. Восстановленные синтетические временные рёбра освобождены от этих логических лимитов, чтобы их нельзя было забыть до сверки, но остаются ограничены физическими exact-картами IPv4/IPv6. Настроенные домены политики и живые домены запросов совместно используют max_tracked_domains, а каждая запись TTL (запрос, совпавший владелец, IP) занимает один слот max_ownership_edges.
При давлении допуск сначала проецирует прочь истёкшие TTL-рёбра. Затем он возвращает полные логические рёбра запроса/владельца с наименьшим физическим сопутствующим ущербом перед новизной, после чего применяет детерминированный наблюдаемый резолвером LRU (канонический IP разрешает равенство). Физическое вытеснение удаляет весь DNS exact-ключ и всех его DNS-владельцев. Входящий физический IP и точное входящее ребро (IP, запрос, владелец) защищены на время транзакции ответа. Восстановленное временное владение защищает свой физический ключ до авторитетной сверки, но несвязанные обычные DNS-метаданные, разделяющие этот ключ, всё равно могут быть возвращены. Авторитетные разрешения управляющей плоскости/системы и все запреты остаются в отдельных LPM-уровнях и никогда не являются кандидатами для DNS-возврата.
Скользящий бюджет churn на вложение начисляет одну единицу за новый физический exact-ключ и одну за каждый вытесненный живой физический DNS-ключ; полная замена старого на новый стоит две единицы. Обновления, только логический возврат, истечение и удаление политики стоят ноль. События остаются активными, пока их возраст меньше dns.churn_window, и истекают ровно на границе. DnsConfig.max_churn_units может только снизить потолок демона; ноль наследует его. Окно не может быть изменено управляющей плоскостью, а изменение потолка/окна демона требует перезапуска. Снижение и последующее повышение лимита вложения не сбрасывает всё ещё активную историю.
Отдельный скользящий реестр работы ограничивает дорогостоящее планирование графа владения. Быстрые обновления и обычные допуски его никогда не затрагивают. Перед тем как путь давления клонирует или анализирует граф, Netfence начисляет стабильные рабочие единицы, выведенные из текущих физических ключей, рёбер владения, отслеживаемых доменов и размера ответа относительно неизменяемых потолков демона/таблицы. Эта попытка начисления сохраняется, даже если план оказывается невозможным или последующая транзакция exact-таблицы терпит неудачу — это закрывает путь повторной попытки без мутаций при давлении CPU/аллокаций, не изменяя транзакционный физический churn-учёт выше. При потолках по умолчанию допуск обеспечивает восемь эквивалентных максимальных проходов графа в окне; снижение DnsConfig.max_churn_units оставляет как минимум один. Снижение и последующее повышение лимита никогда не пересчитывает и не забывает активную историю работы.
Когда ни одно подходящее DNS-состояние не удовлетворяет ограничению или какой-либо из скользящих допусков исчерпан, Netfence сохраняет принятое рабочее множество и возвращает SERVFAIL, не возвращая недопущенный адрес. Отказы ёмкости увеличивают map_full_drops; дроссели физического churn и работы планирования — нет. Heartbeat-ы открывают текущее состояние exact-таблицы, ёмкость и максимальные значения поколения процесса, а также совокупные LRU-вытеснения DNS, все отказы допуска и агрегированный счётчик дросселя бюджета, охватывающий оба скользящих стража. Логи давления/восстановления ёмкости, физического бюджета и рабочего бюджета ограничены по частоте независимо. Операторы могут дождаться восстановления TTL/окна, снизить churn ответов/доменов или повторные попытки при давлении на потолок либо поднять DnsConfig.max_churn_units до потолка dns.max_churn_units демона. Поднятие потолка демона требует перезапуска; увеличение filter.max_dns_rule_entries также требует пересоздания вложения, поскольку закреплённые (pinned) таблицы нельзя изменить в размере на месте.
Защищённая ёмкость CIDR и восстановление при закрытом отказе
Авторитетные CIDR управляющей плоскости и правила системы демона используют четыре независимых, невытесняемых LPM-таблицы: allow/deny × IPv4/IPv6. Каждая таблица имеет filter.max_rule_entries слотов. Начальная запись /32 или /128 DNS-слушателя является системным разрешением и учитывается в соответствующей защищённой allow-таблице. Exact-host записи DNS остаются в своих отдельных таблицах и не могут занимать эти слоты. Ни одно защищённое правило никогда не вытесняется LRU: явные разрешения, системные правила и все запреты остаются до авторизованного удаления или полной замены.
Полный SubscribedAck или BulkUpdate канонизируется, и его итоговая заполненность проверяется для всех четырёх таблиц перед мутацией. Ёмкость основана на итоговом состоянии, поэтому замена ключей в полной таблице допустима; избыточное состояние отклоняется без вытеснения или частичного принятия правил. Уцелевшие не удаляются и не добавляются повторно. Если последующий вызов syscall таблицы завершится неудачей, Netfence восстанавливает и проверяет точный инвентарь четырёх таблиц перед вызовом. Доказанный режим после отката — старый режим или BLOCK_ALL (обычно BLOCK_ALL), поэтому демон удерживает вложение в закрытом состоянии (fail-closed) до успешной полной авторитетной повторной попытки.
Состояние безопасности защищённой политики сохраняется вместе с вложением и экспортируется в heartbeat-ах. BLOCK_ALL сам по себе является нормальным, исправным настроенным режимом: policy_degraded имеет значение false, когда policy_degraded_reason пуст. Рискованная защищённая мутация, начинающаяся при исправном BLOCK_ALL, сначала журналирует protected_policy_mutation_in_progress. Это временный журнал сбоев при краше, а не стабильная диагностика: живая операция может опубликовать предполагаемый режим до окончательного сохранения с очисткой журнала, а успешное завершение само очищает журнал. Если при запуске он обнаружен после краша, запуск сначала принудительно доказывает BLOCK_ALL, затем сохраняет protected_policy_mutation_interrupted. Стабильные коды причины деградации:
-
protected_policy_mutation_interrupted -
authoritative_protected_policy_failed -
incremental_deny_install_failed -
incremental_allow_removal_failed -
incremental_mode_change_failed
Это стабильные классификации, никогда не содержащие сырой текст syscall/store. Журнал in-progress является внутренней сохранённой границей при краше; статистика heartbeat сериализуется вместе с владеющей мутацией и поэтому наблюдает либо её успешную очистку, либо стабильное преобразование отказа, но не живой промежуточный журнал. Стабильные причины деградации/прерывания удерживают пакетное применение в доказанном BLOCK_ALL и отклоняют инкрементальные команды CIDR и пакетного режима. Независимые изменения DNS-конфигурации и истечение TTL DNS могут продолжаться под этой доказанной блокировкой, но не могут очистить стабильную причину или повторно активировать пакетную политику. Для восстановления из стабильной причины требуется одно полное желаемое состояние LPM и DNS: примените BulkUpdate через управляющую плоскость или локальный API (или ответьте на свежий Subscribed восстановленного вложения с SubscribedAck). Netfence подготавливает полное защищённое состояние, применяет авторитетное DNS-состояние, активирует запрошенный режим и очищает устойчивую причину только после успешного выполнения всех шагов. При восстановлении BulkUpdate через управляющую плоскость предпочтительно использовать уникальный command_id и требовать успешного CommandResult; локальный API отклоняет command_id, поскольку его унарный RPC-результат уже сообщает об успехе или неудаче.
Heartbeat-ы открывают текущие физические записи, жёсткую ёмкость и максимальные значения поколения демона независимо для всех четырёх защищённых таблиц. Начальная загрузка включена; принятые закреплённые записи инициализируют максимальное значение нового поколения. map_full_drops является накопительным и включает отклонения при нехватке защищённой ёмкости. Если чтение заполненности завершается неудачей, демон сохраняет последний доказанный снимок и выдаёт предупреждение не чаще одного раза в 30 секунд вместо того, чтобы выдумывать новые счётчики. Для восстановления при давлении сократите полные желаемые правила ниже ёмкости каждой таблицы и повторите полное обновление. Поднятие filter.max_rule_entries применяется только при загрузке и требует пересоздания существующего закреплённого вложения. Если демон не может доказать BLOCK_ALL или надёжно записать свой маркер безопасности, он прекращает приём мутаций; устраните неисправность таблицы/хранилища и перезапустите, а не предполагайте, что применение открылось.
На хосте
Запустите демон, который:
-
Открывает локальный gRPC API (
DaemonService) для вложений, политики и инспекции -
Опционально подключается к вашей управляющей плоскости через двунаправленный поток (
ControlPlane.Connect) -
Загружает и управляет eBPF-программами
Запустить демон:
# Запуск с конфигурацией по умолчанию
netfenced start
# Запуск с пользовательским файлом конфигурации
netfenced start --config /etc/netfence/config.yaml
Проверить статус демона:
netfenced status
При отсутствии control_plane.url новое вложение фиксируется в отключённом режиме пакетной обработки/DNS и может быть настроено немедленно через локальный API или CLI. Для автономного рабочего процесса, описанного в разделе «На уровне вложения», процесс управляющей плоскости не требуется.
Граница доверия локального Unix-сокета
Локальный gRPC API не имеет аутентификации на уровне RPC. Границей авторизации является доступ к файловой системе его Unix-сокета, и каждый процесс, который может подключиться, является полностью доверенным администратором сети хоста: он может подключать или отключать хостовые eBPF-программы, заменять пакетную политику и DNS-политику, а также открывать или закрывать трафик рабочих нагрузок. Держите членство в группе сокета минимальным и защищайте родительский каталог сокета.
# По умолчанию /var/run/netfence.sock.
socket: /run/netfence/netfence.sock
# Имя Unix-группы или числовой GID. Пустое/не задано использует эффективный GID демона.
socket_group: netfence-admin
NETFENCE_SOCKET и NETFENCE_SOCKET_GROUP — эквивалентные переменные окружения. При запуске демон привязывает сокет в приватном промежуточном каталоге, устанавливает его группу и режим 0660, пока он недоступен, а затем публикует его атомарно. Демон удаляет любой существующий Unix-сокет по настроенному целевому пути — он не отличает устаревший сокет от сокета другого живого демона — поэтому ровно один демон должен владеть путём к сокету. Он отказывается удалять нецелевой сокет. В Linux переименование без замены (no-replace rename) предотвращает перезапись нового пути, созданного после этого удаления; при завершении демон удаляет опубликованный путь, только пока тот по-прежнему идентифицирует собственный inode сокета демона. Неверная группа, сбой владения/режима или нецелевой сокет прерывают запуск без публикации незащищённой конечной точки.
Безопасность транспорта управляющей плоскости (TLS / mTLS / bearer-токен)
Канал управляющей плоскости является наиболее ценной поверхностью атаки в системе (тот, кто контролирует его, может отправить правила ALLOW каждой рабочей нагрузке), поэтому демон закрывается при отказе (fails closed): если control_plane.url задан, конфигурация должна явно выбрать транспорт — либо блок control_plane.tls, либо control_plane.insecure: true. URL без ни того ни другого отклоняется при запуске; неявного незашифрованного соединения по умолчанию нет. (Это намеренное изменение поведения: более старые версии молча подключались к управляющей плоскости без шифрования.)
control_plane:
url: cp.internal:443
tls:
# Пакет CA для проверки серверного сертификата управляющей плоскости.
# Путь к PEM-файлу или встроенный PEM; опустите для использования системного пула root.
ca: /etc/netfence/cp-ca.pem
# Клиентский сертификат + ключ (путь или встроенный PEM). Установка ОБОИХ
# включает mTLS: демон предъявляет этот сертификат управляющей плоскости.
# Установка только одного из них является ошибкой конфигурации.
cert: /etc/netfence/daemon.pem
key: /etc/netfence/daemon.key
# Опциональное переопределение имени хоста для проверки серверного сертификата (SNI),
# например при подключении по IP.
server_name: cp.internal
# Опциональный bearer-токен, отправляемый как метаданные `authorization: Bearer <token>`
# в каждом RPC управляющей плоскости. Отклоняется на незашифрованном канале, если
# только не задан явно `insecure: true` (чтобы неверная настройка не могла его утечь).
auth_token: "..."
TLS только с системными корневыми сертификатами (серверный сертификат от публичного CA, без mTLS) — просто пустой блок:
control_plane:
url: cp.example.com:443
tls: {}
Незашифрованное соединение для локальной разработки — явное включение (взаимоисключает tls):
control_plane:
url: localhost:9000
insecure: true
Сертификаты и ключи загружаются один раз при запуске, поэтому неверный путь/PEM прерывает запуск с понятной ошибкой, а не проявляется при каждом переподключении.
Живость управляющей плоскости (keepalive) и откат при переподключении
Демон отправляет HTTP/2 keepalive-пинги по соединению с управляющей плоскостью, чтобы молчаливо мёртвый путь (обрыв кабеля, потерянное NAT-отображение, заблокированный маршрут) был обнаружен и разорван примерно за keepalive_time + keepalive_timeout — вместо того чтобы оставаться в состоянии CONNECTED минутами до истечения TCP retransmission timeout ядра, пока каждый проксируемый DNS-запрос съедает весь свой таймаут. Переподключения регулируются джиттерированным экспоненциальным откатом (начинается с 1 с, удваивается, ±20% джиттер, ограничен reconnect_backoff_max); откат сбрасывается к минимуму только после того, как соединение оставалось стабильным 30 с, поэтому управляющая плоскость, принимающая соединения и сразу их сбрасывающая, продолжает увеличивать откат, а не молотить на минимуме.
control_plane:
# Отправить keepalive-пинг после такого периода бездействия… (по умолчанию 30 с;
# gRPC ограничивает эффективный интервал минимумом 10 с на стороне клиента)
keepalive_time: 30s
# …и объявить узел мёртвым, если подтверждение не получено в течение этого времени (по умолчанию 10 с).
keepalive_timeout: 10s
# Потолок джиттерированного экспоненциального отката при переподключении (по умолчанию 30 с).
reconnect_backoff_max: 30s
Нулевые/незаданные значения означают значения по умолчанию — они не отключают keepalive или откат. Ваша управляющая плоскость должна разрешать эту периодичность пингов в своей политике применения keepalive gRPC (см. ниже), иначе она отклонит демон с ENHANCE_YOUR_CALM (too_many_pings).
Перезапуски, краши и обновления демона (закреплённое BPF-состояние)
Демон закрепляет (pin) BPF-ссылки и таблицы правил каждого вложения в bpffs (filter.bpf_pin_dir, по умолчанию /sys/fs/bpf/netfence, по одному каталогу на ID вложения). Поскольку закреплённое состояние удерживается ядром, а не процессом демона, применение продолжается, пока демон не запущен: краш (kill -9), плановая остановка или обновление оставляют последнюю известную политику (режим + все правила) в действии, а следующий запуск демона повторно принимает закреплённое состояние как есть. Восстановление никогда не повторно подключает и не перезаписывает живые таблицы, поэтому нет окна, в котором разрешённая рабочая нагрузка оказывается заблокирована или заблокированный адрес становится доступен, и нет дублирующихся вложений.
Поведение при остановке является явной настройкой (filter.detach_on_stop):
filter:
# false (по умолчанию): остановка демона СОХРАНЯЕТ ПРИМЕНЕНИЕ — фильтры остаются
# подключёнными через закрепления bpffs и повторно принимаются при следующем запуске
# (fail-closed при перезапусках/обновлениях).
# true: остановка демона отключает фильтры и удаляет их закрепления —
# трафик нефильтруется, пока демон не запущен (явный fail-open).
detach_on_stop: false
# Каталог bpffs для закреплённого состояния. Должен быть на точке монтирования bpffs;
# демон монтирует bpffs в /sys/fs/bpf при необходимости (с привилегиями). Явная "" полностью
# отключает закрепление (BPF-состояние тогда умирает вместе с процессом).
bpf_pin_dir: /sys/fs/bpf/netfence
# Ёмкость каждой авторитетной/системной LPM-таблицы (разрешения/запреты на адресное семейство).
# Защищённые записи невытесняемы; начальная загрузка DNS-слушателя занимает один слот
# в своём адресном семействе. Изменение ёмкости закреплённой таблицы требует пересоздания вложения.
max_rule_entries: 4096
# Независимая ёмкость каждой DNS-производной exact-host HASH-таблицы (IPv4/IPv6).
# Эти записи никогда не могут занять или вытеснить авторитетную/deny ёмкость.
max_dns_rule_entries: 4096
Явный Detach (RPC/CLI) или удаление живого согласованно-принадлежащего целевого объекта уничтожает закреплённое состояние вместе с вложением. При перезапуске отсутствие цели не авторизует угадывание: будущие, не зафиксированные, смешанные или иначе непроверяемые сохранённые закрепления сохраняются, а запуск прерывается для проверки.
Каталоги закреплений являются версионированным форматом хранения. Маркер схемы закрепляется последним, только после того как существуют все необходимые таблицы и ссылки. Обновление вложения предшествующего exact-уровня закрепляет две новые пустые exact-таблицы с маркером in-progress, атомарно заменяет программу каждой ссылки, повторно используя живые авторитетные таблицы, проверяет идентичность программы/таблицы и фиксирует маркер последним. Краш или неоднозначное обновление оставляет маркер незафиксированным; следующий запуск повторно обновляет каждую ссылку, используя те же таблицы. Старые и новые поколения программ применяют одну авторитетную LPM-политику во время этого ограниченного смешанного состояния, поэтому миграция никогда не открепляет и не пересоздаёт рабочий фильтр. Неизвестные, неполные или непроверяемые наборы закреплений сохраняются и прерывают запуск для проверки вместо того, чтобы быть угаданными.
Замечания о повторно принятом состоянии:
-
Каждое успешно восстановленное вложение помечается для авторитетной сверки. При каждом подключении к управляющей плоскости демон сначала отправляет
SyncRequest, затем полное объявлениеSubscribedдля каждого восстановленного вложения, которое ещё нуждается в сверке. Ответьте свежимSubscribedAck: его режим, CIDR, TTL и DNS-конфигурация являются полным желаемым состоянием. Демон применяет дельту (неизменённые CIDR никогда не удаляются) и очищает маркер восстановления только после полного применения ack. Таймаут, разрыв соединения или сбой валидации оставляют применение без изменений. Сбой применения фильтра/таблицы/DNS/хранилища может оставить частичную дельту, но сверка не использует полную очистку таблицы или удаление/повторное добавление неизменённых выживших; маркер восстановления остаётся установленным, и демон повторяет попытку после последующего подключения. -
Дедлайны TTL правил сами по себе не сохраняются. Повторно принятые правила временно считаются постоянными до получения свежего
SubscribedAck; этот авторитетный ack заменяет их сроки точно, включая сокращение дедлайна или превращение временно постоянного правила обратно в правило с конечным TTL. Восстановленные exact DNS-ключи инвентаризируются и представляются ограниченными временными владельцами (фактические ёмкости закреплённых таблиц являются ограничением); первая авторитетная DNS-конфигурация отбрасывает все синтетические претензии, удаляет ключи без владельца и сохраняет ключ только при наличии у него отдельного живого нормального владельца. Неканоническая/конфликтующая инвентаризация прерывает восстановление без угадывания или частичной публикации метаданных владения. -
Локального документа желаемого состояния нет. Если управляющая плоскость не настроена или недоступна, автоматических изменений правил не происходит: принятая закреплённая таблица продолжает применять свои последние известные защищённые CIDR, представленные в реестре пользовательского пространства как временные до полного обновления. Восстановление, которое не может принять действительные закрепления, пересоздаёт вложение в его сохранённом режиме с пустыми таблицами (fail-closed для allowlist/block-all). Автономный оркестратор должен воспроизвести
netfenced apply-rulesпосле каждого перезапуска демона, чтобы заменить временное пакетное состояние и восстановить полное желаемое состояние и TTL. -
Правила DNS-доменов, переопределения вышестоящих серверов на уровне вложения и DNS-лимиты вложения являются желаемым состоянием времени выполнения, предоставляемым через локальный API или управляющую плоскость; они не сохраняются. Восстановленное вложение, последний DNS-режим которого был ALLOWLIST, DENYLIST или PROXY, запускает свой резолвер в пустой позиции ALLOWLIST, возвращая
REFUSED, пока не будет применён полныйBulkUpdateилиSubscribedAck. Явно DISABLED DNS-режим остаётся пересылающим. Если один из зафиксированных UDP/TCP-слушателей позже неожиданно умирает, вложение помещается в карантин в IPBLOCK_ALLи сообщается как ошибка отписки. -
DNS-сервер на уровне вложения является компонентом пользовательского пространства и останавливается вместе с демоном; пока демон не запущен, закреплённые уже разрешённые exact IP продолжают работать под последней известной пакетной политикой, но новые имена через него разрешить невозможно. Их утраченные дедлайны пользовательского пространства считаются временными, а не угадываются до авторитетной сверки.
На уровне вложения
Ваша система оркестрации вызывает локальный API демона.
RPC:
DaemonService.Attach(interface_name: "veth123", tc_direction: TC_DIRECTION_INGRESS, metadata: {vm_id: "abc"})
// или
DaemonService.Attach(cgroup_path: "/sys/fs/cgroup/...", metadata: {container_id: "xyz"})
CLI:
# Подключение к хостовому veth-пиру или VM tap (TC) — используйте направление ingress
netfenced attach --interface veth123 --direction ingress --metadata vm_id=abc
# Подключение к cgroup
netfenced attach --cgroup /sys/fs/cgroup/... --metadata container_id=xyz
# Подключение к аплинку внутри собственного netns рабочей нагрузки (TC) — egress по умолчанию
netfenced attach --interface eth0 --metadata tenant=acme,env=prod
Направление TC: поле tc_direction (CLI --direction) выбирает, к какому хуку TCX подключается фильтр, и правильный выбор зависит от того, с какой стороны ссылки находится интерфейс:
| Интерфейс | Правильное направление | Почему |
|---|---|---|
Аплинк (напр. |
|
Исходящие пакеты рабочей нагрузки передаются через него; адрес назначения является истинным адресом назначения. |
Хостовой veth-пир или VM tap (напр. |
|
Исходящие пакеты рабочей нагрузки поступают на хост через этот интерфейс. Egress там будет вместо этого видеть возвратный трафик хост→рабочая нагрузка и фильтровать по собственному адресу рабочей нагрузки, а не по истинному адресу назначения. |
Направление применяется только к вложениям на интерфейс (TC); для вложений cgroup оно игнорируется.
-
Демон подключает eBPF-фильтр к целевому объекту.
-
Когда
control_plane.urlнастроен, демон отправляетSubscribed{id, target, type, metadata}и ждётSubscribedAckс начальной конфигурацией (режим, CIDR, DNS-правила). Если управляющая плоскость не отвечает в течение таймаута (по умолчанию 5 с, настраивается черезcontrol_plane.subscribe_ack_timeout), вложение откатывается и вызов attach завершается с ошибкой. Сбои валидации и другие сбои до фиксации следуют тому же правилу обычного отката. -
При отсутствии настроенной управляющей плоскости attach фиксируется немедленно в отключённом режиме; используйте приведённые ниже локальные команды политики для его настройки.
-
Допустимая начальная политика, достигающая сбоя защищённой таблицы/хранилища, является намеренным исключением зафиксированной ошибки: демон сохраняет вложение в надёжном
BLOCK_ALLвместо его деструктивного отката. При ограниченном таймаутеAttachвозвращает ошибку, содержащую сохранённый ID вложения; вызывающий может обнаружить этот ID, сопоставив цель вList, и управляющая плоскость должна восстановить его полнымBulkUpdate. -
При
subscribe_ack_timeout: 0новыйAttachвозвращает управление после постановкиSubscribedв очередь; последующий ack всё равно валидируется и применяется. Это нулевое значение не отключает сверку восстановленных вложений: попытки восстановления ждут до 5 с в фоне и повторяют попытку при последующем подключении. Если этот поздний ack натолкнётся на защищённое давление, уже возвращённое вложение сохраняется вBLOCK_ALL;SubscribedAckне выдаёт ниCommandResult, ни ошибочныйUnsubscribed, и демон автоматически не перезапускает объявление до перезапуска. Определяйтеpolicy_degradedи его причину, заполненность/ёмкость иmap_full_dropsвHeartbeat, затем отправляйте полныйBulkUpdateсcommand_idдля получения явного результата восстановления. -
Демон отслеживает удаление цели и автоматически отправляет
Unsubscribed
RPC:
DaemonService.Detach(id)
CLI:
netfenced detach --id <attachment-id>
Список вложений:
netfenced list
netfenced list --all # получить все страницы
Локальная политика и инспекция
Каждая локальная мутация является тонкой CLI-кодировкой единственного RPC DaemonService.ApplyCommand(ControlCommand). Укажите ID вложения, возвращённый командой attach:
# Пакетная политика и защищённые CIDR.
netfenced set-mode <id> allowlist
netfenced allow-cidr <id> 10.0.0.0/8
netfenced allow-cidr <id> 192.0.2.10/32 --ttl 5m
netfenced deny-cidr <id> 10.20.0.0/16
netfenced remove-cidr <id> 10.20.0.0/16 --list deny
# --list принимает allow, deny или both (по умолчанию).
# DNS-политика.
netfenced set-dns-mode <id> denylist
netfenced allow-domain <id> example.com --subdomains
netfenced deny-domain <id> blocked.example.com
netfenced remove-domain <id> blocked.example.com
# Детерминированная инспекция текущей политики в виде protobuf JSON.
netfenced rules <id>
Пакетные режимы: disabled, allowlist, denylist, block-all; DNS-режимы: disabled, allowlist, denylist, proxy. DNS proxy требует доступной настроенной управляющей плоскости. Сопоставление доменов использует наиболее конкретный совпадающий суффикс; при одинаковой специфичности правил allow и deny оба совпадают — побеждает deny. CIDR и домены канонизируются. Отрицательные, некорректные или иначе недействительные TTL, перечисления, CIDR, домены, селекторы и вложенные сообщения отклоняются до мутации, поэтому недействительная команда является no-op политики.
Для полной замены apply-rules читает существующую форму BulkUpdate в protobuf JSON из файла или stdin:
cat >rules.json <<'JSON'
{
"mode": "POLICY_MODE_ALLOWLIST",
"allowCidrs": [{"cidr": "10.0.0.0/8"}],
"dns": {
"mode": "DNS_MODE_DENYLIST",
"denyDomains": [{"domain