Zapret: what's new

Некоторые подробности того, во что выливается проект nfqws2.

Используется 3 уровня фильтрации трафика.

  1. ядро : iptables/nftables/ipfw/pf/windivert. Работает БЫСТРО. Здесь надо отсекать максимум. Особенно помогает connbytes на linux.
  2. C логика nfqws. работает СРЕДНЕ. conntrack, поддержка профилей (практически так же, как в nfqws1). Выделены типы протокола соединения и пейлоада пакета. Каждый тип имеет название. Например, протокол соединения - tls, payload - tls_client_hello, tls_server_hello,unknown. В выборе профиля участвует протокол соединения (–filter-l7), но не участвует тип пейлоада. Отсюда убрано абсолютно все, что касается стратегий. nfqws больше не имеет никаких средств что-либо сделать с трафиком самостоятельно. Но предоставляет инструментарий для LUA.
  3. LUA логика. работает МЕДЛЕННО. Поэтому в пределах профиля используются фильтры, которые позволяют не передавать LUA все. Можно ограничить типы пейлоадов пакетов, а так же традиционные start/cutoff раздельно по in/out. LUA функций может быть несколько на профиль. Они получают управление в порядке указания. LUA функции могут добровольно себя отсечь от получения входящего или исходящего трафика как на уровне самой функции, так и на уровне соединения (conntrack), если посчитают свою миссию выполненной. В качестве параметров передается целый набор. Перечисленный в командной строке список параметров : lua_multisplit:split_pos=1,midsld:seqovl=36 , dissect текущего пакета в виде таблиц нескольких уровней вложенности : dissect.tcp.th_seq , результат реасма/декрипта многопакетного пейлоада, информация по текущему соединению из conntrack. LUA функции могут менять поля, и они будут видимы следующей вызванной функцией. Предусмотрено хранение любых данных , привязанных к соединению conntrack : lua_state. Могут возвращать вердикт : pass, modify, drop. Могут слать пакеты через средства поддержки на стороне C кода.

luajit дает какой-то прирост производительности, но в данном случае не такой уж и критический.
в цикле долбимся на https одним потоком, прогоняя все через nfqws с двумя небольшими lua функциями
есть разница, но в пределах 30% судя по htop. глубокий профайлинг не проводил.
зато в luajit придется использовать bitop модуль вместо встроенных bitwise операций.
в luajit он встроен, но если собирать с обычным lua, то в динамическом варианте его надо доставлять отдельным пакетом, в статическом надо патчить код , чтобы его вбить статически, поскольку модуль не родной, не часть самой lua, а сделан васей, хотя и сия поделка все-же получила признание и есть в дистрибутивах как пакет.
но собирать то придется статику, значит придется патч делать, а это усложняет процесс сборки.
и так со сборкой lua не все очень просто. надо искать где его инклуды, либы. если нет pkg-config, приходится писать эвристику в makefile.
bitop работает с 32-битными числами, возвращая 32 bit signed
встроенные операции lua 5.3 - с 64 битными числами.
но код lua то придется писать один, а не if version .. else ..
мне кажется ну его. собирается, работает, но вряд ли стоит того

а вот за версией lua обычной гнаться стоит. скорость интерпретатора они хорошо оптимизируют от версии к версии. пока что minimum lua 5.3, крайне желательно 5.4

по размеру непакованного exe шника : android c lua - 300 кб, статика musl с lua - 400-500 кб , 600 кб с luajit
потребление памяти в процессе - до 5 мб. скачет в некоторых пределах, потому что язык со сборкой мусора

морда openwrt изначально писалась на lua. значит на роутерах оно живет . серверная часть веб морды намного тяжелее, чем скриптики для nfqws

Первая успешная дурилка nfqws2

function http_domcase(ctx, desync)
	if desync.outgoing and desync.l7payload=="http_req" then
		local host_range = resolve_multi_pos(desync.dis.payload,desync.l7payload,"host,endhost")
		if #host_range == 2 then
			local host = string.sub(desync.dis.payload,host_range[1],host_range[2]-1)
			local newhost="", i
			for i = 1, #host do
				newhost=newhost..((i%2)==0 and string.lower(string.sub(host,i,i)) or string.upper(string.sub(host,i,i)))
			end
			DLOG("domcase: "..host.." => "..newhost)
			memcpy(desync.dis.payload,host_range[1],newhost)
			return VERDICT_MODIFY
		else
			DLOG("domcase: cannot find host range")
		end
	end
	return VERDICT_PASS
end

./nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-legacy.lua --payload-type=http_req --in-range=x --out-range=-d1 --lua-desync=http_domcase

packet: id=4 len=149 mark=00000000 ifin=(0) ifout=eth0(2)
IP6: [my_ip] => 2606:4700:3034::ac43:b6c4 proto=tcp ttl=64 sport=[src_port] dport=80 flags=AP seq=<seq> ack_seq=<ack>
TCP: len=77 : 47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A 48 6F 73 74 3A 20 72 75 74 72 61 63 6B 65 72 2E ... : GET / HTTP/1.1..Host: rutracker. ...
using cached desync profile 1
outgoing packet contains HTTP request
req retrans : tcp seq interval <start_seq>:<end_seq>
hostname: rutracker.org
discovered l7 protocol
discovered hostname
desync profile search for tcp target=[2606:4700:3034::ac43:b6c4]:80 l7proto=http ssid='' hostname='rutracker.org'
desync profile 1 matches
dpi desync src=[[my_ip]]:[src_port] dst=[2606:4700:3034::ac43:b6c4]:80 direction=out connection_proto=http payload_type=http_req
* lua 'http_domcase' : out pos d1 is not beyond d1
* lua 'http_domcase' : out pos n3 d1 in range n0-d1
* lua 'http_domcase' : payload_type 'http_req' satisfy filter
* lua 'http_domcase' : desync
LUA: domcase: rutracker.org => RuTrAcKeR.OrG
reconstructed packet due to VERDICT_MODIFY. size 149 => 149
packet: id=4 pass modified. len=149
        GET / HTTP/1.1
        Host: RuTrAcKeR.OrG
        User-Agent: curl/8.14.1
        Accept: */*

Фейк.
Чтобы избавить lua от загрузки файлов в память (это связано с путями и правами доступа), nfqws2 умеет загружать блобы из файлов и вставлять их как глобальные переменные lua.
Загрузка блобов идет под начальными правами, lua выполняется только со сборошенными.
Дефолтные фейки для tls, http и quic выставляются фиксированно из C кода.
Есть и функция one-time мода tls. Код уже написан на C, зачем переписывать ?
Битовые операции все же решил выставлять как C функции, чтобы не зависеть от версии lua.
Можно и JIT использовать с lua 5.1
Из C так же выставляется всякая мелочь типа быстрой генерации рандомных блобов, функции парсинга tls, ресолва маркеров в стиле nfqws1, DLOG - логгинг туда, куда указано в --debug

Будет функция на LUA для автоматической tcp сегментации. Можно будет слать пейлоады любого размера. Он их разобьет по mss

./nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-legacy.lua --payload-type=tls_client_hello --in-range=x --blob-tls-mod=fake_default_tls:rnd,rndsni --lua-desync=fake:fake=fake_default_tls:tcp_flags_unset=ACK:tcp_md5:tcp_seq=-10:badsum:repeats=11

function fake_(ctx, desync, fake_payload)
	if desync.outgoing then
		local fake = deepcopy(desync.dis)
		fake.payload = fake_payload
		if fake.tcp then
			if tonumber(desync.arg.tcp_seq) then
				fake.tcp.th_seq = fake.tcp.th_seq + desync.arg.tcp_seq
			end
			if tonumber(desync.arg.tcp_ack) then
				fake.tcp.th_ack = fake.tcp.th_ack + desync.arg.tcp_ack
			end
			if desync.arg.tcp_flags_unset then
				fake.tcp.th_flags = bitand(fake.tcp.th_flags, bitnot(parse_tcp_flags(desync.arg.tcp_flags_unset)))
			end
			if desync.arg.tcp_flags_set then
				fake.tcp.th_flags = bitor(fake.tcp.th_flags, parse_tcp_flags(desync.arg.tcp_flags_set))
			end
			if tonumber(desync.arg.tcp_ts) then
				local idx = find_tcp_option(fake.tcp.options,TCP_KIND_TS)
				if idx and (fake.tcp.options[idx].data and #fake.tcp.options[idx].data or 0)==8 then
					fake.tcp.options[idx].data = u32b(u32(fake.tcp.options[idx].data,1)+desync.arg.tcp_ts)..string.sub(fake.tcp.options[idx].data,5)
				else
					DLOG("fake: timestamp tcp option not present or invalid")
				end
			end
			if desync.arg.tcp_md5 then
				if find_tcp_option(fake.tcp.options,TCP_KIND_MD5) then
					DLOG("fake: md5 option already present")
				else
					fake.tcp.options.insert({kind=TCP_KIND_MD5, data=brandom(16)})
				end
			end
		end
		local ttl = tonumber(desync.arg.ttl) or 0
		local ttl6 = tonumber(desync.arg.ttl6) or 0
		if ttl6==0 then ttl6=ttl end
		if ttl~=0 and fake.ip then
			fake.ip.ip_ttl = ttl
		end
		if ttl6~=0 and fake.ip6 then
			fake.ip6.ip6_hlim = ttl6
		end
		DLOG("fake: "..hexdump_dlog(fake.payload))
		rawsend_dissect(fake,{repeats=desync.arg.repeats,ifout=desync.arg.ifout or desync.ifout,fwmark=desync.arg.fwmark or 0,l4badsum=type(desync.arg.badsum)~="nil"})
	end
	return VERDICT_PASS
end

function fake(ctx, desync)
	if desync.outgoing then
		if not desync.arg.fake then
			error("fake: 'fake' parameter must hold global var name with fake data")
		end
		local fake_payload = _G[desync.arg.fake]
		if not fake_payload then
			error("fake: global var '"..desync.arg.fake.."' unavailable")
		end
		return fake_(ctx,desync,fake_payload)
	end
	return VERDICT_PASS
end

Подобным образом будет переписан практически весь desync функционал nfqws1.
Некоторые варианты будут составляться из комбинаций функций, названия некоторых параметров поменяются.
В autohostlist внесено давно напрашивающееся изменение. Профиль теперь не выигрывает, если нет имени хоста, и выигрывает, если есть любое имя хоста.
Раньше он выигрывал даже если не было имени хоста. Что вынуждало дублировать параметры нулевой фазы на разные профили

Фильтры на стороне C --in-range, --out-range, --payload-type задаются для всех последующих lua функций внутри профиля. Поэтому место этих параметров - важно.
--payload-type=empty --lua-desync=xxx1 --payload-type=http_req,tls_client_hello --lua-desync=xxx2

Так примерно будут выглядеть мультистратегии

--blob my_fake:@my_fake.bin
--filter-tcp=80,443 --hostlist=xxxx1.txt --in-range=x --payload-type=http_req,tls_client_hello --lua-desync=fake:fake=my_fake:badsum --lua-desync=multisplit:pos=1,midsld
--new
--filter-udp=443 --hostlist=xxxx1.txt --in-range=x --payload-type=quic_initial --lua-desync=fake:ttl=5

Мультисплит.
Допустим, есть задача дропнуть пустой ACK в ответ на SYN,ACK , на client hello сделать --dpi-desync=fake,multisplit --dpi-desync-split-pos=host,5 --dpi-resync-repeats=3 , а следующие за ним несколько исходящих tls отсылов порезать на позиции 1.
В nfqws1 что-то похожее можно изобразить, если поставить ttl=1 на пустой ACK --orig-ttl=1 --orig-mod-start=s1 --orig-mod-cutoff=d1, выбрать сплит позиции 1,5,host, включить `–dpi-desync-any-protocol --dpi-desync-cutoff=d5’ . Но так client hello будет дополнительно порезан на позиции 1, хотя этого не требовалось.
Будем считать, что client hello может быть с kyber до 3 пакетов.
Как это выглядит в zapret 2

./nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-legacy.lua --out-range=s1-n2 -payload-type empty --lua-desync=drop --out-range=d1-d3 --payload-type tls_client_hello --lua-desync=fake:fake=fake_default_tls:tcp_ts=-10000:repeats=3 --lua-desync=multisplit:pos=host,5 --out-range=d3-d4 --payload-type=unknown --lua-desync=multisplit:pos=1

Фаза дропания пустого ACK.
drop проходит, остальное отсекается по range и типу пейлоада.
Названия типа drop_1_1 означает function instance. Включает имя функции, номер профиля и порядковый номер функции в профиле.
Может быть несколько вызовов одной функции в одном профиле. Имя у них одинаковое, а инстансы разные. Параметры у каждого инстанса могут быть тоже разные.

packet: id=3 len=52 mark=00000001 ifin=(0) ifout=eth0(2)
IP4: 192.168.4.83 => 81.19.72.32 proto=tcp ttl=64 sport=22222 dport=443 flags=A
using cached desync profile 1
updated ttl cache
dpi desync src=192.168.4.83:22222 dst=81.19.72.32:443 direction=out connection_proto=unknown payload_type=empty
* lua 'drop_1_1' : out pos n2 is not beyond n2
* lua 'fake_1_2' : out pos d0 is not beyond d3
* lua 'multisplit_1_3' : out pos d0 is not beyond d3
* lua 'multisplit_1_4' : out pos d0 is not beyond d4
* lua 'drop_1_1' : out pos s1 n2 in range s1-n2
* lua 'drop_1_1' : payload_type 'empty' satisfy filter
* lua 'drop_1_1' : desync
* lua 'fake_1_2' : out pos d0 d0 out of range d1-d3
* lua 'multisplit_1_3' : out pos d0 d0 out of range d1-d3
* lua 'multisplit_1_4' : out pos d0 d0 out of range d3-d4
packet: id=3 drop

Фаза fake,multisplit на client hello.
Функция drop отсекается через range и тип пейлоада.
Функции fake и multisplit проходят с параметрами для client hello.
С фейком все ясно.
С мультисплитом есть кое-что интересное.
Если в nfqws1 работа шла по каждому пакету реасма, то в nfqws2 идет работа сразу с реасмом целиком,
а последующие пакеты реасма в случае успеха сегментированной отсылки дропаются.
Факт успеха приходится запоминать в conntrack.lua_state. Это специальная таблица, ассоциированная с conntrack записью.
В ней можно хранить любые данные lua, и они будут возвращаться с каждым пакетом.
Так можно делать stateful обработку.
При отсылке сегмента функция multisplit не заботится о длинах кусков. Влезут ли они в MTU.
Используется хелпер rawsend_dissect_segmented, который сам дополнительно сегментирует куски согласно запомненному для соединения MSS.
Это видно как 2 строчки “rawsend” после “sending part”



packet: id=4 len=1500 mark=00000001 ifin=(0) ifout=eth0(2)
IP4: 192.168.4.83 => 81.19.72.33 proto=tcp ttl=64 sport=22222 dport=443 flags=A
TCP: len=1448 : 16 03 01 06 10 ....
using cached desync profile 1
outgoing packet contains partial TLS ClientHello
starting reassemble. now we have 1448/1557
DELAY desync until reasm is complete (#1)
packet: id=4 drop

packet: id=5 len=161 mark=00000001 ifin=(0) ifout=eth0(2)
IP4: 192.168.4.83 => 81.19.72.33 proto=tcp ttl=64 sport=22222 dport=443 flags=AP
TCP: len=109 : A2 99 CC C3 ....
using cached desync profile 1
reassemble : feeding data payload size=109. now we have 1557/1557
outgoing packet contains full TLS ClientHello
TLS record layer version : TLS 1.0
TLS handshake version : TLS 1.2
TLS supported versions ext : TLS 1.3
TLS supported versions ext : TLS 1.2
TLS ALPN ext : h2
TLS ALPN ext : http/1.1
TLS ECH ext : not present
DELAY desync until reasm is complete (#2)
REPLAYING delayed packet #1 offset 0
REPLAY IP4: 192.168.4.83 => 81.19.72.33 proto=tcp ttl=64 sport=22222 dport=443 flags=A
TCP: len=1448 : 16 03 01 06 10 ....
using cached desync profile 1
outgoing packet contains full TLS ClientHello
TLS record layer version : TLS 1.0
TLS handshake version : TLS 1.2
TLS supported versions ext : TLS 1.3
TLS supported versions ext : TLS 1.2
TLS ALPN ext : h2
TLS ALPN ext : http/1.1
TLS ECH ext : not present
hostname: lenta.ru
discovered l7 protocol
discovered hostname
desync profile search for tcp target=81.19.72.33:443 l7proto=tls ssid='' hostname='lenta.ru'
desync profile 1 matches
dpi desync src=192.168.4.83:22222 dst=81.19.72.33:443 direction=out connection_proto=tls payload_type=tls_client_hello
* lua 'drop_1_1' : out pos n4 is beyond n2
* lua 'fake_1_2' : out pos d2 is not beyond d3
* lua 'multisplit_1_3' : out pos d2 is not beyond d3
* lua 'multisplit_1_4' : out pos d2 is not beyond d4
* lua 'drop_1_1' : out pos s1449 n4 out of range s1-n2
* lua 'fake_1_2' : out pos d2 d2 in range d1-d3
* lua 'fake_1_2' : payload_type 'tls_client_hello' satisfy filter
* lua 'fake_1_2' : desync
LUA: fake: 16 03 01 02 A3 01 00 02 9F 03 03 41 88 82 2D 4F FD 81 48 9E E7 90 65 1F BA 05 7B FF A7 5A F9 5B ... ...........A..-O..H...e...{..Z.[ ... 
rawsend_dissect repeats=3 size=732 ifout=eth0 fwmark=40000001
* lua 'multisplit_1_3' : out pos d2 d2 in range d1-d3
* lua 'multisplit_1_3' : payload_type 'tls_client_hello' satisfy filter
* lua 'multisplit_1_3' : desync
LUA: multisplit: split pos: host,5
LUA: multisplit: resolved split pos: 5 1531
LUA: multisplit: sending part 0 0-4: 16 03 01 06 10  .....
rawsend_dissect repeats=1 size=57 ifout=eth0 fwmark=40000001
LUA: multisplit: sending part 1 5-1530: 01 00 06 ....
rawsend_dissect repeats=1 size=1500 ifout=eth0 fwmark=40000001
rawsend_dissect repeats=1 size=130 ifout=eth0 fwmark=40000001
LUA: multisplit: sending part 2 1531-1556: 6C 65 6E 74 61 2E 72 75 00 10 00 0E 00 0C 02 68 32 08 68 74 74 70 2F 31 2E 31  lenta.ru.......h2.http/1.1
rawsend_dissect repeats=1 size=78 ifout=eth0 fwmark=40000001
* lua 'multisplit_1_4' : out pos d2 d2 out of range d3-d4
DROPPING delayed packet #1
REPLAYING delayed packet #2 offset 1448
REPLAY IP4: 192.168.4.83 => 81.19.72.33 proto=tcp ttl=64 sport=22222 dport=443 flags=AP
TCP: len=109 : A2 99 CC C3 ....
using cached desync profile 1
outgoing packet contains full TLS ClientHello
TLS record layer version : TLS 1.0
TLS handshake version : TLS 1.2
TLS supported versions ext : TLS 1.3
TLS supported versions ext : TLS 1.2
TLS ALPN ext : h2
TLS ALPN ext : http/1.1
TLS ECH ext : not present
hostname: lenta.ru
dpi desync src=192.168.4.83:22222 dst=81.19.72.33:443 direction=out connection_proto=tls payload_type=tls_client_hello
* lua 'drop_1_1' : out pos n4 is beyond n2
* lua 'fake_1_2' : out pos d2 is not beyond d3
* lua 'multisplit_1_3' : out pos d2 is not beyond d3
* lua 'multisplit_1_4' : out pos d2 is not beyond d4
* lua 'drop_1_1' : out pos s1449 n4 out of range s1-n2
* lua 'fake_1_2' : out pos d2 d2 in range d1-d3
* lua 'fake_1_2' : payload_type 'tls_client_hello' satisfy filter
* lua 'fake_1_2' : desync
* lua 'multisplit_1_3' : out pos d2 d2 in range d1-d3
* lua 'multisplit_1_3' : payload_type 'tls_client_hello' satisfy filter
* lua 'multisplit_1_3' : desync
LUA: multisplit: dropping replay packet because reasm was already sent
* lua 'multisplit_1_4' : out pos d2 d2 out of range d3-d4
DROPPING delayed packet #2
reassemble session finished
packet: id=5 drop

И последняя фаза нарезки unknown пейлоадов дальше.
Первый инстанс multisplit не проходит, второй со сплит позицией 1 проходит.

packet: id=16 len=145 mark=00000001 ifin=(0) ifout=eth0(2)
IP4: 192.168.4.83 => 81.19.72.33 proto=tcp ttl=64 sport=22222 dport=443 flags=AP
TCP: len=93 : 16 03 03 00 25 ....
using cached desync profile 1
dpi desync src=192.168.4.83:22222 dst=81.19.72.33:443 direction=out connection_proto=tls payload_type=unknown
* lua 'drop_1_1' : out pos n8 is beyond n2
* lua 'fake_1_2' : out pos d3 is not beyond d3
* lua 'multisplit_1_3' : out pos d3 is not beyond d3
* lua 'multisplit_1_4' : out pos d3 is not beyond d4
* lua 'drop_1_1' : out pos s1558 n8 out of range s1-n2
* lua 'fake_1_2' : out pos d3 d3 in range d1-d3
* lua 'fake_1_2' : payload_type 'unknown' does not satisfy filter
* lua 'multisplit_1_3' : out pos d3 d3 in range d1-d3
* lua 'multisplit_1_3' : payload_type 'unknown' does not satisfy filter
* lua 'multisplit_1_4' : out pos d3 d3 in range d3-d4
* lua 'multisplit_1_4' : payload_type 'unknown' satisfy filter
* lua 'multisplit_1_4' : desync
LUA: multisplit: split pos: 1
LUA: multisplit: resolved split pos: 1
LUA: multisplit: sending part 0 0-0: 16  .
rawsend_dissect repeats=1 size=53 ifout=eth0 fwmark=40000001
LUA: multisplit: sending part 1 1-92: 03 03 00 25 ...
rawsend_dissect repeats=1 size=144 ifout=eth0 fwmark=40000001
packet: id=16 drop

А дальше все функции достигают cutoff condition. Они больше никогда не будут вызваны в этом соединении.
C код помечает в conntrack , что lua в направлении out больше вызывать не надо.
Это экономит проц.

packet: id=21 len=90 mark=00000001 ifin=(0) ifout=eth0(2)
IP4: 192.168.4.83 => 81.19.72.33 proto=tcp ttl=64 sport=22222 dport=443 flags=AP
TCP: len=38 : 17 03 03 00 21 ....
using cached desync profile 1
dpi desync src=192.168.4.83:22222 dst=81.19.72.33:443 direction=out connection_proto=tls payload_type=unknown
* lua 'drop_1_1' : out pos n10 is beyond n2
* lua 'fake_1_2' : out pos d5 is beyond d3
* lua 'multisplit_1_3' : out pos d5 is beyond d3
* lua 'multisplit_1_4' : out pos d5 is beyond d4
all out desync functions reached cutoff condition
packet: id=21 pass unmodified

Фильтры ‘–payload-type’, ‘–out-range’, ‘–in-range’ выполняются на стороне C кода. Их задача - ограничить вызовы LUA с целью экономии CPU.
Если вдруг их не хватит, всегда можно дофильтровать в вашей собственной LUA функции.

lua код multisplit

function multisplit(ctx, desync)
	if desync.dis.tcp and direction_check(desync) and #desync.reasm_data>0 then
		local replay_drop_key = desync.func_instance .. "_replay_drop"
		local replay_drop = false
		if desync.track then
			if desync.replay then
				replay_drop = desync.track.lua_state[replay_drop_key] or false
			else
				desync.track.lua_state[replay_drop_key] = nil
			end
		end
		if desync.reasm_offset==0 then
			local data = desync.reasm_data
			local spos = desync.arg.pos or "2"
			if debug then DLOG("multisplit: split pos: "..spos) end
			local pos = resolve_multi_pos(data, desync.l7payload, spos)
			if #pos>0 then
				-- check debug to save CPU
				if debug then DLOG("multisplit: resolved split pos: "..table.concat(zero_based_pos(pos)," ")) end
				for i=0,#pos do
					local pos_start = pos[i] or 1
					local pos_end = i<#pos and pos[i+1]-1 or #data
					local part = string.sub(data,pos_start,pos_end)
					if debug then DLOG("multisplit: sending part "..i.." "..(pos_start-1).."-"..(pos_end-1)..": "..hexdump_dlog(part)) end
					if not rawsend_payload_segmented(desync,part,pos_start-1) then
						return VERDICT_PASS
					end
				end
				if desync.replay and desync.track then
					desync.track.lua_state[replay_drop_key] = true
				end
				return VERDICT_DROP
			else
				DLOG("multisplit: no pos resolved")
			end
		else
			if desync.track and (desync.reasm_offset + #desync.dis.payload) >= #desync.reasm_data then
				-- last piece of reasm
				desync.track.lua_state[replay_drop_key] = nil
			end
			if replay_drop then
				DLOG("multisplit: dropping replay packet because reasm was already sent")
				return VERDICT_DROP
			end
		end
	end
	return VERDICT_PASS
end

Немного про недостатки lua 5.1, которые исправлены в 5.3+, но с ними придется жить в luajit, который 5.1.
Про внешние модули можно сразу забыть. Надо, чтобы работало на статической сборке без всяких внешних so на любых lua - как jit, так и 5.3, 5.4. И чтобы не накатывать патчи для влинковки этих модулей в ядро LUA.
Все, чего нет в 5.1, приходится делать в виде собственных C функций. Да, код не будет работать вне nfqws, если не переписать эти функции, например, на новых возможностях lua 5.3+ . Но он и предназначен для nfqws и не особо полезен в отрыве от него.

Итак, чего нет :

  1. Битовые операции и сдвиги
  2. Отсутствие типа int. Все числа хранятся в формате double, у которого мантисса всего 53 бита вместе со знаком. Поэтому 64 бита хранить невозможно как целое число. А значит и оперировать полями данных 64 бита.
  3. Целочисленное деление //. Только варианты с округлением через floor
  4. Паковка/распаковка структур в стиле Питон. string.pack/string.unpack

Хочу сделать nfqws2 универсальным по направлению соединения. Входящее или исходящее.
Понятие “направление пакета” как “входящий” или “исходящий” относится только к ситуации, когда обмен идет с самой системой. Когда идет форвард, то есть только входящий и исходящий интерфейсы (которые, кстати, невозможно получить на BSD).
В nfqws традиционно направление определяется через conntrack по роли в его установлении.
Если записи в коннтрак еще нет, и пробегает SYN, значит это направление считается условно исходящим, поскольку nfqws предназначен для дурения со стороны клиента.
Если пробегает SYN,ACK, то направление считается входящим.
Запоминаются эндпоинты - ip1, port1, ip2, port2. В следующий раз пакет 2 раза пробивается по коннтраку. В обычном режиме и в реверсивном, то есть с обменом эндпоинтов. Если направление пробилось прямо - это исходящий пакет. Если направление пробилось с обменом - это входящий.
Для udp исходящим считается первый увиденный пакет.
Проверки на адреса локальных сетей - это костыли, которых хочется избежать. На ipv6 они вообще не работают.

Если мы дурим со стороны сервера, то роли меняются.
Потому сделан параметр --server. Если его включить, то направление, спускаемое в lua, инвертируется. Например, http request будет входящим, а http reply - исходящим.
Так же меняются местами порты в --filter-tcp, --filter-udp. Фильтр начинает работать по source , а не destination port.
На винде --wf-tcp и --wf-udp будут заменены на --wf-tcp-out, --wf-tcp-in, --wf-udp-out, --wf-udp-in, роли так же будут инвертироваться.

Функции дурения обычно пишутся с учетом направления. Никакого смысла нет отсылать фейк или сегментировать пейлоад самому себе. И чтобы клиент/серверную логику не тянуть в сами функции, им просто говорят, что реплай - это out. И они работают по реплаю, но не работают по реквесту.

nfqws2 можно будет использовать в том числе и для частичной обфускации протоколов с обоих концов. Поэтому он должен работать как на клиенте, так и на сервере.

В nfqws поддерживается детекция протокола xmpp и его основных пейлоадов для случая с использованием STARTTLS.
Практическая демонстрация как может случиться 2 profile transitions за одно соединение.
В nfqws1 такой случай был малореален, тк и протокол, и хост детектились на базе http или tls, которые несли в себе сразу инфу о протоколе и хосте.
nfqws1 не поддерживал отдельное определение пейлоада и протокола сеанса. В nfqws2 эти понятия разнесены. Протокол сеанса определяется на основании первого известного обнаруженного пейлоада и не меняется до его конца. Но тип пейлоада относится к каждому пакету или группе пакетов для случая reasm. Поэтому в xmpp вылезает несколько типов пейлоада самого xmpp и tls_client_hello, затем tls_server_hello.
Если бы это был порт 5223 или https, то протокол сеанса был бы tls, и так же проскакивали пейлоады tls_client_hello, tls_server_hello

nfqws2 запущен на XMPP сервере, поэтому направления инвертированы
./nfqws2 --qnum 200 --debug --server --filter-l7=xmpp --hostlist-domains=jabber.ru --new --filter-l7=xmpp

Первый поиск профиля

packet: id=1 len=80 mark=00000000 ifin=eth0(18) ifout=(0)
IP6: [client] => [server] proto=tcp ttl=63 sport=33333 dport=5222 flags=S
server mode desync profile/ipcache search target ip=[client] port=5222
desync profile search for tcp ip=[client] port=5222 l7proto=unknown ssid='' hostname=''
desync profile 0 matches
incoming TTL 63
dpi desync src=[[client]]:33333 dst=[[server]]:5222 track_direction=out fixed_direction=in connection_proto=unknown payload_type=empty
no lua functions in this profile
packet: id=1 pass unmodified

Первый переход и серия STREAM, STREAM, FEATURES, STARTTLS, PROCEED
Имя хоста неизвестно, профиль 1 не подходит

packet: id=4 len=244 mark=00000000 ifin=eth0(18) ifout=(0)
IP6: [client] => [server] proto=tcp ttl=63 sport=33333 dport=5222 flags=AP
TCP: len=172 : 3C 3F 78 6D 6C 20 76 65 72 73 69 6F 6E 3D 22 31 2E 30 22 20 65 6E 63 6F 64 69 6E 67 3D 22 55 54 ... : <?xml version="1.0" encoding="UT ...
server mode desync profile/ipcache search target ip=[client] port=5222
using cached desync profile 0
packet contains xmpp_stream payload
discovered l7 protocol
desync profile search for tcp ip=[client] port=5222 l7proto=xmpp ssid='' hostname=''
desync profile 2 matches
desync profile changed by revealed l7 protocol or hostname !
dpi desync src=[[client]]:33333 dst=[[server]]:5222 track_direction=out fixed_direction=in connection_proto=xmpp payload_type=xmpp_stream
no lua functions in this profile
packet: id=4 pass unmodified


packet: id=6 len=254 mark=00000000 ifin=(0) ifout=eth0(18)
IP6: [server] => [client] proto=tcp ttl=64 sport=5222 dport=33333 flags=AP
TCP: len=182 : 3C 3F 78 6D 6C 20 76 65 72 73 69 6F 6E 3D 27 31 2E 30 27 3F 3E 3C 73 74 72 65 61 6D 3A 73 74 72 ... : <?xml version='1.0'?><stream:str ...
server mode desync profile/ipcache search target ip=[client] port=5222
using cached desync profile 2
packet contains xmpp_stream payload
dpi desync src=[[server]]:5222 dst=[[client]]:33333 track_direction=in fixed_direction=out connection_proto=xmpp payload_type=xmpp_stream
no lua functions in this profile
packet: id=6 pass unmodified



packet: id=7 len=179 mark=00000000 ifin=(0) ifout=eth0(18)
IP6: [server] => [client] proto=tcp ttl=64 sport=5222 dport=33333 flags=AP
TCP: len=107 : 3C 73 74 72 65 61 6D 3A 66 65 61 74 75 72 65 73 3E 3C 73 74 61 72 74 74 6C 73 20 78 6D 6C 6E 73 ... : <stream:features><starttls xmlns ...
server mode desync profile/ipcache search target ip=[client] port=5222
using cached desync profile 2
packet contains xmpp_features payload
dpi desync src=[[server]]:5222 dst=[[client]]:33333 track_direction=in fixed_direction=out connection_proto=xmpp payload_type=xmpp_features
no lua functions in this profile
packet: id=7 pass unmodified

packet: id=9 len=123 mark=00000000 ifin=eth0(18) ifout=(0)
IP6: [client] => [server] proto=tcp ttl=63 sport=33333 dport=5222 flags=AP
TCP: len=51 : 3C 73 74 61 72 74 74 6C 73 20 78 6D 6C 6E 73 3D 22 75 72 6E 3A 69 65 74 66 3A 70 61 72 61 6D 73 ... : <starttls xmlns="urn:ietf:params ...
server mode desync profile/ipcache search target ip=[client] port=5222
using cached desync profile 2
packet contains xmpp_starttls payload
dpi desync src=[[client]]:33333 dst=[[server]]:5222 track_direction=out fixed_direction=in connection_proto=xmpp payload_type=xmpp_starttls
no lua functions in this profile
packet: id=9 pass unmodified

packet: id=10 len=122 mark=00000000 ifin=(0) ifout=eth0(18)
IP6: [server] => [client] proto=tcp ttl=64 sport=5222 dport=33333 flags=AP
TCP: len=50 : 3C 70 72 6F 63 65 65 64 20 78 6D 6C 6E 73 3D 27 75 72 6E 3A 69 65 74 66 3A 70 61 72 61 6D 73 3A ... : <proceed xmlns='urn:ietf:params: ...
server mode desync profile/ipcache search target ip=[client] port=5222
using cached desync profile 2
packet contains xmpp_proceed payload
dpi desync src=[[server]]:5222 dst=[[client]]:33333 track_direction=in fixed_direction=out connection_proto=xmpp payload_type=xmpp_proceed
no lua functions in this profile
packet: id=10 pass unmodified


Второй переход - TLS_CLIENT_HELLO и обнаружение имени хоста, переход на профиль 1

packet: id=11 len=249 mark=00000000 ifin=eth0(18) ifout=(0)
IP6: [client] => [server] proto=tcp ttl=63 sport=33333 dport=5222 flags=AP
TCP: len=177 : 16 03 03 00 AC ...........
server mode desync profile/ipcache search target ip=[client] port=5222
using cached desync profile 2
packet contains full TLS ClientHello
TLS record layer version : TLS 1.2
TLS handshake version : TLS 1.2
TLS supported versions ext : not present
TLS ALPN ext : not present
TLS ECH ext : not present
hostname: jabber.ru
discovered hostname
desync profile search for tcp ip=[client] port=5222 l7proto=xmpp ssid='' hostname='jabber.ru'
* hostlist check for profile 1
[fixed] include hostlist check for jabber.ru : positive
desync profile 1 matches
desync profile changed by revealed l7 protocol or hostname !
dpi desync src=[[client]]:33333 dst=[[server]]:5222 track_direction=out fixed_direction=in connection_proto=xmpp payload_type=tls_client_hello
no lua functions in this profile
packet: id=11 pass unmodified

packet: id=12 len=1420 mark=00000000 ifin=(0) ifout=eth0(18)
IP6: [server] => [client] proto=tcp ttl=64 sport=5222 dport=33333 flags=A
TCP: len=1348 : 16 03 03 00 41 ...
server mode desync profile/ipcache search target ip=[client] port=5222
using cached desync profile 1
packet contains tls_server_hello payload
TLS record layer version : TLS 1.2
TLS handshake version : TLS 1.2
TLS supported versions ext : not present
dpi desync src=[[client]]:5222 dst=[[server]]:33333 track_direction=in fixed_direction=out connection_proto=xmpp payload_type=tls_server_hello
no lua functions in this profile
packet: id=12 pass unmodified

В nfqws2 основной и правильный путь привнесения бинарных данных - через блобы.
Блобы загружаются из файла или hex строки в C коде и присваиваются глобальной переменной. Например, здесь blob ‘bb’ используется как seqovl pattern

./nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-legacy.lua --lua-desync=multisplit:seqovl=10:seqovl_pattern=bb --blob=bb:@blob.bin

Но никто не мешает присвоить значение в lua коде
Если, допустим, нужен рандом, генерируемый 1 раз пр старте

./nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-legacy.lua --lua-desync=multisplit:seqovl=10:seqovl_pattern=bb --lua-init "bb=brandom(10)"

А если каждый раз нужен разный рандом ? Придется написать простую функцию и использовать порядок поиска блобов. Первым делом ищется в таблице desync, которая передается последовательно от функции к функции и живет все время обработки пакета.

./nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-legacy.lua --lua-init "function genbb(ctx,desync) desync.bb=brandom(10); end" --lua-desync=genbb --lua-desync=multisplit:seqovl=10:seqovl_pattern=bb

Конечно, если функция более сложная или так более удобно, код можно вынести в файл

На таких примерах хорошо видна расширяемость алгоритмов в отличие от фиксированных возможностей nfqws1

Вот что получает lua-desync функция на вход в параметре desync.
Чтобы было понятно с чем имеем дело.
ctx - это контекст для некоторых вызовов C функций. тип переменной - lightuserdata
lua код с ней ничего сделать не может, только передать как параметр

-- prints desync to DLOG
function debug(ctx, desync)
	DLOG("desync:")
	var_debug(desync)
end
LUA: desync:
LUA: .outgoing
LUA:   boolean true
LUA: .fwmark
LUA:   number 0
LUA: .track
LUA:   .hostname
LUA:     string google.com
LUA:   .hostname_is_ip
LUA:     boolean false
LUA:   .incoming_ttl
LUA:     number 52
LUA:   .lua_state
LUA:   .tcp
LUA:     .seq0
LUA:       number ....
LUA:     .ack0
LUA:       number ....
LUA:     .ack
LUA:       number ....
LUA:     .pos_orig
LUA:       number 1571
LUA:     .winsize_orig
LUA:       number 502
LUA:     .winsize_orig_calc
LUA:       number 64256
LUA:     .scale_orig
LUA:       number 7
LUA:     .mss_orig
LUA:       number 1460
LUA:     .pos_reply
LUA:       number 1
LUA:     .winsize_reply
LUA:       number 65160
LUA:     .winsize_reply_calc
LUA:       number ....
LUA:     .seq
LUA:       number ....
LUA:     .mss_reply
LUA:       number 1460
LUA:     .scale_reply
LUA:       number 7
LUA:   .lua_in_cutoff
LUA:     boolean true
LUA:   .pcounter_orig
LUA:     number 5
LUA:   .pdcounter_orig
LUA:     number 2
LUA:   .pbcounter_orig
LUA:     number 1570
LUA:   .pcounter_reply
LUA:     number 2
LUA:   .pdcounter_reply
LUA:     number 0
LUA:   .pbcounter_reply
LUA:     number 0
LUA:   .l7proto
LUA:     string tls
LUA:   .lua_out_cutoff
LUA:     boolean false
LUA: .func_instance
LUA:   string debug_1_1
LUA: .func_n
LUA:   number 1
LUA: .instance_cutoff
LUA: .func
LUA:   string debug
LUA: .dis
LUA:   .ip
LUA:     .ip_ttl
LUA:       number 64
LUA:     .ip_v
LUA:       number 4
LUA:     .ip_hl
LUA:       number 5
LUA:     .ip_tos
LUA:       number 0
LUA:     .ip_len
LUA:       number 1500
LUA:     .ip_off
LUA:       number 16384
LUA:     .ip_p
LUA:       number 6
LUA:     .ip_src
LUA:       string C0 ...
LUA:     .ip_id
LUA:       number ...
LUA:     .ip_dst
LUA:       string 2D ....
LUA:   .payload
LUA:     string 16 03 01 06 1D ....
LUA:   .l4proto
LUA:     number 6
LUA:   .tcp
LUA:     .th_ack
LUA:       number ...
LUA:     .th_sport
LUA:       number ...
LUA:     .th_dport
LUA:       number 443
LUA:     .th_x2
LUA:       number 0
LUA:     .th_off
LUA:       number 8
LUA:     .th_sum
LUA:       number ...
LUA:     .th_urp
LUA:       number 0
LUA:     .options
LUA:       .1
LUA:         .kind
LUA:           number 1
LUA:       .2
LUA:         .kind
LUA:           number 1
LUA:       .3
LUA:         .data
LUA:           string 33 .......
LUA:         .kind
LUA:           number 8
LUA:     .th_flags
LUA:       number 16
LUA:     .th_win
LUA:       number 502
LUA:     .th_seq
LUA:       number ...
LUA:   .transport_len
LUA:     number 1480
LUA: .arg
LUA:   .testarg2
LUA:     string 22222
LUA:   .testarg
LUA:     string 111
LUA: .l7payload
LUA:   string tls_client_hello
LUA: .ifout
LUA:   string eth0
LUA: .replay
LUA:   boolean true
LUA: .tcp_mss
LUA:   number 1460
LUA: .profile_n
LUA:   number 1
LUA: .reasm_data
LUA:   string 16 03 01 06 1D .......
LUA: .reasm_offset
LUA:   number 0

Первый ознакомительный релиз zapret2.
Пока нет скриптов запуска и полной доки.

winws2 интегрирован в zapret-win-bundle.
Переписана стратегия preset_example.cmd в preset2_example.cmd под winws2.
В cygwin-admin.cmd создан alias “winws2-antidpi”.

Можно так писать, не заморачиваясь с путями, для тестирования стратегий

winws2-antidpi --wf-tcp-out=80 --debug --lua-desync=pass

Для максимальной производительности релизы zapret2 собираются с luajit 2.1 от openresty для всех платформ, кроме lexra и ppc. На lexra luajit невозможен в принципе, поскольку обладает не совсем стандартной системой команд mips, а на ppc были какие-то проблемы в сборке. На проблемных платформах собирается с LUA 5.4.

Загрузка модулей через “require” невозможна на статических билдах, поставляемых с релизом.
Если вам нужны модули - собирайте динамическую версию с исходников.

Имейте в виду, что lua в nfqws2 запускается с ограниченными правами.

  • Под всеми unix права те, что указаны в --user или --uid. Если не указано - uid по умолчанию 0x7FFFFFFF.
  • В linux используется seccomp фильтр, который блокирует exec и некоторые файловые операции. Читать и писать файлы - можно, но лазать по директориям (читать список файлов), создавать, удалять директории - нет.
  • Принудительно удалены из глобального пространства имен : модуль debug, os.execute, io.popen, package.loadlib. Гитхаб экшины собирают luajit без поддержки FFI. Экономит прилично размер exe-шников и убирает ненужное пространство для манипуляций.
  • Под виндой процесс выполняется с максимальными правами NT AUTHORITY/SYSTEM. Это само по себе нехорошо, но тут ничего не сделать, кроме пункта 3. Для windivert нужны админские права. Разве что запускать дочерний процесс как в броузерах, но это сильно испортит производительность и усложнит код.
  • LUA код несмотря на ограничения - все же программа, и если вам дадут какой-то скриптик, то считайте - вы запустили EXEшник. Что мог - запретил, но риск все равно есть, а под виндой он колоссален.

Частично мне удалось сделать более-менее эффективный sandbox под виндой.
Главная проблема в том, что инициализация windivert требует админских прав.
Это никак не поменять, не пересобирая драйвер, который нужно потом подписывать.
В winobj ясно видны права на Device\WinDivert как Administrators:F, SYSTEM:F
Этот девайс будет просто не открыть , не имея такого доступа.
Можно, конечно, принудительно поставить на него DACL, пока есть админ права, но это открыло бы секурити дырку

Но можно ведь открыть, потом сбросить права. Да, можно, с некоторыми оговорками.
Нельзя сбрасывать, если заданы --nlm-filter или --wlan-filter. Потому что механика обслуживания этих фильтров предполагает останов перехвата в случае отсутствия сети и повторный его запуск при появлении.
Эти параметры используются нечасто, так что польза от сброса прав все равно есть.

Как он реализован технически.
Что вообще делает админа админом ? В linux это uid=0 и caps.
В windows это привилегии Se* и группа BUILTIN/Administrators в токене текущего процесса.
Такие инструменты, как process hacker или process explorer от Руссиновича (sysinternals) показывают во вкладках security что там в токене процесса.

Привилегии Se* можно сбросить без проблем. Нам не нужно право загружать драйвер или ребутать комп. Драйвера загружает менеджер служб, напрямую через недокументированное API - не к чему.
Привилегии сбрасываются в любом случае, вне зависимости от nlm/ssid фильтра. Оставляется только одна привилегия - SeChangeNotifyPrivilege. Она отвечает за траверс директорий, если на самой директории в ACL нет права Traverse для запрашивающего доступ. Эта привилегия стандартна и обычно включена по умолчанию для всех процессов.

Как сбросить админа ? Для этого надо заменить токен текущего процесса, убрав оттуда группу Administrators. Документированным API это сделать не получается. SetTokenInformation(TokenGroups) не дает поменять группы, возвращая ERROR_INVALID_PARAMETER. Вероятно это сделать можно через NtSetProcessInformation - недокументированной API из ntdll.dll. Но это жесткие хаки.
AdjustTokenGroups тоже не вариант, поскольку все группы получают флаг mandatory. Их нельзя задисаблить. Не получается и поставить флаг “deny only” - то, что выставляется при использовании UAC для процессов, если юзер имеет группу Administrators.
Можно только запустить дочерний процесс с другим токеном - CreateProcessAsUser. Но нам это не надо.

Есть вариант попроще. Можно поставить low mandatory label. Мандатность - фишка, придуманная еще с висты. Несколько костыльная штука. Если на обьекте не стоит в ACL mandatory label, то считается, что он имеет medium mandatory level. Если mandatory level процесса (наличие в token группах mandatory label SID) ниже mandatory label обьекта, то к нему запрещается доступ на чтение/запись/исполнение в зависимости от поставленных в mandatory ACE опций. Обычно это запрет на запись - icacls показывает как “NW” - No Write.
Получается, можно отсечь возможность записи на практически всю файловую систему.
И это хорошо, что LUA ничего не сможет записать, если не выставят на NTFS low mandatory на разрешенные директории, куда можно писать

Как можно посмотреть или изменить mandatory level ?

icacls dir
icacls dir  /setintegritylevel low

Типичный пример, где это используется - %USERPROFILE%/AppData

cd "%USERPROFILE%\AppData"
icacls Local
      BUILTIN\Administrators:(I)(OI)(CI)(F)
      COMP\username:(I)(OI)(CI)(F)

Successfully processed 1 files; Failed processing 0 files

icacls LocalLow
         BUILTIN\Administrators:(I)(OI)(CI)(F)
         COMP\username:(I)(OI)(CI)(F)
         Mandatory Label\Low Mandatory Level:(OI)(CI)(NW)

LocalLow - это appdata для процессов под текущим юзером, имеющих low mandatory level.
И некоторые процессы действительно сбрасывают свои права. Замечал, что это делают некоторые игры и пишут сейвы в LocalLow.
Броузеры вообще ставят Untrusted mandatory level для своих дочерних процессов. Часть механизма песочницы.

Так что если ваш LUA код хочет писать какие-то файлы, то придется для этого ставить low mandatory label, если, конечно, у вас нет nlm или ssid фильтра в параметрах winws2.
Или писать в LocalLow

На все файлы, создаваемые в C коде еще до сброса мандатности в low, буду принудительно ставить low mandatory. Это логи прежде всего и автолисты. Иначе будет их потом не записать

Прикинул что делать с nlm/ssid filter.
Решил, что при их использовании лучше, пользуясь пока еще не сброшенными правами админа, поставить low mandatory label на девайс \\.\\WinDivert и ставить тот же label себе на процесс , то есть сбрасывать права.
Чем это грозит ?
DACL не меняется, там по-прежнему полный доступ для administrators и system. Меняется только mandatory label в SACL.
Значит юзера без группы administrators ничего не получают. Так же не получают ничего юзера, у кого есть группа administrators, но используется UAC и не было элевации. Группа administrators в их токене стоит в режиме “deny only”.
Чтобы что-то получить из того, чего иначе получить было нельзя, только сценарий, когда процесс имеет включеную группу Administrators и сам себе сделал песочницу - поставил себе в токен low mandatory label. Если вдруг был запущен winws2 с фильтром ssid/nlm , то такой процесс получает возможность работать с windivert, которую иначе бы не имел.
Мне кажется это небольшой риск.

Зато по поводу левых скриптиков LUA мне гораздо спокойней. Этот вопрос надо решить сейчас, на старте.

Выставляю env variable, чтобы LUA сразу без манипуляций могла узнать куда можно писать на любой системе.
Путь в формате cygwin unix-style

--lua-init="print(os.getenv('APPDATALOW'))"
/cygdrive/C/Users/user/AppData/LocalLow
--lua-init="print(os.getenv('WRITABLE'))"
/cygdrive/C/Users/user/AppData/LocalLow/zapret2"

lua не может создавать директории - нет в стандартном API
диру создает сам winws2

Все-таки нехорошо просто так вне зависимости от нужды оставлять пустую директорию на системе, по которой можно точно определить, что запускался zapret2.

Поэтому несколько расширим понятие “writable dir”.
По умолчанию ее нет. Под виндой по-прежнему и безусловно выставляется env APPDATALOW.
Если скрипт хочет куда-то писать внутрь этой диры - пожалуйста. Он должен будет знать путь, тк в LUA без модулей не посмотреть ls и не создать dir. В статик билде их загрузить невозможно вообще под unix. Под виндой можно, наверно, не проверял. Но требуется DLL.
Путь можно передавать относительный, чтобы скрипт его конкатенировал с os.getenv('APPDATALOW'). Так избавляемся от зависимости от конкретной системы, имени юзера и локации “special folder”.
Чтобы создать writeable dir, нужно указать параметр --writeable[=dir].
Без параметров выбирается путь по умолчанию. Для винды - %APPDATALOW/zapret2, для unix - $TMPDIR/zapret2, а если переменной TMPDIR нет - /tmp/zapret2.
Создается директория. Для винды ставится low mandatory с наследованием на container/object (субдиректории и файлы внутри), для unix делается chown на target user. Если он не прокатывает, проверяется доступность директории на запись через создание и удаление временного файла. Если нет - ошибка и останов.

Под linux nfqws/nfqws2 можно запускать без рута, но с cap_net_raw и cap_net_admin. Если нет прав на смену юзера, она отменяется, выполнение продолжается как есть.
Достичь этого можно или через setcap на исполняемый файл, или через ambient caps в systemd unit.

Writeable dir можно предсоздать, дать на нее право через group + w без назначения юзера nfqws владельцем. Тест через временный файл пройдет, ошибки не будет

Локация writable dir передается LUA через env “WRITEABLE”

В lua функции передаются аргументы --lua-desync=func:arg1=val1:arg2=val2. Функция их получает через таблицу desync.arg
Сейчас только статические строки. Если val нет, то передается пустая строка.
В некоторых случаях это не очень удобно.
Сделаю вот так :
arg=%val - разименование блоба. Берется значение переменной desync.val, если ее нет, то глобальную переменную val, подставляется значение.
arg=#val - то же самое, но подставляется длина строки блоба. так удобно будет делать seqovl на размер tls hello
Чтобы передать в начале символы % или # как есть, будет эскейп \\% и \\%#
Подстановка будет на уровне C кода прозрачно для LUA.
Если блоба нет - параметр будет ставиться nil, то есть попросту убираться.

Пример использования. Надо послать исходный запрос с известным пейлоадом с seqovl случайного размера от 5 до 10 символов со случайным содержимым, состоящим из букв от ‘a’ до ‘z’.
Здесь раскрывается не декларативный характер стратегий, а алгоритмический. Стратегия - это программа, и пишите ее вы на языке программирования. А для облегчения простых или стандартных действий есть готовые средства, так что далеко не всегда надо писать свою функцию. Частенько можно обойтись простенькими кусками lua кода в дополнение к имеющимся.

--lua-desync=luaexec:code='desync.rnd=brandom_az(math.random(5,10))' --lua-desync=tcpseg:pos=0,-1:seqovl=#rnd:seqovl_pattern=rnd --lua-desync=drop:payload=known

В будущем блокчек2 будут выкинуты PKTWS_EXTRA. Потому что изменился способ передачи параметров, дописывать параметры командной строки особо нет смысла.
Они использовались в основном для репитов, фейков, паттернов.
Но надо как-то передавать наиболее распространенные модификаторы.
Буду это делать через конкретные переменные. Тупо FAKE_REPEATS, FAKE_HTTP, FAKE_TLS, FAKE_QUIC, SEQOVL_PAT_HTTP, SEQOVL_PAT_TLS.
А что не вписывается - будет blockcheck.d. Пишите туда код на шелле типа кастом скрипта. Или используете проверялку стратегий из файла построчно.

Простенький tcpdump, позволяющий записать что проходит по инстансу “pcap” в cap file и посмотреть шарком
nfqws2 --qnum 200 --debug --lua-init=@zapret-lib.lua --lua-init=@zapret-pcap.lua --writeable=zdir --in-range=a --lua-desync=pcap:file=test.pcap

пишет в zdir/test.pcap

В винде по умолчанию на уровне wf фильтра отсекаются пустые tcp пакеты без FIN,RST,SYN для экономии CPU. Чтобы их включить нужно указывать --wf-tcp-empty

Может использоваться для диагностики или как замена tcpdump, когда на роутере мало места, и нормальный tcpdump не влезает, а zapret2 уже там. Правда, будет работать только по tcp/udp, все остальное - нет.

И в догонку динамические “фишки”. Если несколько инстансов (лоад балансинг настроили), как сделать, чтобы все они писали в файл test<PID>.cap ? Можно, конечно, внешними средствами в шелле менять параметры. А можно так :

nfqws2 --qnum 200 --debug --lua-init=@lua/zapret-lib.lua --lua-init=@lua/zapret-pcap.lua --writeable=zdir --in-range=a --lua-init="capf='test'..getpid()..'.cap'" --lua-desync=pcap:file=%capf

Кстати, получение raw пакета для записи в файл делается через отдельный вызов C функции raw_packet(ctx). Поскольку пакет в таком виде редко востребован, пихать его в desync - значит зря расходовать проц на копирование памяти. Все присваивания lua переменных из C кода означают копирование памяти.

Блокчек2 будет еще менее копипастный, чем блокчек1.
Даже пытаться не буду сделать , чтобы было удобно копировать.
Надо понимать что написано в стратегии, чтобы это перенести в итоговый конфиг.
Надо понимать как обьединить части для разных версий ip, протоколов.
Без этого будет совсем тяжело.
Талмуд, надеюсь, напишу. И он будет нести цель обьяснить как это работает, а не что куда вписать. Хотя на примерах обьяснять, наверно, буду. Для входа в сложный софт очень полезно.

Принцип работы zapret2 пляшет то того, что вы хотите сделать с трафиком. Это ключ.
Дальше вы разбираетесь как это сделать с помощью инструмента. Это средство.
И никак иначе.

Иначе только : задача - вызов мастера - решение - (потенциально) оплата - профит