WinDivert фильтры (--wf-*) в zapret2 для Windows
WinDivert — это драйвер для Windows, который позволяет перехватывать и модифицировать сетевые пакеты на уровне ядра. В zapret2 для Windows используется программа winws2 (вместо nfqws2 для Linux), которая работает с WinDivert.
WinDivert фильтр применяется ПЕРВЫМ — это фундаментальная архитектура.
Порядок фильтрации (pipeline):
┌─────────────────────────────────────────────────────────────────┐
│ СЕТЕВОЙ СТЕК │
│ (все пакеты) │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1️⃣ WinDivert ФИЛЬТР (--wf-*) │
│ [уровень драйвера] │
│ │
│ Пропускает только пакеты, соответствующие фильтру │
│ Остальные идут мимо nfqws напрямую в сеть │
└──────────────────────────┬──────────────────────────────────────┘
│ только захваченные пакеты
▼
┌─────────────────────────────────────────────────────────────────┐
│ nfqws │
│ │
│ 2️⃣ --filter-tcp/udp — фильтр по портам │
│ 3️⃣ --ipset — фильтр по IP │
│ 4️⃣ --hostlist — фильтр по доменам (из payload) │
│ 5️⃣ desync actions — применение обхода DPI │
└─────────────────────────────────────────────────────────────────┘
Почему это важно:
- Производительность: WinDivert фильтрует на уровне драйвера — CPU-efficient
- nfqws видит только нужные пакеты — не тратит ресурсы на разбор всего трафика
--wf-*= грубый фильтр (порты, протоколы, IP версия)--hostlist= тонкий фильтр (по содержимому пакета, доменам)
Да, --wf-* применяется первым, на уровне драйвера, до того как пакет попадёт в nfqws.
📦 Основные параметры WinDivert фильтров
1. --wf-iface - Фильтр по сетевому интерфейсу
Синтаксис:
--wf-iface=<int>[.<int>]Параметры:
int- индекс сетевого интерфейсаint.int- индекс интерфейса и подинтерфейса
Примеры:
--wf-iface=1 # интерфейс 1
--wf-iface=2.0 # интерфейс 2, подинтерфейс 02. --wf-l3 - Фильтр L3 протокола
Синтаксис:
--wf-l3=ipv4|ipv6Параметры:
ipv4- только IPv4ipv6- только IPv6- Можно указывать несколько через запятую
Примеры:
--wf-l3=ipv4
--wf-l3=ipv4,ipv63. --wf-tcp-in - TCP входящие порты
Синтаксис:
--wf-tcp-in=[~]port1[-port2]Параметры:
port- конкретный портport1-port2- диапазон портов~port- отрицание (все кроме)- Можно указывать несколько через запятую
Примеры:
--wf-tcp-in=80,443 # входящие на порты 80 и 443
--wf-tcp-in=1000-2000 # диапазон
--wf-tcp-in=~443 # все кроме 4434. --wf-tcp-out - TCP исходящие порты
Синтаксис:
--wf-tcp-out=[~]port1[-port2]Параметры: (аналогично --wf-tcp-in)
Примеры:
--wf-tcp-out=80,443 # исходящие на порты 80 и 4435. --wf-udp-in - UDP входящие порты
Синтаксис:
--wf-udp-in=[~]port1[-port2]Параметры: (аналогично TCP)
Примеры:
--wf-udp-in=53,443 # DNS и QUIC входящие6. --wf-udp-out - UDP исходящие порты
Синтаксис:
--wf-udp-out=[~]port1[-port2]Параметры: (аналогично TCP)
Примеры:
--wf-udp-out=443 # QUIC исходящие7. --wf-tcp-empty - Обработка пустых TCP пакетов
Синтаксис:
--wf-tcp-empty=[0|1]Параметры:
0- не обрабатывать пустые TCP пакеты без флагов SYN/RST/FIN (по умолчанию)1- обрабатывать пустые TCP пакеты
Примеры:
--wf-tcp-empty=1 # включить обработку пустых пакетов8. —wf-raw-part - Частичный raw фильтр ⭐
Синтаксис:
--wf-raw-part=<filter>|@<filename>Параметры:
filter- строка фильтра на языке WinDivert@filename- загрузить фильтр из файла- Можно указывать несколько раз - они объединяются через OR
Особенности:
- Фильтры работают на уровне ядра (очень быстро)
- Язык фильтров похож на tcpdump/Wireshark
- Позволяет фильтровать по содержимому payload
- Экономит CPU, не перенаправляя весь поток
Примеры:
# Прямо в командной строке
--wf-raw-part="outbound and udp.DstPort=443 and udp.PayloadLength>=256"
# Из файла
--wf-raw-part=@windivert_part.discord_media.txt
# Несколько фильтров
--wf-raw-part=@discord.txt --wf-raw-part=@stun.txt --wf-raw-part=@quic.txt9. --wf-filter-lan - Фильтр локальных сетей
Синтаксис:
--wf-filter-lan=0|1Параметры:
0- не фильтровать локальные сети1- исключить локальные IP адреса (по умолчанию)
Исключаемые диапазоны:
- IPv4: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16
- IPv6: ::1/128, fc00::/7, fe80::/10, ff00::/8
Примеры:
--wf-filter-lan=0 # не исключать локальные сети10. --wf-raw - Полный raw фильтр
Синтаксис:
--wf-raw=<filter>|@<filename>Параметры:
filter- полная строка фильтра WinDivert@filename- загрузить из файла- ЗАМЕНЯЕТ все
--wf-tcp,--wf-udp,--wf-raw-part
Примеры:
--wf-raw="!impostor and !loopback and outbound and tcp.DstPort=443"
--wf-raw=@full_filter.txt11. --wf-save - Сохранить фильтр и выйти
Синтаксис:
--wf-save=<filename>Параметры:
filename- путь для сохранения сгенерированного фильтра- После сохранения программа завершается
Примеры:
--wf-tcp-out=80,443 --wf-save=generated_filter.txt🔤 Язык фильтров WinDivert
Основные операторы:
Логические:
- and - И
- or - ИЛИ
- not или ! - НЕ
Сравнения:
- == или = - равно
- != - не равно
- <, >, ⇐, >= - сравнения
Направление:
- outbound - исходящие пакеты
- inbound - входящие пакеты
Специальные:
- impostor - пакеты от других драйверов
- loopback - loopback интерфейс
Доступные поля:
IP (IPv4):
ip # IPv4 пакет
ip.SrcAddr # IP источника
ip.DstAddr # IP назначения
ip.Length # Длина IP пакета
ip.TTL # Time To Live
ip.Protocol # Протокол (6=TCP, 17=UDP)
IPv6:
ipv6 # IPv6 пакет
ipv6.SrcAddr # IPv6 источника
ipv6.DstAddr # IPv6 назначения
ipv6.Length # Длина пакета
ipv6.HopLimit # Hop Limit
ipv6.NextHdr # Следующий заголовок
TCP:
tcp # TCP пакет
tcp.SrcPort # Порт источника
tcp.DstPort # Порт назначения
tcp.Syn # SYN флаг
tcp.Rst # RST флаг
tcp.Fin # FIN флаг
tcp.Ack # ACK флаг
tcp.Psh # PSH флаг
tcp.PayloadLength # Длина payload
tcp.Payload[N] # Байт payload по индексу N
tcp.Payload16[N] # 16-bit значение (little-endian)
tcp.Payload32[N] # 32-bit значение (little-endian)
UDP:
udp # UDP пакет
udp.SrcPort # Порт источника
udp.DstPort # Порт назначения
udp.PayloadLength # Длина payload
udp.Payload[N] # Байт payload по индексу N
udp.Payload16[N] # 16-bit значение (little-endian)
udp.Payload32[N] # 32-bit значение (little-endian)
Интерфейс:
ifIdx # Индекс интерфейса
subIfIdx # Индекс подинтерфейса
📝 Примеры фильтров
Пример 1: QUIC Initial (IETF)
outbound and
udp.PayloadLength>=256 and
udp.Payload[0]>=0xC0 and udp.Payload[0]<0xD0 and
udp.Payload[1]=0 and udp.Payload16[1]=0 and udp.Payload[4]=1
Пример 2: STUN
outbound and
udp.PayloadLength>=20 and
udp.Payload32[1]=0x2112A442 and udp.Payload[0]<0x40
Пример 3: WireGuard
outbound and
udp.PayloadLength=148 and
udp.Payload[0]=0x01
Пример 4: Discord Media
outbound and ip and
udp.DstPort>=50000 and udp.DstPort<=50099 and
udp.PayloadLength=74 and
udp.Payload32[0]=0x00010046 and
udp.Payload32[2]=0 and
udp.Payload32[3]=0
Пример 5: TLS Client Hello
outbound and
tcp.PayloadLength>=5 and
tcp.Payload[0]=0x16 and
tcp.Payload[1]=0x03 and
tcp.Payload[2]>=0x01 and tcp.Payload[2]<=0x03
Пример 6: HTTP Redirect (302/307)
tcp.PayloadLength>=12 and
tcp.Payload32[0]==0x48545450 and
tcp.Payload16[2]==0x2F31 and
tcp.Payload[6]==0x2E and
tcp.Payload16[4]==0x2033 and
tcp.Payload[10]==0x30 and
(tcp.Payload[11]==0x32 or tcp.Payload[11]==0x37)
💡 Полные примеры использования
Пример 1: Базовый перехват HTTP/HTTPS
winws2 ^
--wf-tcp-out=80,443 ^
--lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua ^
--filter-tcp=80,443 --filter-l7=http,tls ^
--lua-desync=fake:blob=fake_default_tlsПример 2: С частичными фильтрами
winws2 ^
--wf-tcp-out=80,443 ^
--wf-raw-part=@windivert_part.discord_media.txt ^
--wf-raw-part=@windivert_part.stun.txt ^
--wf-raw-part=@windivert_part.quic_initial_ietf.txt ^
--lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua ^
--filter-tcp=80,443 --filter-l7=http,tls ^
--lua-desync=fake:blob=fake_default_tls ^
--new ^
--filter-l7=quic,stun,discord ^
--lua-desync=fake:blob=fake_default_quic:repeats=2Пример 3: Только QUIC с фильтром
winws2 ^
--wf-raw-part=@windivert_part.quic_initial_ietf.txt ^
--lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua ^
--filter-l7=quic ^
--lua-desync=fake:blob=fake_default_quic:repeats=6Пример 4: Полный raw фильтр
winws2 ^
--wf-raw="!impostor and !loopback and outbound and tcp.DstPort=443 and tcp.PayloadLength>0" ^
--lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua ^
--filter-tcp=443 ^
--lua-desync=fake:blob=fake_default_tls⚠️ Важные особенности и ограничения
1. Отсутствие conntrack
- WinDivert НЕ отслеживает соединения (нет аналога connbytes из iptables)
- Если перехватывать порт целиком, весь трафик идет через winws2
- Это нагружает процессор при передаче больших объемов данных
2. Ограничения языка фильтров
- Нет операций с битовыми полями
- Нет сдвигов (shift)
- Нет побитовой логики (AND, OR, XOR)
- Фильтры могут пропускать неправильные пакеты
- Дофильтрация выполняется в winws2
3. Рекомендации
- Используйте
--wf-raw-partдля точной фильтрации payload - Это экономит CPU, отсекая ненужный трафик на уровне ядра
- Комбинируйте несколько
--wf-raw-partдля разных протоколов
4. Автоматическая генерация
- winws2 автоматически добавляет
!impostor and !loopback - Автоматически добавляет фильтр для SYN/RST/FIN (для conntrack)
- Автоматически добавляет фильтр HTTP redirect (302/307)
- Автоматически исключает локальные сети (если
--wf-filter-lan=1)
📚 Документация WinDivert
Полная документация языка фильтров: https://reqrypt.org/windivert-doc.html#filter_language
🎯 Резюме
WinDivert фильтры — это мощный механизм для перехвата пакетов в Windows на уровне ядра:
--wf-tcp-in/out,--wf-udp-in/out— простая фильтрация по портам--wf-raw-part— частичные фильтры по содержимому payload (рекомендуется!)--wf-raw— полный контроль над фильтром- Язык фильтров — похож на tcpdump, но с ограничениями
- Производительность — фильтры работают в ядре, очень быстро
Используйте --wf-raw-part для оптимальной производительности и точной фильтрации!
Разница между --wf-tcp-out=80,443 и --wf-tcp-out=80 --wf-tcp-out=443
🔍 Анализ кода
Изучив функцию wf_make_pf в коде (строки 1193-1222), я могу точно объяснить разницу:
✅ Вариант 1: --wf-tcp-out=80,443 (ПРАВИЛЬНЫЙ)
Что происходит:
- Параметр обрабатывается один раз
- Функция
wf_make_pfпарсит строку"80,443"и разбивает по запятой - Генерируется один фильтр WinDivert:
((tcp.DstPort == 80) or (tcp.DstPort == 443))
Результат:
- ✅ Один вызов парсера
- ✅ Оптимальный фильтр
- ✅ Меньше строковых операций
❌ Вариант 2: --wf-tcp-out=80 --wf-tcp-out=443 (НЕПРАВИЛЬНЫЙ)
Что происходит:
- Первый параметр
--wf-tcp-out=80обрабатывается - Генерируется фильтр:
((tcp.DstPort == 80)) - Второй параметр ПЕРЕЗАПИСЫВАЕТ первый!
- Генерируется новый фильтр:
((tcp.DstPort == 443))
Результат:
- ❌ Первый порт (80) теряется!
- ❌ Работает только последний указанный порт (443)
- ❌ Два вызова парсера (лишняя работа)
📊 Доказательство из кода
Посмотрим на обработку параметра (строки 2330-2337):
case IDX_WF_TCP_OUT:
hash_wf_tcp_out = hash_jen(optarg, strlen(optarg));
if (!wf_make_pf(optarg, "tcp", "SrcPort", params.wf_pf_tcp_src_out, WINDIVERT_PORTFILTER_MAX) ||
!wf_make_pf(optarg, "tcp", "DstPort", params.wf_pf_tcp_dst_out, WINDIVERT_PORTFILTER_MAX))
{
DLOG_ERR("bad value for --wf-tcp-out\n");
exit_clean(1);
}
break;Ключевой момент: Каждый вызов --wf-tcp-out перезаписывает буферы params.wf_pf_tcp_src_out и params.wf_pf_tcp_dst_out, а не добавляет к ним!
🔬 Как работает wf_make_pf
static bool wf_make_pf(char *opt, const char *l4, const char *portname, char *buf, size_t len)
{
// ...
for (n = 0, p = opt, *buf = '(', buf[1] = 0; p; n++) // Начинает с '('
{
if ((e = strchr(p, ','))) // Ищет запятую
{
c = *e;
*e = 0;
}
if (!pf_parse(p, &pf)) return false;
// Генерирует условие для порта
if (pf.from == pf.to)
snprintf(s1, sizeof(s1), "(%s.%s %s %u)", l4, portname, pf.neg ? "!=" : "==", pf.from);
if (n) strncat(buf, " or ", len - strlen(buf) - 1); // Добавляет "or" между портами
strncat(buf, s1, len - strlen(buf) - 1);
// ...
}
strncat(buf, ")", len - strlen(buf) - 1); // Закрывает скобку
return true;
}Функция:
- Парсит строку по запятым
- Для каждого порта генерирует условие
- Объединяет их через
or - Возвращает один готовый фильтр
📈 Производительность
Вариант 1: --wf-tcp-out=80,443
Время парсинга:
- 1 вызов
wf_make_pf - 2 итерации цикла внутри функции
- 1 выделение строки
Итоговый фильтр:
((tcp.DstPort == 80) or (tcp.DstPort == 443))
Производительность WinDivert: ⚡ Оптимальная
- Один компактный фильтр
- Эффективная проверка в ядре
Вариант 2: --wf-tcp-out=80 --wf-tcp-out=443
Время парсинга:
- 2 вызова
wf_make_pf - 2 выделения строк
- Первый результат выбрасывается!
Итоговый фильтр:
((tcp.DstPort == 443)) // Порт 80 потерян!
Производительность WinDivert: ❌ Неправильная работа
- Фильтрует только последний порт
- Порт 80 не перехватывается!
🎯 Вывод
✅ Используйте:
--wf-tcp-out=80,443Преимущества:
- ✅ Работает правильно - оба порта перехватываются
- ✅ Быстрее парсинг - один вызов вместо двух
- ✅ Меньше памяти - одна строка вместо двух
- ✅ Читабельнее - короче командная строка
❌ НЕ используйте:
--wf-tcp-out=80 --wf-tcp-out=443Проблемы:
- ❌ Не работает! - только последний порт (443)
- ❌ Медленнее - лишний вызов функции
- ❌ Больше кода - длиннее командная строка
- ❌ Теряются данные - первый порт (80) игнорируется
📋 Правильные примеры
# Правильно: несколько портов через запятую
--wf-tcp-out=80,443
--wf-tcp-out=80,443,8080,8443
--wf-udp-out=53,443,853
# Правильно: диапазоны
--wf-tcp-out=1000-2000
--wf-tcp-out=80,443,8000-9000
# Правильно: отрицание
--wf-tcp-out=~443
--wf-tcp-out=~80,~443
# НЕПРАВИЛЬНО: повторяющиеся параметры
--wf-tcp-out=80 --wf-tcp-out=443 # ❌ Работает только 443!🔧 Техническое объяснение
Параметры командной строки в getopt_long обрабатываются последовательно. Когда встречается один и тот же параметр дважды, второй вызов перезаписывает результат первого, потому что они записываются в один и тот же буфер (params.wf_pf_tcp_dst_out).
Это не баг, а особенность дизайна - параметры WinDivert фильтров не предназначены для множественного указания. Вместо этого используется встроенная поддержка списков через запятую.
Итог: Всегда используйте запятые для перечисления портов в одном параметре. Это правильно, быстрее и работает как задумано!