Zapret: what's new

Немного удалось ускорить обработку большого потока пакетов на windivert за счет bulk mode.
В bulk mode windivert может выдать за 1 обращение в ядро сразу множество пакетов, если они идут пачкой. Буфер выставлен в 192K при макс количестве пакетов 128. Как раз по 1500.

Тестировалось через iperf на loopback. Без bulk - 17.4G, c bulk - 18.6G
Проц не требует митигации от meltdown, выигрыш 7%., учитывая, что по лупбэку идут пакеты 64K, их помещается только 3.
На виртуалке linux через host only network прирост около 30-40%. Там идут стандартные мелкие пакеты.
Если включена митигация от meltdown, скорее всего выигрыш будет еще больше, тк syscall-ы дороже.

Если все упирается не в процессор, а в скорость сети, то профит будет не в возрастании скорости, а в уменьшении cpu usage

Потенциально может быть минорное замедление на arm64 при процессинге пакетов за счет отсутствия alignment в буфере windivert - он не выравнивает, сует как есть пакет за пакетом. На x86 должно быть побоку, а на других процах, где без alignment беда, винды и нет

Сделан свободный и множественный импорт шаблонов в другие шаблоны и профили.

Когда имеется много сложных и повторяющихся стратегий или групп одинаковых параметров, может быть удобно использовать шаблоны. Шаблон - это такой же профиль, только он не идет в работу, а попадает в отдельный список шаблонов. Шаблоном профиль становится через задание параметра --template=. Далее он может быть импортирован (–import=) в другой профиль или другой шаблон. Простые параметры - число, строка или bool - импортируются только, если они были заданы в импортируемом шаблоне. При иморте шаблона в шаблон в шаблоне назначения они так же считаются заданными. Списочные параметры добавляются в конец списка. Такими параметрами является все, что может принимать список значений. Например, хостлисты или --filter-tcp. Номер и имя шаблона не копируются. Директив --import может быть сколько угодно в любом месте. Предыдущие простые заданные параметры затираются новыми импортированными или заданными в текущем профиле.

Особый запрет - замещение autohostlist. Шаблон может иметь autohostlist, но при импорте куда-то, где уже есть autohostlist, будет ошибка.

Пока делал детектор торента, обнаружил, что по utp каналу из 50 пиров обычно 2-3 сыпят quic google.com
Иного обьяснения как сборка запрета найти сложно.
Казалось бы запрет - какая-то дичь непонятная, но юзает народ
Причем непонятно то ли это намеренно по торренту бьют , то ли какой-то лось засборил --wf-udp-out=1-65535 any protocol . Хоть с cutoff слава оллаху

Это, кстати, возможная причина почему может неверно назначиться l7proto на входящие. Если incoming сыпет запретом. Запрет дурит запрет. До чего мир докатился. Лучше бы РКН задурил РКН

C хелперы для работы с unixtime : localtime,gmtime,timelocal,timegm

Распознавание нешифрованного bittorrent протокола.
tcp - l7proto = “bt”, l7payload = “bt_handshake”
udp - l7proto = “utp_bt”, l7payload = “utp_bt_handshake”
именно uTp, а не uDp

Как ловить в винде ? --wf-tcp-out,–wf-udp-out ? Да, но нужно еще --server
отдельный инстанс
1024 - ваш порт torrent клиента. зафиксировать
учесть, что direction может стать любым. конекты могут быть как входящие , так и исходящие.
поэтому в antidpi функциях смотрим “standard direction”

В linux - кастом скрипты или свои tables. Прописывание портов в конфиг не поможет.

Для экшина над шифрованным bt не забывать про фильтр antidpi функций “standard payload”

Новый оркестратор per_instance_condition.
Бывает, что в одном профиле нужно вызывать разные инстансы с разными параметрами в
зависимости от текущей обстановки, которая не может быть выражена условиями --payload или range.

Оркестратор все последующие инстансы вызывает только, если у них есть аргумент “cond”. “cond” - имя iff функции. “cond_neg” - признак инверсии. Инстанс выполняется только если cond вернул true.
Имена аргументов выбраны не iff/neg, чтобы не было конфликтов с уже существующими вложенными оркестраторами.

Например, сервер не поддерживает timestamps. tcp_ts обламывается, соединение ожидаемо виснет, потому что фейк принимается сервером. Что делать ?
Можно было бы поломать код fake. Наплодить там тучу условий, аргументов. И реплицировать их на fakedsplit, hostfakesplit и тд. Но это путь уродования кода и превращения его в монстро-комбайн. Мы так не делаем

вот решение :

--lua-desync=per_instance_condition
--lua-desync=fake:blob=fake_default_tls:tcp_ts=-1000:cond=cond_tcp_has_ts
--lua-desync=fake:blob=fake_default_tls:ip_ttl=7:ip6_ttl=4:cond=cond_tcp_has_ts:cond_neg

И на закуску.

Аргумент “instances” в condition и per_instance_condition.
Выполнять условно только последующее кол-во инстансов, остальные безусловно.

cond_lua - iff функция, выполняющая произвольный код. Код должен вернуть значение условия.
Для всякой мелочи, чтобы не писать целые функции

Следующие 2 инстанса только для пакетов tcp с длиной данных не менее 1200 байт
--lua-desync=condition:instances=2:iff=cond_lua:code='return desync.dis.tcp and #desync.dis.payload>=1200'

В принципе, condition может заменить per_instance_condition, если его писать перед каждым инстансом и дать аргумент instances=1.
На самом деле это будут вложенные оркестраторы condition. Но запись с per_instance_condition будет компактнее

Только сейчас выяснилось, что в multiport в iptables нельзя больше 15 портов.
Причем диапазон считается за 2 порта.
Годы прошли, никому не надо было туда писать монстросписок. Но понадобилось.
В z2 multiport в стандартных портах из конфига переведен на ipset bitmap:port
z1 EOL, его не буду трогать. Кому надо - напишет кастом с дополнительным правилом

Набор скриптов “builder-linux” в “docs/compile” для простой сборки статиков под любую архитектуру на x86_64 хосте с традиционным linux.

Все элементарно

  1. Установить нужные тулзы на хост. gcc, gcc-multilib, make, curl, unzip. может что-то еще
  2. “get_toolchains.sh” . выбрать какая нужна архитектура. можно выбрать ALL или запустить скрипт несколько раз, скачав только нужные тулчейны
  3. “build_deps.sh” . выбрать “ALL”. соберет зависимости под все скачанные тулчейны
  4. “build_zapret2.sh”. выбрать “ALL”. соберет nfqws2,mdig,ip2net под все скачанные тулчейны. результат в binaries. Имена фолдеров совместимы с релизами на github и c install_bin.sh
  5. Чтобы избежать диалога, можно установить переменную TGT. Она должна включать список тулчейнов через пробел. Элементы списка - это то, что предлагается в диалоге или имена фолдеров в “toolchain”. Например, “mips-unknown-linux-muslsf”.
  6. upx можно самому накатить по желанию. автоматом не делается

НЕСОВМЕСТИМОЕ ИЗМЕНЕНИЕ

В cond_lua параметр “code” переименован в “cond_code”.
Это сделано для исправления конфликта с luaexec, когда cond_lua применяется в per_instance_condition.
При вызовах cond функции теперь отдается desync, преднастроенный для текущего инстанса.
Раньше отдавался desync от предыдущего инстанса.

Результат профилирования nfqws2, слинкованного с libluajit, на x86_64 системе на сплошном потоке выгрузки iperf
без ограничителей connbytes
2 инстанса - multisplit и fake без ограничителя standard payload. без внутрипрофильных фильтров на оба направления

valgrind --tool=callgrind --callgrind-out-file=callgrind.out 
/opt/zapret2/nfq2/nfqws2 --user=root --qnum 200
--lua-init=@/opt/zapret2/lua/zapret-lib.lua
--lua-init=@/opt/zapret2/lua/zapret-antidpi.lua
--in-range=a
--lua-desync=multisplit:payload=all
--lua-desync=fake:ip_ttl=1:badsum:blob=0x10000000:payload=all
--debug=0
gprof2dot.py -f callgrind callgrind.out | dot -Tpng -o 1.png

Что здесь примечательно.

  1. Вся логика C кода без процедуры desync занимает всего 5% cpu. desync - это анализ какие есть lua функции, какие у них внутрипрофильные фильтры, проверить фильтры, проверить условие cutoff, если проходит - запушить содержимое desync и вызывать через pcall саму функцию
  2. Само выполнение кода lua в варианте JIT занимает всего 15% cpu.
  3. Остальное - это накладные расходы, чтобы каждый раз пушить всю таблицу desync. lua API работает очень небыстро, львиная доля сьедается на подготовке структуры до передачи в функцию. Затем и был придуман FFI в luajit, но я его не использую, потому что не будет совместимости со стандартным lua и отлетят некоторые платформы. А 2 кода писать нет желания. Да и не очень хотел бы давать возможности FFI непонятно кем писанном скриптам. Уж слишком там low level все. Загоняю в песочницы, а тут возможность вызвать любую C функцию.
  4. При lua cutoff 95% сразу отлетает - не выполняется. Понимаете теперь зачем нужно ставить внутрипрофильные фильтры по range, если нет connbytes ? При превышении up limit по range всех инстансов наступает тишина - lua больше нет !

Вот что творится без lua совсем, когда все ушло в cutoff или функций просто нет.
Большие проценты начинают занимать C функции, которые в lua варианте отьедали доли процента

Почему lua работает так медленно ? Любую строку он уникализирует.
Видимо считает какой-то хэш и сравнивает с базой строк, и если уже есть, то пишет просто указатель.
Потому pushstring работает очень нешустро. string - это не только “abc123”, а любые бинарные блобы, которые могут быть несколько кб
Да и tables не особо скачут.
Там чуть что сразу malloc, копирование памяти.
А пушить приходится много - структура desync довольно обширна

Но NFQUEUE еще больше жрет. Сам процесс перехвата, переключение контекстов, копирование в user mode, возврат вердикта.
Совсем без lua функций - жрет 45% ядра, упираясь в возможности роутера принимать 600-700 мбит.
С 2 инстансами - жрет 55% ядра, выдавая ту же скорость.
Получается, что вся обработка в lua сьедает около 20% от NFQUEUE.
Видите как важно не гнать лишнее на nfqws ?

tun, кстати, по той же причине тормоз. разница openvpn через tun и wireguard - 5-10 раз

Итак :

  1. Главный монстр - NFQUEUE
  2. Монстр поменьше - LUA
  3. C код - на закуску

Подниму тему про ASLR.

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

Как дела с этим в zapret2

  1. Винда. До сих пор ASLR не применялся вообще никак к самому winws2 и windivert. Был изменен makefile, чтобы компилировалось с dynamic base, high entropy. Но это полностью не решило проблему из-за windivert. Он собран тоже без dynamic base и с нехарактерным для DLL адресом загрузки в пределах 4 gb. Это значит, что конфликта с другими DLL не будет почти 100% и релокации тоже не будет, а будет он висеть жестко прибитый к стенке в одном месте. В github actions добавлена коррекция windivert.dll , чтобы сделать ему rebase и добавить флаги dynamic base и high entropy. Если base address в пределах 4G, то ASLR его и гоняет в пределах 4G, а так все красиво стало - они все висят в терабайтах линейных адресов. МОГУТ ВЗВЫТЬ АНТИВИРУСЫ, встретив что-то новенькое из серии windivert. Но подписи на DLL нет и не было, поэтому по идее не должны.

  2. Linux. В статике для ASLR нужен PIE - position independent executable. Это касается всех либ, и libc в том числе. Теоретически можно пересобрать тулчейн, чтобы был PIE на musl. Но upx идею помножит на ноль. Если хотите +security - собирайте под динамику. И luajit в том числе. Если хоть что-то будет без PIE, оно не будет подвержено ASLR. Если влинковать статическую либу без PIE в бинарник , он тоже будет без PIE. В классическом linux достаточно установить dev пакеты и собрать через make - ASLR будет.

  3. ANDROID версии под NDK. Там вообще все жестко, без pie оно даже не запустится, так что он там есть с гарантией

  4. В BSD готовые бинарники без PIE и без ASLR, но если сделать make - PIE будет. Опции в makefile добавлены

nfqws и nfqws2 изначально строились на одной единственной событийной модели
процесс ждет пакет с перехвата и запускает обработку полученных данных. цикл повторяется.
единственное событие, способное привести процесс к активным действиям, это получение перехваченного пакета из сети. Сюда еще можно добавить некоторые unix сигналы, которые инициируют определенные действия.

Если посмотреть на tcp/ip стек ОС, то для реализации, скажем, tcp этой модели недостаточно.
Необходимо обрабатывать таймауты. Что-то послали, но не получили ожидаемого ответа в отведенный интервал времени. Следовательно, в событийной модели нужен таймер.

Именно это и было добавлено в nfqws2, чтобы он перестал быть только лишь пассивным слушателем и отвечателем. Все достаточно просто

timer_set(name, func, period, oneshot, data) - запустить таймер
timer_del(name) - удалить таймер

func(name,data) - таймер функция. получает name и data, заданные в timer_set

period задается в миллисекундах.
разрешение таймеров задается в параметре --timer-res. По умолчанию 50 мсек, но можно уменьшить до 10.
Если oneshot=true, вызов происходит 1 раз и таймер удаляется автоматически. Иначе дергается каждые period мсек.
data - произвольный обьект, который передается таймеру. может быть таблицей, в которой значения могут меняться самим таймером
name - уникальный идентификатор - имя таймера. при дублировании вызывается error

В режиме --intercept=0 после выполнения фазы lua-init обработка входит в цикл ожидания таймеров, пока они есть. Когда их не становится - процесс выходит

Никаких прямых sleep не предусмотрено - это сломает асинхронную обработку и повесит очередь. Только через события в стиле конечного автомата. Состояние автомата может включать data и глобальные переменные.

timer_set и timer_del можно вызывать из самих таймеров в том числе и в отношении самих себя

зачем это все делается
чтобы можно было реализовать практически любые сценарии обмена raw пакетами.
любая идея. спуф IP, портов, протоколов, разделение upstream и downstream, самостоятельная инциация обмена данными, отложенные во времени реакции. чтобы оно не зависло, если кто-то не отвечает, не плодило повисшие наборы данных в памяти
уже есть функции для получения ifconfig текущей системы и определения source ip для отправки в сторону dest ip. то есть вы можете сразу в lua init начать что-то делать, слать пакеты, а в intercept и timer ждать реакции от сети
если не получилось - можно по таймауту повторить. куда-то долбиться, подключаться

Тест таймеров из zapret-tests.lua

function timer1(name, data)
	print("timer "..name.." fired. data="..tostring(data))
end
function timer2(name, data)
	data.n = data.n+1
	print("timer "..name.." fired. data.n="..tostring(data.n))
	if data.n>=3 then
		timer_del(name)
	end
end
function test_timer(opts)
	timer_set("t1","timer1",500,true,"sample_data");
	local tbl = {n=0}
	timer_set("t2","timer2",700,false,tbl);
end
timer: 't1' function 'timer1' period 500 oneshot 1
timer t1 fired. data=sample_data
timer: 't1' deleted because of oneshot

timer: 't2' function 'timer2' period 700 oneshot 0
timer t2 fired. data.n=1

timer: 't2' function 'timer2' period 700 oneshot 0
timer t2 fired. data.n=2

timer: 't2' function 'timer2' period 700 oneshot 0
timer t2 fired. data.n=3
timer: 't2' deleted
no intercept quit

Добавлен параметр “delay” в antidpi функцию “send”.
Все очень просто - отправить с указанной задержкой в миллисекундах.
Создается oneshot timer, в качестве data подсовывается текущий dissect и send options.
Когда таймер срабатывает, пакет выплевывается в сеть.

Чтобы не задерживать все, есть внутрипрофильные фильтры по range и payload.
Если этого мало - можно использовать оркестраторы с iff функциями - прежде всего condition.
Простые условия можно писать через iff функцию cond_lua, чтобы не городить свой код в файлах.

Пока не очень понятно зачем нужно что-то задерживать - на незначительные задержки ТСПУ плевать. Там таймауты в минутах исчисляются. Но как иллюстрация использования таймеров годится. Может какой-то DPI не в России работает по коротким таймаутам для экономии ресурсов.

Чтобы не городить велосипед с именами таймеров, созданы хелперы в zapret-lib

function dis_timer_name(dis)

Конструирует имя таймера, включающее в себя ip адреса src,dst, имя протокола l4, номера портов или icmp коды.
Имя таймера может не быть уникальным.

function desync_timer_name(desync)

Конструирует имя таймера, включающее в себя результат dis_timer_name + номер пакета в conntrack по прямому направлению.
Если track отсутствует, добавляются случайные символы.
Имя таймера можно считать уникальным на desync, его можно использовать как oneshot timer name.

z2 достаточно устаканился, чтобы уходить с версии 0.